Atom

思考、故事和创意

编程

对 Python 异常处理的笔记

简述 Pro Python 这本书里对 Python 异常处理的讨论。

包括异常的捕获、处理,异常链,以及异常处理相关的 else, finally, with 关键字等内容。


最简单的捕获异常的方式

def count_lines(filename):
    try:
        return len(open(filename, 'r').readlines())
    except:
        return 0

很少有人在生产环境写这样的代码,它会捕获所有的异常,可能包括你希望处理的,也包括你其实并不想处理的异常,都会走到 except 下的代码块里,执行了 return 0 这行代码。多数情况下,这可能不是我们想要的结果。

好的做法是明确写出自己希望要捕获、要处理的异常类型。

def count_lines(filename):
    try:
        return len(open(filename, 'r').readlines())
    except IOError:
        return 0

这样如果出现 IOError 类型的异常(比如文件不存在,不可读等),会执行 return 0 语句。如果出现了不是 IOError 的异常,比如给这个函数传进去一个字典作为参数,则会抛出 TypeError 异常。

建议使用 logging 模块来输出异常信息,由 as 关键字来提供一个包含了异常信息的变量。

import logging

def count_lines(filename):
    try:
        return len(open(filename, 'r').readlines())
    except IOError as e:
        logging.error(e)
        return 0

捕获多个异常

如果要捕获多个异常,而且希望对这多个异常都执行一样的处理逻辑

def count_lines(filename):
    try:
        return len(open(filename, 'r').readlines())
    except (EnvironmentError, TypeError):
        logging.error(e)
        return 0

这样捕获到 EnvironmentErrorTypeError进行同样的处理。

如果要捕获多种异常,对各种异常做不同的处理

import logging

def count_lines(filename):
    try:
        return len(open(filename, 'r').readlines())
    except TypeError as e:
        logging.error(e)
        return 0
    except EnvironmentError as e:
        logging.error(e.args[1])
        return 0

这里对 TypeError 异常,输出错误的内容(e),如果捕获到 EnvironmentError 异常,输出 e.args[1](EnvironmentError 异常里,e.args[0] 是错误码,e.args[1] 是错误信息)。

Exception Chain

如果在处理异常的过程中,这块处理异常的代码又出现了一个异常,像下面这段代码

def get_value(dictionary, name):
	try:
    	return dictionary[name]
    except Exception as e:
    	log = open('logfile.txt', 'w')
        log.write('%s\n' % e)
        log.close()

这样调用 get_value 函数

get_value({}, 'test')`

会走到异常处理的逻辑,如果 logfile.txt 是只读的,那么会出现另外一个异常。

Traceback (most recent call last):
...
KeyError: 'test'
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
...
IOError: [Errno 13] Permission denied: 'logfile.txt'

写文件时出现的这个异常会额外包含一个 __context__ 属性,它保存着之前的那个异常(KeyError)

下面是另外一个例子,主动抛出 (raise) 的异常

def validate(value, validator):
	try:
    	return validator(value)
    except Exception as e:
    	raise ValueError('Invalid value: %s' % value) from e

def validator(value):
	if len(value) > 10:
    	raise ValueError("Value can't exceed 10 characters")

这样调用 validate 函数

validate(False, validator)`

会抛出异常,输出以下信息:

Traceback (most recent call last):
...
TypeError: object of type 'bool' has no len()
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
...
ValueError: invalid value: False

最新的异常会被抛出(validate 函数里主动抛出的 ValueError 异常),这个异常里包含 __context__ 属性(其中包含执行 validator(value)时的 TypeError 异常 )。

else

如果一段代码需要在没有出现任何异常的情况才执行,使用 else 关键字。

import logging

def count_lines(filename):
    try:
        file = open(filename, 'r')
    except TypeError as e:
        logging.error(e)
        return 0
    except EnvironmentError as e:
        logging.error(e.args[1])
        return 0
    else:
        return len(file.readlines())

finally

如果不论是否产生了异常,都要执行某段代码,用 finally 关键字

import logging

def count_lines(filename):
	file = None
    try:
        file = open(filename, 'r')
        lines = file.readlines()
    except TypeError as e:
        logging.error(e)
        return 0
    except EnvironmentError as e:
        logging.error(e.args[1])
        return 0
    except UnicodeDecodeError as e:
        logging.error(e)
        return 0
    else:
        return len(file.readlines())
    finally:
        if file:
            file.close()

如果没有异常出现,会走 else 分支的逻辑,也会走 finally 的逻辑,这个函数的返回值是 else 分支的 return 语句返回的内容。(else 分支先于 finally 执行,并且 else 分支里有 return,但是 finally 分支还是会执行。)如果在 finally 分支里返回,那么函数的返回值就是 finally 分支里返回的内容。

类似的,如果在一个循环里使用了 finally,那么即使在执行这次循环里有 continue 或者 break 中断了这一次或中断整个循环,在这一次循环内的 finally 块的代码还是会执行。

for i in range(3):
    try:
        a = {}
        print(a['hello'])
    except:
        print('continue')
        continue
    finally:
        print('finally')

这段代码的输出是

continue
finally
continue
finally
continue
finally

with

可能有时候想达到这样的效果:有异常就往外抛,不去捕获,但是最后有个 finally 分支去做一些必要的清理工作。比如这样

def count_lines(filename):
    file = open(filename, 'r')
    try:
        return len(file.readlines())
    finally:
        file.close()

如果函数的第一行打开文件就出现了异常,那就抛出异常,下面的代码就不执行了。如果文件正常打开,在 try 块里的代码不论出现什么样的异常都会正常地抛出,最终都会在 finally 块里执行 file.close() 关闭文件。用异常处理来做这样的事,有点“浪费”。更好的方法是用 with 关键字。

def count_lines(filename):
    with open(filename, 'r') as file:
        return len(file.readlines())

with 这一块代码中,有异常就会抛出。对于 open 打开文件的操作,出了 with 这一块,会自动关闭打开的文件。效果跟前一段代码一样,代码变漂亮不少。

好好写 Shell 脚本

shell 种类众多,并且语法各异,如果自己又不熟悉任何一种 shell 的话,就会经常感觉语法怪异,而且似乎不够严谨,甚至有时候要边搜边写,这就使得一些脚本成为了一些勉强可用的语句的拼凑,几乎不可维护。即便是一些所谓的“技术大牛”,各种高大上的词都能吹得天花乱坠的,也写不了像样的脚本,这是个蛮尴尬的事情,固然是术业有专攻,但是写个 Linux Shell 脚本,应该算是个基础(其实有可能他们连链表怎么实现都不知道)。这里主要是说最为通用的 bash,以下是几条 bash “代码规范”。

1. 及早退出

脚本的开头,#! 语句之后,加上这几行

set -o errexit
set -o pipefail

第一行语句的作用是,在脚本执行过程中,如果有错误,就退出脚本,不再继续下去。如果一条语句执行完,返回值不是 0,就是错误。

第二行语句的作用是,如果脚本中有一行命令是由一个或多个管道连起来的多个命令,如果其中有一条或者多条命令出现错误(返回非 0 值),这一整行命令返回的结果就是最后那条返回失败结果的命令的返回值。

如果预期一条语句可能会产生失败,就自己做好判断以及之后的处理,可以使用 if 语句或者 &&||。例如:

nc -z 8.8.8.8 53 && echo "OK" || echo "FAIL"

如果能连上 8.8.8.8 的 53 端口,则结果为真,输出 OK,如果连接不上,也就是最开始的语句结果为假,echo "OK" 就被“短路”了,最后输出了 FAIL。

2. 使用函数

在 bash 里这样定义和调用函数

func_name()
{
    echo "do something"
}

func_name

在函数中 $1, $2 等等就是第一个参数,第二个参数等。

  • func_name { ... } 跟上面的区别是函数名后直接是函数内容,这种定义函数的方法已经被废弃
  • 一个脚本要有一个 main 函数,直接写 main 进行调用,或者 main "$@" 把命令行参数传给它

使用函数的好处跟其他语言中使用函数的好处一样,但是很长时间以来,都不是很习惯在 shell 脚本里用函数,或许是最开始受“shell 脚本就是命令的堆积”这样一个指示的影响。

3. 更好地使用全局变量、局部变量

这跟使用函数一样是放之四海皆准的规范:尽量避免使用全局变量。除非是些全局用的常量,定义这种变量时,用 readonly 对变量进行修饰。在函数内部使用局部变量,用 local 修饰变量。

为什么要显式地去声明一个变量是局部变量?注意一个天大的误会,不像 C 语言,在一个函数里的变量就是局部变量,在 bash 里,函数里的变量如果没有用 local 修饰,那么它是一个全局变量!或许这是认为 bash 比较怪异的一个原因。

如果要在一个函数内定义一个只读的全局变量,使用 declare -r var_name 或者 local -r var_name,在函数内通过 declarelocal 声明的变量都是局部变量,-r 参数使变量只读。

declarelocal 作用非常类似,不过 local 只能在函数内使用,declare 在函数内外都可以使用。在函数内用 declare 声明的是局部变量,在函数外使用 declare 的变量自然是全局变量。

4. 其他细节

  • 使用 [[ ... ]] 而不是使用 [ ... ] 或者 test 进行判断
  • 使用 $( ... ) 代替丑恶的反引号 ```
  • 不要有光秃秃的 $ 符号,把变量都放进双引号,例如 "$var"
  • 大多数情况下都使用双引号,除非有强有力的原因
  • 简单的条件判断不需要用 if,用 &&|| 即可,就像开头 nc -z 8.8.8.8 53 && echo "OK" || echo "FAIL" 这个例子
  • then, do 等等,放到 if, for 等关键字的同一行

利用 git 分支的开发流程

这篇 2010 年的文章 A successful Git branching model 讲了一个比较合适的使用 git 进行版本控制的流程。主要是有两个主要分支:master 和 develop 分支,和其他辅助分支:feature 分支、release 分支、hotfix 分支。

主要分支

每个 repo 包含两个永远都不删除的分支

  • master 生产环境的版本
  • develop 待发布的最新开发代码分支

develop 分支不会进行很大的功能、特性方面的开发。当 develop 分支稳定,准备发布了,把它(经过 release 辅助分支)合并到 master 分支,然后以版本号打一个标签。

辅助分支

辅助分支最终会被删除,一般是这几个分支类型:feature 分支、release 分支 和 hotfix 分支

feature 分支

  • 一般由 develop 分支创建
  • 必须合并回 develop 分支
  • 不要命名为 master, develop, release-*, hotfix-* 这样的分支名称

通常这类分支只在开发者本地仓库,不在远程代码仓库里。

流程示例

git checkout -b myfeature develop

git checkout develop
git merge --no-ff myfeature
git branch -d myfeature
git push origin develop

release 分支

  • 一般由 develop 分支创建
  • 必须合并回 develop 和 master 分支
  • 命名约定为 release-*

用在新的正式发布版本之前,进行一些很小的 bug 修改,版本号,构建日期的修改
等等。创建 release 分支之后,就暂时不要再把包含重大新特性的 feature 分支
往 develop 分支合并了
。在创建 release 分支 时,就定下当前要发布的版本号

流程示例

git checkout -b release-1.2 develop
./bump-version.sh 1.2
git commit -a -m "Bumped version number to 1.2"

git checkout master
git merge --no-ff release-1.2
git tag -a 1.2

git checkout develop
git merge --no-ff release-1.2

git branch -d release-1.2

hotfix 分支

  • 一般从 master 分支创建
  • 必须合并回 develop 和 master 分支
  • 命名约定 hotfix-*

当线上版本出现需要立即修改的 bug 时,从 master 分支创建 Hotfix 分支。不影响其他分支的开发。

流程示例

git checkout -b hotfix-1.2.1 master
./bump-version.sh 1.2.1
git commit -a -m "Bumped version number to 1.2.1"

//fix bugs, then
git commit -m "Fixed severe production problem"

git checkout master
git merge --no-ff hotfix-1.2.1
git tag -a 1.2.1

git checkout develop
git merge --no-ff hotfix-1.2.1

如果当前有 release 分支,就要把 hotfix 分支先合并到 release 分支里,而不是develop 分支,release 分支最终还是会合并到 develop 分支里。


一点个人理解

  • master 分支不直接合并 develop 分支,更不直接合并 feature 分支。

  • feature 生存周期可长可短,由开发时长决定。多个 feature 可以同时开发,完成后合并到 develop 分支里,删除掉对应的feature分支

  • release 和 hotfix 分支的生存周期短,它们从比较稳定的 develop 和 master分支上创建出来,做小的改动后合并回 develop 和 master 分支,删除相应分支。可以说是“随用随删”的。

  • 在 release 分支里生成较大版本号,在 hotfix 分支里生成小版本号,在 master分支里按照版本号打上 tag 标签。

Tips

  • 合并分支时加上 --no-ff 参数,不要 fast-forward
  • 不要看国内的译名为“一个成功的 git 分支模型”的文章,翻译的像坨屎一样
  • 阮一峰写的Git分支管理策略 ,同样基于开头提到的文章,比较简洁清晰,比其他那些译文好一万倍。

free 命令 buffers 和 cached

大多数 Linux 用户应该都知道通过 free 命令或者 cat /proc/meminfo 查看内存使用情况。

下面是 free 命令结果的一个示例:

             total      used     free    shared    buffers    cached
Mem:       2049916   1139976   909940       680     105628    381768
-/+ buffers/cache:    652580  1397336
Swap:            0         0        0

第一行 Mem

  • 第一列值 2049916 表示总共有多少物理内存 (total)
  • 第二列 1139976 表示已使用了多少内存 (used)
  • 第三列表示未使用的内存 (free)

第二列和第三列,即 used 和 free 的值相加,等于 total 的值。第四列表示共享内存大小。第五列和第六列分别是 buffers 和 cached,都是和缓存有关的值,为了分清二者区别,需要仔细的理一下相关的知识。

第二行 -/+ buffers/cache

  • 第一列值 652580 在 used 项的下面,它表示的是第一行 used 的值减去第一行的 buffers 和 cached 的值 (2049916 - 105628 - 381768),这意味着系统除缓存外真实占用的内存有 652580KB,因为缓存相关的内存区域在内存紧张时系统会释放掉。
  • 第二行在 free 项的值 1397336 是第一行 free 的值加上第一行 buffers 和 cached 的值 (909940 + 105628 + 381768),也就是指真正能够使用的内存,空闲内存+用于缓存的内存。

第三行就是 swap 空间的总共、已用、空闲的大小。

最主要的问题是 buffers 和 cached 分别代表什么,它们的区别是什么。为了搞清楚,需要从缓存“两端”的磁盘、内存说起。

block

块设备(block device)是可以对其中的数据进行随机寻址的硬件设备,最常见的块设备就是磁盘,另外软盘、闪存等也都是块设备。读取字符设备的数据需要一个字节一个字节按顺序以数据流的方式访问,键盘就属于字符设备。

块设备最小的可寻址单元是 sector,它的大小为 2 的 N 次方,最常见的是 512 字节。sector 大小是硬件设备的物理属性。

对于软件系统来说,逻辑最小可寻址单元是 block,block 是文件系统的一个抽象,文件系统只能以 block 的倍数的大小来访问。虽然硬件设备在 sector 级别进行寻址,但是 Linux 内核执行的所有的磁盘操作都是 block 级别。

block 的大小

  • 不能小于 sector (硬件设备的最小可寻址单元)的大小
  • 不能大于内存页(physical page, 内存管理的基本单元)的大小,这是为了简化内核而做的人为限制
  • 是 2 的 N 次方

当一个 block 存储在内存里时,这块内存被称为 buffer。每个 buffer 关联一个 block,这个 buffer 代表在内存中的磁盘块。一个内存页可能包含一个或多个 block。buffer 在内核中由 buffer head (一个名为 buffer_head 的结构体)来描述。

page cache

Linux 内核的磁盘缓存叫做 page cache,把数据存储在物理内存中最大程度上减少磁盘 IO。page cache 里是 RAM 的内存页(physical page)。page cache 的大小是动态的,可能消耗掉所有未使用的内存,也可能减小以缓解机器的内存压力。

内核进行读操作时(比如某进程调用 read()),首先会检查 page cache 中的数据,如果数据在缓存中,就直接从 RAM 中读取这些数据,这是“缓存命中”。如果数据不在缓存中,就必须对磁盘进行阻塞读取操作,从磁盘读完数据后,内核会把这些数据放到 page cache,之后的读操作可能会用到。

Linux 采用 write-back 的写缓存策略,写操作直接在 page cache 上进行,不会立刻就刷新到磁盘上。写了数据的内存页标记为 dirty,放在一个列表里,这个列表里的内存页被周期性地写回到磁盘,这些内存页也就不再被标记为 dirty。

buffer cache, page cache

磁盘 block 通过 buffer 映射到内存页,这样内存页的缓存 page cache 也缓存了磁盘 block,这个缓存叫做 buffer cache,现在它基本是 page cache 的一部分。

2.4 内核之前 buffer cache 和 page cache 是分开的,前者缓存 buffer(一个磁盘 block 对应 一个 buffer),后者缓存内存页,这样就导致了磁盘 block 可能即缓存在 buffer cache 里,同时又在 page cache 里,两个 cache 可能就需要通过复制来进行同步,并且因为重复的缓存浪费了内存。对于这个情况,现在只有 page cache 了,内核用 buffer 来表示在内存中的磁盘 block,把 block 映射到内存页,而内存页在 page cache 中。

有些表示磁盘 block 的 buffer 不在 page cache 中,这里的 buffer 包含的不是文件的数据内容。例如,保存文件的元数据信息、原始的磁盘 block IO,会用到这样的 buffer。Linux 的 free 命令中 buffers 表示的就是这种 buffer 占用的内存大小。

总结起来就是,page cache 里是内存页,大部分磁盘 block(主要是常规文件) 由 buffer 映射到内存页,这部分是 buffer cache,属于 page cache 的一部分。有一小部分 buffer cache 不属于 page cache。

free

最后回到 free 命令来。

free 命令 cached 表示的是物理内存中的 page cache 占用的内存大小(page cache 可能还有部分是 swap cache,swap cache 的大小没有计算在内)。buffers 表示不属于 page cache 的那部分 buffer cache 占用的内存大小,可以想得到, buffers 的值一般都要比 cached 的值小很多。


参考:

Linux 信号处理

使用 sigaction 绑定信号

比较早的时候,使用 signal,现在正在逐渐被抛弃,sigaction 是更好的选择。主要是因为如下原因:

  • signal 在不同系统的行为可能不一致,如果自定义了信号处理函数,进入信号处理函数时,对当前信号的操作可能变为默认,也可能屏蔽该信号。只有设置信号处理方式是 SIG_IGN(忽略)、SIG_DFL(默认)是可移植的。
  • signal 不能设置在信号处理函数执行过程中屏蔽其他信号
  • 在多线程的进程中 signal 的效果不确定

signal 接口最主要的问题还是不可移植,其实现在 Linux 上的 signal 是对 sigaction 的一个封装。sigaction 有更灵活和强大的功能,现在通常使用 sigaction 绑定自己定义的信号处理函数,改变处理信号默认方式,当然用起来比 signal 也要复杂一点。

常见用法如下:

void handler(int sig, siginfo_t *siginfo, void *arg)
{
    printf("recv signal: %d\n", sig);
    sleep(2);   //假装在工作...
    printf("%s\n", "done");
}

int main(int argc, char *argv[])
{
    struct sigaction sa;
    memset(&sa, 0, sizeof(struct sigaction));

    //sigemptyset(&sa.sa_mask);
    //sigaddset(&sa.sa_mask, SIGQUIT);

    sa.sa_flags = SA_SIGINFO;
    sa.sa_sigaction = handler;

    if (sigaction(SIGINT, &sa, NULL) == -1) {
        perror("sigaction error");
        return -1;
    }

    while (1) {
        sleep(10);
    }
    return 0;
}

当进程接收到 SIGINT 信号(CTRL-C),触发 handler 函数。

如果把上面注释的两行打开,在 sa.sa_mask 添加了 SIGQUIT 信号(CTRL-\),在 handler 函数执行过程中,进程屏蔽 SIGQUIT 信号。此外还默认屏蔽与 handler 绑定的 SIGINT 信号,除非设置 sa_flagsSA_NODEFER 改变此默认行为,这样在 handler 执行过程中不会屏蔽触发 handler 的信号。

handler 执行完毕之后,会恢复进程在执行 handler 之前屏蔽的信号集,所以 SIGQUIT 不再被屏蔽。

进程屏蔽信号

更多情况下,我们是要进程/线程主动去屏蔽些信号的。需要使用上面提到的 sigprocmask,多线程环境要使用 pthread_sigmask

sigset_t set;
sigemptyset(&set);
sigaddset(&set, SIGINT);
sigaddset(&set, SIGUSR1);
// sidaddset(&set, xxxx);
sigprocmask(SIG_BLOCK, &set, NULL);

这样进程就屏蔽了 SIGINT, SIGUSR1 信号,通过 sigaddset 添加信号到一个集合里,然后调用 sigprocmask 指定屏蔽该集合里的信号。
因为 SIGINT 信号被屏蔽,也就不会触发 handler 函数。

另外,如果没有屏蔽 SIGINT,然后在 handler 内调用 sigprocmask 屏蔽 SIGINT 信号,要注意出了 handler 函数这个屏蔽作用就失效了,因为 handler 执行完,进程要屏蔽的信号集就复原了。如果要在 handler 执行过程中屏蔽某信号,建议在调用 sagaction 之前通过 sa.sa_mask 设置要在这个过程中屏蔽的信号,而不是在 handler 里通过 sigprocmask 去屏蔽,一是避免产生认为”会在整个进程执行过程中都屏蔽该信号”这样的误会,更重要的是如果进到 handler 里才去屏蔽,有可能跟预期结果不符,例如想在 handler 函数执行过程中屏蔽 SIGUSR1 信号,由于在进到 handler 函数和调用 sigprocmask 之间还有一段时间,这期间 SIGUSR1 信号是没有被屏蔽的。

屏蔽?忽略?

上面说的屏蔽信号,并不是忽略信号(或许叫做“阻塞”信号更合适)。如果一个信号被屏蔽,会被内核放在一个队列里等候着,等到进程不屏蔽它的时候,进程就会收到该信号(不再阻塞)。例如前面通过 sa.sa_mask 设定了在 handler 执行过程中屏蔽的 SIGQUIT 信号,向进程发送 SIGINT 信号触发 handler 之后,立刻又发送 SIGQUIT 信号,在 handler 执行过程中,SIGQUIT 信号被屏蔽,在 handler 执行完之后,进程就会收到该信号。

使用 sigwait 可以等待信号到来,如果队列里有等着的信号,会被取出来(先进先出的顺序)。

sigwaitsigaction 是处理信号的两种截然不同的方式,sigwait 阻塞进程/线程的执行,等待信号。

int sig_num;
sigset_t set;
sigemptyset(&set);
sigaddset(&set, SIGINT);
sigprocmask(SIG_BLOCK, &set, NULL);
while (sigwait(&set, &sig_num) == 0) {
    printf("caught sig: %d\n", sig_num);
    sleep(2);    // 假装在工作...
}

不要忘了用 sigprocmask 屏蔽了 SIGINT 信号,之后再用 sigwait 接收 SIGINT 信号,如果没有屏蔽 SIGINT 信号的话,进程收到 SIGINT,会进行默认操作,一般就是退出程序。

被忽略的

如果在 handler 执行过程中(没设置 SA_NODEFER),发送了很多次 SIGINThandler 执行完之后,进程只会收到一个 SIGINT 信号,而不是发了多少 SIGINT,之后 handler 就被触发多少次。

sigwait 等到 SIGINT 来之后进行处理,处理过程中如果向进程发送了多个 SIGINT,也是只会收到一个 SIGINTsigwait 后的处理也只有一次。

上面两个例子是说,如果进程执行过程中有信号要“等候处理”的时候,多个相同的普通信号只会有一个入队,其余的被忽略。

实时信号

实时信号是不同于普通信号的。

SIGRTMINSIGRTMAX 之间的信号属于实时信号,不会被忽略。

在上面代码段里加一行sigaddset(&set, SIGRTMIN),把 SIGRTMIN 信号的处理也接手过来,然后在 sleep(2) 的过程中,向进程发送多个 SIGINT,然后再试试发送多个 SIGRTMIN 就可以看出区别:

int sig_num;
sigset_t set;
sigemptyset(&set);
sigaddset(&set, SIGINT);
sigaddset(&set, SIGRTMIN);
sigprocmask(SIG_BLOCK, &set, NULL);
while (sigwait(&set, &sig_num) == 0) {
    printf("caught sig: %d\n", sig_num);
    sleep(2);    // 假装在工作...
}
  • "caught sig: 2" (SIGINT) 出现两次,第一次收到时输出一次,剩下多个 SIGINT 只接收到一个又输出一次;
  • "caught sig: 34" 出现多次,发送了多少 SIGRTMIN 信号,就会每隔两秒输出一次。

新东西

sigaction 时,需要小心翼翼地写异步信号安全函数(Async-Signal-Safe Function)。sigwait 处理信号相比 sigaction安全、方便,因为什么时候处理信号在自己掌握之中(同步方式),不会打断进程正在执行的操作,但是 sigwait 阻塞来等待信号可能并不满足一些场景,比如一个工作忙碌的线程,又可能需要处理某个信号。

在 2.6.22 版本的内核之后,Linux 系统提供了 signalfd 接口,一个可以替代sigactionsigwait的新方法,给这个接口一个信号集合作为参数,返回一个文件描述符,调用 read 获取信号。手册里有一个例子:

#include <sys/signalfd.h>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>

#define handle_error(msg) \
    do { perror(msg); exit(EXIT_FAILURE); } while (0)

int main(int argc, char *argv[])
{
    sigset_t mask;
    int sfd;
    struct signalfd_siginfo fdsi;
    ssize_t s;

    sigemptyset(&mask);
    sigaddset(&mask, SIGINT);
    sigaddset(&mask, SIGQUIT);

    /* Block signals so that they aren't handled
       according to their default dispositions */

    if (sigprocmask(SIG_BLOCK, &mask, NULL) == -1)
        handle_error("sigprocmask");

    sfd = signalfd(-1, &mask, 0);
    if (sfd == -1)
        handle_error("signalfd");

    for (;;) {
        s = read(sfd, &fdsi, sizeof(struct signalfd_siginfo));
        if (s != sizeof(struct signalfd_siginfo))
            handle_error("read");

        if (fdsi.ssi_signo == SIGINT) {
            printf("Got SIGINT\n");
        } else if (fdsi.ssi_signo == SIGQUIT) {
            printf("Got SIGQUIT\n");
            exit(EXIT_SUCCESS);
        } else {
            printf("Read unexpected signal\n");
        }
    }
}

使用signalfd很大的优势是,可以把这个文件描述符交给 select, poll, epoll进行检测,就像打开普通文件的普通描述符和网络套接字一样,当select这类函数通知你该文件描述符可读时再去读取、处理信号。

跟使用 sigwait 一样,不要忘记在调用 signalfd 之前用 sigprocmask 阻塞信号让它们不被按照默认方式处理。另外在使用完 signalfd 之后不要忘记 close 这个文件描述符。