Linux -- 进程信号

xiaoxiao2021-02-28  24

进程信号的基本概念

用户输入命令,在Shell下启动一个前台进程用户按下Ctrl-C,这个键盘输入产生一个硬件中断如果CPU当前正在执行这个进程的代码,则该进程的用户空间代码暂停执行,CPU从用户态切换到内核态处理硬件中断终端驱动程序将Ctrl-C解释成一个SIGINT信号,记在该进程的PCB中(也可以说发送了一 个SIGINT信号给该进程)当某个时刻要从内核返回到该进程的用户空间代码继续执行之前,首先处理PCB中记录的信号,发现有一个SIGINT信号待处理,而这个信号的默认处理动作是终止进程,所以直接终止进程而不再返回它的用户空间代码往下执行

这里通过键盘输入发送信号,只是信号产生的方式之一

注意

Ctrl-C产生的信号只能发给前台进程。一个命令后面加个 & 可以将进程放到后台运行,这样Shell不必等待进程结束就可以接受新的命令,启动新的进程。Shell可以同时运行一个前台进程和任意多个后台进程,只有前台进程才能接到像Ctrl-C这种控制键产生的信号。前台进程在运行过程中用户随时可能按下Ctrl-C而产生一个信号,也就是说该进程的用户空间代码执行到任何地方都有可能收到SIGINT信号而终止,所以信号相对于进程的控制流程来说是异步的。

使用 kill -l 命令可以查看系统定义的信号列表 其中一共有 62 个信号, 前 31 个信号为普通信号, 后 31 个为实时信号

这些信号各自在什么条件下产生,默认的处理动作是什么,在signal(7)中有详细说明

man 7 signal

信号产生方式概览

用户在终端按下某些按键时,终端驱动程序会发送信号给前台进程,例如Ctrl-C产生SIGINT信号,Ctrl-\ 产生SIGQUIT信号,Ctrl-Z产生SIGTSTP信号硬件异常产生信号,这些条件由硬件检测到并通知内核,然后内核向当前进程发送适当的信号。例如: 当前进程执行了 除以0 的指令,CPU的运算单元会产生异常,内核将这个异常解释为SIGFPE信号发送给进程。再比如当前进程访问了非法内存地址,,MMU会产生异常,内核将这个异常解释为SIGSEGV信号发送给进程。一个进程调用kill(2)函数可以发送信号给另一个进程。 可以用kill(1)命令发送信号给某个进程,kill(1)命令也是调用kill(2)函数实现的,如果不明确指定信号则发送SIGTERM信号,该信号的默认处理动作是终止进程。当内核检测到某种软件条件发生时也可以通过信号通知进程,例如闹钟超时产生 SIGALRM信号; 向读端已关闭的管道写数据时产生SIGPIPE信号; 如果不想按默认动作处理信号, 用户程序可以调用sigaction(2)函数告诉内核如何处理某种信号

信号处理常见方式

忽略此信号执行该信号的默认处理动作提供一个信号处理函数,要求内核在处理该信号时切换到用户态执行这个处理函数,这种方式称为信号捕捉
core dump

什么是Core Dump ?

当一个进程要异常终止时,可以选择把进程的用户空间内存数据全部保存到磁盘上,文件名通常是core.(进程pid) 这叫做Core Dump。进程异常终止通常是因为有Bug,比如非法内存访问导致段错误,事后可以用gdb等调试器检查core文件以查清错误原因,这叫做Post-mortem Debug(事后调试)。一个进程允许产生多大的core文件取决于进程的Resource Limit(这个信息保存在PCB中)。默认是不允许产生core文件的,因为core文件中可能包含用户密码等敏感信息,不安全。在开发调试阶段可以用ulimit命令改变这个限制,允许产生core文件。 ulimit -c 1024

允许core文件最大为1024K

写一个死循环运行, 然后ctrl - \ 杀掉它, 就会产生一个core文件 (ctrl - \是发送一个3号信号SIGQUIT, 它的默认处理动作是终止进程并core dump) 使用core文件

gdb 可执行文件名 core文件名

调用系统函数向进程发信号

首先在后台执行死循环程序, 然后用kill命令给它发SIGSEGV信号 4370 是test进程的id。 之所以要再次回车才显示段错误(Segmentation fault), 是因为在进程终止之前已经回到了Shell提示符等待用户输入下一条命令,Shell不希Segmentation fault信息和用户的输入交错在一起,所以等用户输入命令之后才显示。 指定发送某种信号的kill命令可以有多种写法,上面的命令还可以写成kill -11 4370 , 11是信号SIGSEGV的编号。 以往遇到的段错误都是由非法内存访问产生的,而这个程序本身没错,给它发SIGSEGV也能产生段错误。 kill命令是调用kill函数实现的。kill函数可以给一个指定的进程发送指定的信号。 raise函数可以给当前进程发送指定的信号(自己给自己发信号)。

#include <signal.h> int kill(pid_t pid, int signo); int raise(int signo); //这两个函数都是成功返回0,错误返回-1

abort函数使当前进程接收到信号而异常终止

#include <stdlib.h> void abort(void);

就像exit函数一样,abort函数总是会成功,所以没有返回值。

软件条件产生信号

alarm函数 和 SIGALRM信号

#include <unistd.h> unsigned int alarm(unsigned int seconds);

调用alarm函数可以设定一个闹钟,也就是告诉内核在seconds秒之后给当前进程发SIGALRM信号, 该信号的默认处理动作是终止当前进程。

这个函数的返回值是0 或者是以前设定的闹钟时间剩余的秒数。 打个比方,某人要小睡一觉,设定闹钟为30分钟之后响 20分钟后被人吵醒了,还想多睡一会儿,于是重新设定闹钟为15分钟之后响,“以前设定的闹钟时间还余下的时间”就是10分钟 如果seconds值为0,表示取消以前设定的闹钟,函数的返回值仍然是以前设定的闹钟 时间剩余的秒数

#include <stdio.h> #include <unistd.h> int main() { int count = 0; size_t ret = alarm(1); while(1) { count++; printf("count = %d\n", count); printf("ret = %d\n", ret); } return 0; }

在1s内一直数数, 1s到了就向该进程发送SIGALRM信号, 进程被终止 实际上, cpu 1s累加的次数远不仅如此, 因为打印到屏幕以及一些其他原因, 只累加到了64485


信号的阻塞

实际执行信号的处理动作称为 信号递达(Delivery) 信号从产生到递达之间的状态,称为信号未决(Pending)进程可以选择阻塞(Block)某个信号, 被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作
信号在内核中的表示

每个信号都有两个标志位分别表示阻塞(block)和未决(pending) 还有一个函数指针表示处理动作 信号产生时,内核在进程控制块中设置该信号的未决标志,直到信号递达才清除该标志 在上图中 SIGHUP信号未阻塞也未产生过,当它递达时执行默认处理动作 SIGINT信号产生过,但正在被阻塞,所以暂时不能递达。虽然它的处理动作是忽略,但在没有解除阻塞之前不能忽略这个信号,因为进程仍有机会改变处理动作之后再解除阻塞。 SIGQUIT信号未产生过,一旦产生SIGQUIT信号将被阻塞,它的处理动作是用户自定义函数sighandler 如果在进程解除对某信号的阻塞之前, 这种信号产生过多次,将如何处理? POSIX.1允许系统递送该信号一次或多次 Linux是这样实现的: 常规信号在递达之前产生多次只计一次,而实时信号在递达之前产生多次可以依次放在一个队列里 从上图来看,每个信号只有一个bit的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也是这样 因此,未决和阻塞标志可以用相同的数据类型sigset_t来存储, sigset_t称为信号集,这个类型可以表示每个信号的“有效”或“无效”状态,在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞 而在未决信号集中“有效”和“无效”的含义是该信号是否处于未决状态 阻塞信号集也叫做当前进程的信号屏蔽字(Signal Mask),这里的“屏蔽”应该理解为阻塞而不是忽略 sigset_t类型对于每种信号用一个bit表示“有效”或“无效”状态,至于这个类型内部如何存储这些bit位, 则依赖于系统实现,从使用者的角度是不必关心的,使用者只能调用以下函数来操作sigset_t变量,而不应该对它的内部数据做任何解释,比如用printf直接打印sigset_t变量是没有意义的

#include <signal.h> int sigemptyset(sigset_t *set); int sigfillset(sigset_t *set); int sigaddset (sigset_t *set, int signo); int sigdelset(sigset_t *set, int signo); int sigismember(const sigset_t *set, int signo);

函数sigemptyset初始化set所指向的信号集,使其中所有信号的对应bit清零,表示该信号集不包含任何有效信号 函数sigfillset初始化set所指向的信号集,使其中所有信号的对应bit置位,表示该信号集的有效信号包括系统支持的所有信号 注意,在使用sigset_ t类型的变量之前,一定要调用sigemptyset或sigfillset做初始化,使信号集处于确定的状态 初始化sigset_t变量之后就可以调用sigaddset和sigdelset在该信号集中添加或删除某 种有效信号 这四个函数都是成功返回0,出错返回-1 sigismember是一个布尔函数,用于判断一个信号集的有效信号中是否包含某种信号,若包含则返回1,不包含则返回0,出错返回-1


sigprocmask

调用函数sigprocmask可以读取或更改进程的信号屏蔽字(阻塞信号集)

#include <signal.h> int sigprocmask(int how, const sigset_t *set, sigset_t *oset);

返回值: 若成功则为0,若出错则为-1 如果oset是非空指针,则读取进程的当前信号屏蔽字通过oset参数传出 如果set是非空指针,则更改进程的信号屏蔽字,参数how指示如何更改 如果oset和set都是非空指针,则先将原来的信号屏蔽字备份到oset里,然后根据set和how参数更改信号屏蔽字

参数how的取值不同,带来的操作行为也不同,该参数可选值如下:

SIG_BLOCK: 该值代表的功能是将newset所指向的信号集中所包含的信号加到当前的信号掩码中,作为新的信号屏蔽字。SIG_UNBLOCK: 将参数newset所指向的信号集中的信号从当前的信号掩码中移除。SIG_SETMASK: 设置当前信号掩码为参数newset所指向的信号集中所包含的信号。

注意事项:sigprocmask()函数只为单线程的进程定义的,在多线程中要使用pthread_sigmask变量,在使用之前需要声明和初始化

如果调用sigprocmask解除了对当前若干个未决信号的阻塞,则在sigprocmask返回前,至少将其中一个信号递达

sigpending
#include <signal.h> int sigpending(sigset_t *set

sigpending函数返回 在送往进程的时候被阻塞挂起的信号集合。这个信号集合通过参数set返回 函数调用成功返回0,否则返回-1

阻塞信号测试代码 #include <stdio.h> #include <stdlib.h> #include <asm/signal.h> #include <unistd.h> void print_sigset(sigset_t* set); //测试pending void print_sigset(sigset_t* set) { for(int i=1; i<=31; i++) { if(i%8 == 0) printf(" "); //int sigismember(const sigset_t *set,int signum); //如果信号集里已有signum信号则返回1,否则返回0。如果有错误则返回-1 if(sigismember(set, i) == 1) { putchar('1'); } else { putchar('0'); } } puts(" "); } int main() { //定义信号集对象, 清空并初始化 sigset_t s, p; //sigemptyset()用来将参数set信号集初始化并清空。 sigemptyset(&s); //sigaddset()用来将参数signum 代表的信号加入至参数set 信号集里。 sigaddset(&s, SIGINT);//将2号信号SIGINT加入s信号集 //int sigprocmask(int how, const sigset_t *restrict set, sigset_t *restrict oldset); //用来检测或改变目前的信号屏蔽字,其操作依参数how来决定 //参数how的取值不同,带来的操作行为也不同,该参数可选值如下: //1.SIG_BLOCK:该值代表的功能是将newset所指向的信号集中所包含的信号加到当前的信号掩码中,作为新的信号屏蔽字。 //2.SIG_UNBLOCK:将参数newset所指向的信号集中的信号从当前的信号掩码中移除。 //3.SIG_SETMASK:设置当前信号掩码为参数newset所指向的信号集中所包含的信号。 sigprocmask(SIG_BLOCK, &s, NULL); //设置阻塞信号集, 阻塞s里的SIGINT信号 while(1) { //sigpending函数返回在送往进程的时候被阻塞挂起的信号集合。这个信号集合通过参数set返回 sigpending(&p); //获取未决信号集 print_sigset(&p); sleep(1); } return 0; }

这个代码的作用是每秒钟打印 31 个普通信号的未决状态 因为我们阻塞了 2号信号 SIGINT 所以在按下 ctrl - c 时, 进程并没有终止, 而是SIGINT信号的未决标志位由 0 变 1, 按 ctrl - z 可以终止进程, 因为 SIGQUIT 没有被阻塞


捕捉信号

内核如何实现信号的捕捉

如果信号的处理动作是用户自定义的信号处理函数, 在信号递达时就执行该处理函数, 这称为信号的捕捉

信号的捕捉过程

例如: 用户程序注册了SIGQUIT信号的处理函数sighandler 当前正在执行main函数,这时发生中断或异常切换到内核态 在中断处理完毕后要返回用户态的main函数之前, 检查到有信号SIGQUIT递达 内核决定返回用户态后不是恢复main函数的上下文继续执行,而是执行sighandler函数,sighandler和main函数使用不同的堆栈空间,它们之间不存在调用和被调用的关系,是两个独立的控制流程。 sighandler函数返回后自动执行特殊的系统调用sigreturn再次进入内核态 如果没有新的信号要递达,这次再返回用户态就是恢复main函数的上下文继续执行了

sigaction
#include <signal.h> int sigaction(int signo, const struct sigaction *act, struct sigaction *oact);

sigaction函数可以读取和修改与指定信号相关联的处理动作。调用成功返回0,出错返回 -1 signo是指定信号的编号。若act指针非空,则根据act修改该信号的处理动作。 若oact指针非空,则通过oact传出该信号原来的处理动作。 act和oact指向sigaction结构体

/* Structure describing the action to be taken when a signal arrives. */ struct sigaction { /* Signal handler. */ #ifdef __USE_POSIX199309 union { /* Used if SA_SIGINFO is not set. */ __sighandler_t sa_handler; /* Used if SA_SIGINFO is set. */ void (*sa_sigaction) (int, siginfo_t *, void *); } __sigaction_handler; # define sa_handler __sigaction_handler.sa_handler # define sa_sigaction __sigaction_handler.sa_sigaction #else __sighandler_t sa_handler; #endif /* Additional set of signals to be blocked. */ __sigset_t sa_mask; /* Special flags. */ int sa_flags; /* Restore handler. */ void (*sa_restorer) (void); };

将sa_handler赋值为常数 SIG_IGN 传给 sigaction 表示忽略信号,赋值为常数SIG_DFL表示执行系统默认处理动作,赋值为一个函数指针表示用自定义函数捕捉信号,或者说向内核注册了一个信号处理函数,该函数返回值为void,可以带一个int参数,通过参数可以得知当前信号的编号,这样就可以用同一个函数处理多种信号。显然,这也是一个回调函数,不是被main函数调用,而是被系统所调用。 当某个信号的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字,当信号处理函数返回时自动恢复原来的信号屏蔽字,这样就保证了在处理某个信号时,如果这种信号再次产生,那么它会被阻塞到当前处理结束为止。 如果在调用信号处理函数时,除了当前信号被自动屏蔽之外,还希望自动屏蔽另外一些信号,则用sa_mask字段说明这些需要额外屏蔽的信号,当信号处理函数返回时自动恢复原来的信号屏蔽字。 sa_flags字段包含一些选项, 一般都把sa_flags设为0, sa_sigaction是实时信号的处理函数

pause函数
#include <unistd.h> int pause(void);

pause函数使调用进程挂起, 直到有信号递达。 如果信号的处理动作是终止进程,则进程终止,pause函数没有机会返回 如果信号的处理动作是忽略,则进程继续处于挂起状态,pause不返回 如果信号的处理动作是捕捉,则调用了信号处理函数之后, pause返回-1,errno设置为EINTR 所以pause只有出错的返回值(exec函数族也只有出错返回) 错误码EINTR表示“被信号中断”。

下面我们用alarm和pause模拟实现sleep函数,称为mysleep

main函数调用mysleep函数, 后者调用sigaction注册SIGALRM信号的处理函数sig_alrm调用alarm(nsecs)设定闹钟调用pause等待, 内核切换到别的进程运行nsecs秒之后,闹钟超时,内核发SIGALRM信号给这个进程从内核态返回这个进程的用户态之前处理未决信号,发现有SIGALRM信号,其处理函数 是sig_alrm切换到用户态执行sig_ alrm函数进入sig_ alrm函数时SIGALRM信号被自动屏蔽, 从sig_alrm函数返回时SIGALRM信号自动解除屏蔽。然后自动执行系统调用sigreturn再次进入内核再返回用户态继续执行进程的主控制流程(main函数调用的mysleep函数)pause函数返回-1,然后调用alarm(0)取消闹钟,调用sigaction恢复SIGALRM信号以前的处理动作 #include <stdio.h> #include <stdlib.h> #include <signal.h> #include <unistd.h> ////////////////////////////// //实现mysleep ////////////////////////////// void sig_alrm(int signo) { ;//do nothing } unsigned int mysleep(unsigned int nsecs) { struct sigaction new, old;//定义两个sigaction结构体 unsigned int unslept = 0; new.sa_handler = sig_alrm;//信号处理函数为 sig_alrm sigemptyset(&new.sa_mask);//sa_mask 为 sigset_t 类型, 所以要这样初始化 new.sa_flags = 0; sigaction(SIGALRM, &new, &old);//注册信号处理函数 alarm(nsecs);//设置闹钟 pause();//挂起 unslept = alarm(0);//清空闹钟 sigaction(SIGALRM, &old, NULL);//恢复默认处理动作 return unslept; } int main() { while(1) { mysleep(2); printf("sleep 2 s\n"); } return 0; }

Makefile

main:test.c gcc $^ -o $@ -std=c99 -g -D_GNU_SOURCE clean: rm -f main

注意: 我在直接编译时,无法通过, 在上网查询后发现加入 -D_GNU_SOURCE 就可以了, 猜测是 signal.h 里面定义了条件编译, 需要在编译时更改宏定义, 使其代码加入编译 原来是 signal.h 包含头文件 features.h 这是用来让用户配置编译环境的头文件。 再看一下 _GUN_SOURCE 这个宏,这个宏可以让用户打开所有feature

/* If _GNU_SOURCE was defined by the user, turn on all the other features. */ #ifdef _GNU_SOURCE # undef _ISOC95_SOURCE # define _ISOC95_SOURCE 1 # undef _ISOC99_SOURCE # define _ISOC99_SOURCE 1 # undef _POSIX_SOURCE # define _POSIX_SOURCE 1 # undef _POSIX_C_SOURCE # define _POSIX_C_SOURCE 200809L # undef _XOPEN_SOURCE # define _XOPEN_SOURCE 700 # undef _XOPEN_SOURCE_EXTENDED # define _XOPEN_SOURCE_EXTENDED 1 # undef _LARGEFILE64_SOURCE # define _LARGEFILE64_SOURCE 1 # undef _BSD_SOURCE # define _BSD_SOURCE 1 # undef _SVID_SOURCE # define _SVID_SOURCE 1 # undef _ATFILE_SOURCE # define _ATFILE_SOURCE 1 #endif

问题

q1: 信号处理函数sig_alrm什么都没干,为什么还要注册它作为SIGALRM的处理函数? a1: 因为要让 pause 出错返回

q2: 为什么在mysleep函数返回前要恢复SIGALRM信号原来的sigaction? a2: 否则以后SIGALRM信号就无法正常终止进程了

q3: mysleep函数的返回值表示什么含义?什么情况下返回非0值? a3: 返回值代表闹钟剩余的秒数, 在闹钟到时间之前, 收到SIGALRM信号, 则返回值为闹钟剩余时间


可重入函数

main函数调用insert函数向一个链表head中插入节点node1 插入操作分为两步,刚做完第一步的时候,因为硬件中断使进程切换到内核 再次返回用户态之前检查到有信号待处理,于是切换到sighandler函数 sighandler也调用insert函数向同一个链表head中插入节点node2 插入操作的两步都做完之后, 从sighandler返回内核态 再次回到用户态就从main函数调用的insert函数中继续往下执行 先前做第一步之后被打断,现在继续做完第二步 结果是,main函数和sighandler先后向链表中插入了两个节点 而实际最后只有一个节点真正插入链表中了 像上例这样,insert函数被不同的控制流程调用,有可能在第一次调用还没返回时就再次进入该函数 这称为重入,insert函数访问一个全局链表,有可能因为重入而造成错乱 像这样的函数称为 不可重入函数 反之,如果一个函数只访问自己的局部变量或参数,则称为可重入函数

如果一个函数符合以下条件之一则是不可重入的

调用了malloc或free,因为malloc也是用全局链表来管理堆的。调用了标准I/O库函数。标准I/O库的很多实现都以不可重入的方式使用全局数据结构。
volatile

限定符 在上面的例子中,main和sighandler都调用insert函数则有可能出现链表的错乱 其根本原因在于对全局链表的插入操作要分两步完成 不是一个原子操作 假如这两步操作必定会一起做完, 中间不可能被打断,就不会出现错乱了。

如果对全局数据的访问只有一行代码,是不是原子操作呢? 比如,main和sighandler都只对一个全局变量赋值,会不会出现错乱呢?

long long a; int main() { a = 5; return 0; }

虽然C代码只有一行,但是在32位机上对一个64位的long long变量赋值需要两条指令才能完成 因此不是原子操作。 同样地,读取这个变量到寄存器需要两个32位寄存器才放得下,也需要两条指令, 不是原子操作。 设想一种时序,main和sighandler都对这个变量a赋值,最后变量a的值发生错乱。 如果上述程序在64位机上编译执行,则有可能用一条指令完成赋值,因而是原⼦子操作。如果a是32位的int变量,在32位机上赋值是原子操作,在16位机上就不是。 如果在程序中需要使用一个变量,要保证对它的读写都是原子操作,应该采用什么类型呢? 为了解决这些平台相关的问题,C标准定义了一个类型sig_atom_ict,在不同平台的C语言库中取不同的类型 例如在32位机上定义sig_atom_ict为int类型 在使用sig_atom_ict类型的变量时,还需要注意另一个问题。 看如下的例子:

在main函数中首先要注册某个信号的处理函数sighandler 然后在一个while死循环中等待信号发生,如果有信号递达则执行sighandler sighandler中将a改为1,这样再次回到main函数时就可以退出while循环,执行后续处理

将全局变量a从内存读到eax寄存器 对eax和eax做AND运算,若结果为0则跳回循环开头 再次从内存读变量a的值 可见这三条指令等价于C代码的 while(!a); 循环

第一条指令 将全局变量a的内存单元直接和0比较 如果相等,则第二条指令成了一个死循环,注意,这是一个真正的死循环 即使sighandler将a改为1,只要没有影响Zero标志位 回到main函数后仍然在第二条指令上 因为不会再次从内存读取变量a的值 设想一下,如果程序只有单一的执行流程,只要当前执行流程没有改变a的值,a的值就没有理由会变,不需要反复从内存读取,因此上面的两条指令和while(!a);循环是等价的并且优化之后省去了每次循环读内存的操作,效率非常高 所以不能说编译器做错了,只能说编译器无法识别程序中存在多个执行流程 之所以程序中存在多个执行流程,是因为调用了特定平台上的特定库函数 比如sigaction、pthread_ create, 这些不是C语言本身的规范,不归编译器管,程序员应该自己处理这些问题。 C语言提供了volatile限定符,如果将上述变量定义为volatile sig_atomic_t a=0; 那么即使指定了优化选项,编译器也不会优化掉对变量a内存单元的读写。 对于程序中存在多个执行流程访问同一全局变量的情况,volatile限定符是必要的 此外,虽然程序只有单一的执行流程,但是变量属于以下情况之一的,也需要volatile限定:

变量的内存单元中的数据不需要写操作就可以自己发生变化,每次读上来的值都可能不一样 即使多次向变量的内存单元中写数据,只写不读,也并不是在做无用功,而是有特殊意义的什么样的内存单元会具有这样的特性呢? 肯定不是普通的内存,而是映射到内存地址空间的硬件寄存器,例如串口的接收寄存器属于上述第一种情况,而发送寄存器属于上述第二种情况。

sig_ atomic_ t 类型的变量应该总是加上volatile限定符,因为要使用sig_ atomic_ t 类型的理由也正是要加volatile限定符的理由


竞态条件与 sigsuspend 函数

现在重新审视“mysleep”程序,设想这样的时序:

注册SIGALRM信号的处理函数调用alarm(nsecs)设定闹钟内核调度优先级更高的进程取代当前进程执行,并且优先级更高的进程有很多个,每个都要执行很长时间nsecs秒钟之后闹钟超时了,内核发送SIGALRM信号给这个进程,处于未决状态。优先级更高的进程执行完了,内核要调度回这个进程执行。SIGALRM信号递达,执行处理函数sig_alrm之后再次进入内核。返回这个进程的主控制流程,alarm(nsecs)返回,调用pause()挂起等待。可是SIGALRM信号已经处理完了,还等待什么呢 ?

出现这个问题的根本原因是 系统运行的时序(Timing)并不像我们写程序时所设想的那样。 虽然alarm(nsecs)紧接着的下一行就是pause(),但是无法保证pause()一定会在调用alarm(nsecs)之后的nsecs秒之内被调用。 由于异步事件在任何时候都有可能发生(这里的异步事件指出现更高优先级的进程) 如果我们写程序时考虑不周密,就可能由于时序问题而导致错误 这叫做 竞态条件 (Race Condition)

如何解决上述问题呢? 看以下方法可行吗? 1. 屏蔽SIGALRM信号 2. alarm(nsecs); 3. 解除对SIGALRM信号的屏蔽 4. pause();

从解除信号屏蔽到调用pause之间存在间隙,SIGALRM仍有可能在这个间隙递达。 要消除这个间隙, 我们把解除屏蔽移到pause后面可以吗? 1. 屏蔽SIGALRM信号 2. alarm(nsecs); 3. pause(); 4. 解除对SIGALRM信号的屏蔽 这样更不行了,还没有解除屏蔽就调用pause,pause根本不可能等到SIGALRM信号。 要是“解除信号屏蔽”和“挂起等待信号”这两步能合并成一个原子操作就好了 这正是sigsuspend函数的功能。 sigsuspend包含了pause的挂起等待功能,同时解决了竞态条件的问题,在对时序要求严格的场合下都应该调用sigsuspend而不是pause。

#include <signal.h> int sigsuspend(const sigset_t *sigmask);

和pause一样,sigsuspend没有成功返回值,只有执行了一个信号处理函数之后sigsuspend才返回 返回值为-1,errno设置为EINTR 调用sigsuspend时,进程的信号屏蔽字由sigmask参数指定,可以通过指定sigmask 来临时解除对某个信号的屏蔽,然后挂起等待 当sigsuspend返回时,进程的信号屏蔽字恢复为原来的值 如果原来对该信号是屏蔽的,从sigsuspend返回后仍然是屏蔽的

改进 mysleep

unsigned int mysleep(unsigned int nsecs) { struct sigaction new, old;//定义两个sigaction结构体 unsigned int unslept = 0; sigset_t newmask, oldmask, suspmask; new.sa_handler = sig_alrm;//信号处理函数为 sig_alrm sigemptyset(&new.sa_mask);//sa_mask 为 sigset_t 类型, 所以要这样初始化 new.sa_flags = 0; sigaction(SIGALRM, &new, &old);//注册信号处理函数 sigemptyset(&newmask); sigaddset(&newmask, SIGALRM); sigprocmask(SIG_BLOCK, &newmask, &oldmask); alarm(nsecs);//设置闹钟 suspmask = oldmask; sigdelset(&suspmask, SIGALRM); sigsuspend(&suspmask); unslept = alarm(0);//清空闹钟 sigaction(SIGALRM, &old, NULL);//恢复默认处理动作 sigprocmask(SIG_SETMASK, &oldmask, NULL); return unslept; } 调用sigprocmask(SIG_BLOCK, &newmask, &oldmask); 屏蔽 SIGALRM调用sigsuspend(&suspmask); 解除对SIGALRM的屏蔽,然后挂起等待待SIGALRM递达后suspend返回,自动恢复原来的屏蔽字,也就是再次屏蔽SIGALRM调用sigprocmask(SIG_ SETMASK, &oldmask, NULL); 再次解除对SIGALRM的屏蔽
SIGCHILD信号

用wait和waitpid函数可以清理僵尸进程,父进程可以阻塞式等待子进程结束 也可以非阻塞地查询是否有子进程结束, 等待清理(也就是轮询的方式) 采用第一种方式,父进程阻塞了就不能处理自己的工作了 采用第二种方式,父进程在处理自己的工作的同时还要记得时不时地询问一 下,程序实现复杂 其实,子进程在终止时会给父进程发送SIGCHLD信号,该信号的默认处理动作是忽略父进程可以自定义SIGCHLD信号的处理函数 这样父进程只需专心处理自己的工作,不必关心子进程,子进程终止时会通知父进程 父进程在信号处理函数中调用wait清理子进程即可

请编写一个程序完成以下功能 父进程fork出子进程,子进程调用exit(2)终止,父进程自定义SIGCHLD信号的处理函数 在其中调用wait获得子进程的退出状态并打印

#include <stdio.h> #include <stdlib.h> #include <signal.h> #include <unistd.h> #include<sys/types.h> #include<sys/wait.h> void handler(int sig) { pid_t id; while( (id = waitpid(-1, NULL, WNOHANG)) > 0 ) { printf("wait child success: %d\n", id); } printf("child is quit! %d\n", getpid()); } int main() { signal(SIGCHLD, handler); pid_t cid; if((cid = fork()) == 0) { //child printf("child : %d\n", getpid()); sleep(3); exit(1); } while(1) { printf("father proc is doing some thing!\n"); sleep(1); } return 0; }

事实上,由于 UNIX 的历史原因,要想不产生僵尸进程还有另外一种办法:父进程调用sigaction将SIGCHLD的处理动作置为SIG_IGN 这样fork出来的子进程在终止时会自动清理掉,不会产生僵尸进程,也不会通知父进程

系统默认的忽略动作和用户用sigaction函数自定义的忽略通常是没有区别的,但这是一个特例。 此方法对于Linux可用,不保证在其它UNIX系统上都可用。

转载请注明原文地址: https://www.6miu.com/read-2621251.html

最新回复(0)