• 1. Linux环境高级编程
  • 2. 第五讲 信号
  • 3. 第五讲 信号信号的概念 signal函数 不可靠信号与可靠信号 信号的发送接收机制 信号集与可靠信号机制 sigsetjmp和siglongjmp函数 abort函数
  • 4. 第五讲 信号信号的概念 signal函数 不可靠信号与可靠信号 信号的发送接收机制 信号集与可靠信号机制 sigsetjmp和siglongjmp函数 abort函数
  • 5. 信号的概念基本概念 信号的产生 信号的处理方式 常见信号 信号与文字说明的转换5
  • 6. 信号的概念当某个程序长时间运行,如何中断它的执行,让其退出? 示例5.8 Ctrl+c中断程序执行 是如何中断程序执行的呢?6
  • 7. 信号的概念信号是软件中断。 例如,Ctrl + C,将产生SIGINT信号,中断程序执行 它可以作为进程间通信的一种方式,但更主要的是,信号总是中断一个进程的正常运行,它更多地被用于处理一些非正常情况 每个信号都有一个名字,以SIG开头。 SIGABRT:进程异常终止信号,abort产生 SIGALRM:闹钟信号,计时器超时后产生 Linux 2.4.22共有31种不同的信号 查看头文件/usr/include/bits/signum.h7
  • 8. 信号的产生很多条件可以产生一个信号 当用户按某些终端键时,产生信号。例如在终端上按Ctrl+C键通常产生中断信号(SIGINT)。 硬件异常产生信号:除数为0、无效的存储访问等等。这些条件通常由硬件检测到,并将其通知内核。然后内核为该条件发生时正在运行的进程产生适当的信号。 进程用Kill函数可将信号发生给另一个进程或进程组; 用户用Kill命令将信号(终止信号SIGTERM)发送给其他进程 当检测到某种软件条件已经发生,并将其通知有关进程时产生信号。如SIGPIPE、SIGALRM 等等8
  • 9. 信号的概念基本概念 信号的产生 信号的处理方式 常见信号 信号与文字说明的转换9
  • 10. 进程对信号的处理方式进程对信号的处理方式可以有三种: 忽略此信号。大多数信号都可使用这种方式进行处理,但有两种信号却不能被忽略。它们是:SIGKILL和SIGSTOP。这两种信号不能被忽略的原因时:它们向超级用户提供了一种使进程终止或停止的可靠方法; 捕捉信号。为了做到这一点要通知内核在某种信号发生时,调用一个用户函数。在用户函数中,可执行用户希望对这种事件进行的处理。 执行系统默认动作。对大多数信号的系统默认动作是终止该进程。10
  • 11. 常见的信号SIGABRT 处理异常信号。当前进程调用abort后发送该信号; SIGALRM 报时时钟。当进程设置的时钟超时后,内核向进程发送一个时钟信号; SIGCHLD 子进程被终止或停止。每当子进程被终止或停止时,内核都会给父进程发送一个信号SIGCHLD,该信号表示的是一个子进程的消亡。 SIGHUP 挂起信号。当终端连接断开时,内核向所有依附在此控制终端上的进程发送此信号; SIGINT 中断信号。当用户按下中断键时,从内核向所有与终端会话有联系的进程发出信号。11
  • 12. 常见的信号(续)SIGPIPE 写无接收方的管道或套接字信号。管道和套接字都是一种进程间通讯机制; SIGQUIT 退出信号。与SIGINT信号非常类似,当用户在终端上敲下退出键时,内核会发出这个信号。与SIGINT不同点在于,该信号会导致进程的异常终止。 SIGTTIN/SIGTTOU 后台进程读/写信号。每当一个后台进程试图从控制终端读入/写数据时就会产生此信号,此信号的默认动作是终止进程的运行。 SIGURG 高带宽数据通知信号。该信号通知进程,网络连接中出现了紧急情况或者带外数据; SIGUSR1和SIGUSR2 用户自定义信号。12
  • 13. 信号的概念基本概念 信号的产生 信号的处理方式 常见信号 信号与文字说明的转换13
  • 14. 信号值与文字说明的转换Linux提供了一个数组 extern char* sys_siglist[]; 数组的下标是信号的编号,给出的字符串指针指向相应信号的说明文字 示例5.9 注意,有效的信号编号是从1到3114
  • 15. psignal函数该函数类似于perror,根据信号值,输出相关的信息 函数原型 #include void psignal(int signo, const char* msg); 该函数将字符串msg输出,后面接一个冒号和一个空格,再接着对该信号的说明,最后一个换行符 示例(5.10)15
  • 16. strsignal函数该函数类似于strerror函数,将信号值翻译成可读的字符串 函数原型 #include char* strsignal(int signo); 参数:signo,即信号编号 返回值:signo对应的说明文字 在Solaris 9中,若信号编号无效,则返回NULL,在FreeBSD、Linux中,则返回字符串,说明信号不可识别 示例(5.11)16
  • 17. strsignal函数strsignal函数返回了一个字符串指针 该指针指向的内存区域需要释放吗? 该指针指向的内存区域可以被修改吗? 若能修改,修改后,再调用strsignal函数会有什么结果? 示例(5.12)17
  • 18. strsignal函数为什么不需要释放内存? 可以修改strsignal返回的指针,所对应的内存区域 但是,再次调用strsignal后,并未修改成功 为什么?18
  • 19. strsignal函数查看源文件strsignal.c 实际上,对于有效的信号编号,若修改对应的内存区域,将段错误 而对于实时信号和未知信号,则可以随意修改; 但每次调用strsignal后,修改的内容都不存在,原因即strsignal内部通过重新分配buffer,填充信号说明导致19
  • 20. 第五讲 信号信号的概念 signal函数 不可靠信号与可靠信号 信号的发送接收机制 信号集与可靠信号机制 sigsetjmp和siglongjmp函数 abort函数
  • 21. signal函数用于设置信号的处理方法 函数原型 void (*signal(int signo, void (*func)(int)))(int); 返回类型与参数 返回类型:void (*)(int) 第一个参数signo:信号名 第二个参数func:类型void (*)(int) 21
  • 22. signal函数说明: typedef void (*Sigfunc)(int); Sigfunc *signal(int, Sigfunc *); 第二个参数func SIG_IGN:内核忽略该信号(SIGKILL、SIGSTOP不能忽略) SIG_DFL:接收到信号signo后,按照系统默认动作处理 用户自己提供的信号处理函数。当收到信号时,进程立即转移去执行信号处理函数 返回值: 成功,返回信号以前的处理方式 出错,返回SIG_ERR #define SIG_ERR (void (*)())-1 #define SIG_DFL (void (*)()) 0 #define SIG_IGN (void (*)()) 1 22
  • 23. signal函数程序演示(5.1后台启动) $kill -USR1 进程ID $kill -s 信号编号 进程ID 23
  • 24. signal函数当一个进程调用fork时,其子进程继承父进程的信号处理方式(处理函数地址在子进程有意义?) 当执行一个程序时,所有信号的状态通常都是系统默认(除非调用exec的进程忽略某信号) exec函数将原先设置为要捕捉的信号,改为它们的默认动作(为什么?),其他信号的状态则不变24
  • 25. signal函数例子(5.8) 命令行中:cc main.c & 输入Ctrl+C,产生中断信号,为什么后台进程不退出? Shell将后台进程对中断和退出信号的处理方式设置为忽略25
  • 26. signal函数程序演示(5.2)(两个终端) 父进程忽略信号 父进程捕获信号26
  • 27. signal的封装示例5.23 缺点: CSignalDispatcher权责不清 需要显示调用signal,未封装SIGKILL和SIGSTOP的特殊性 需要显示删除具体的信号处理对象27
  • 28. signal函数获取当前信号处理方式 SigFunc *sig_int; sig_int = signal(SIGINT, SIG_IGN); signal(SIGINT, sig_int); 必须两次调用signal(两次改变信号处理方式),才能获取当前信号处理方式 当改变信号处理方式之间,有信号达到,该如何? 28
  • 29. 问题?signal函数存在的问题-不可靠在早期的UNIX系统中(如SVR4中),进程每次处理信号时,随即将信号动作复位为默认动作。因此需要重复安装信号函数,例如: void sig_int( int signo ); … signal( SIGINT, sig_int ); … void sig_int( int signo ) { signal( SIGINT, sig_int ); … }29
  • 30. signal函数存在的问题(续)上述解决方法存在的问题是: 在信号发生之后到信号处理程序中调用signal函数之间有一个时间窗口。在此期间,可能发生另一次信号,而第二个信号有可能造成执行默认动作。(信号丢失,即不可靠) 进程所能做的仅仅是捕捉或忽略信号,而不能让内核阻塞信号 不要忽略信号 但是在发生信号时,记住它,在进程做好准备的时候传递它30
  • 31. 第二个问题的典型例子int sig_int_flag; int main(void) { void sig_int(); ... signal(SIGINT,sig_int); ... while(sig_int_flag == 0) pause(); ... } void sig_int() { signal(SIGINT,sig_int); sig_int_flag = 1; }问题:有可能在while检测变量之后,而在pause函数之前发生SIGINT信号,而且信号只发生此一次,由此,该程序将一直睡眠。 这种情况导致程序在大多数时间能正确运行。假设在此处发生信号?31
  • 32. 第五讲 信号信号的概念 signal函数 不可靠信号与可靠信号 信号的发送接收机制 信号集与可靠信号机制 sigsetjmp和siglongjmp函数 abort函数
  • 33. 不可靠信号与可靠信号不可靠信号:建立在早期机制上的信号。这些信号的值从1到31,已有预定义含义,如SIGINT 进程每次处理信号后,系统就将对信号的响应设置为默认动作(需两次调用signal)linux已解决 信号可能丢失 不可靠信号不支持排队(程序演示5.3,同一种信号,test后台启动) 在信号处理函数执行过程中,到来的所有相同信号,都被合并为一个信号33
  • 34. 不可靠信号与可靠信号可靠信号:后期引入的,其信号值从34到64 可靠信号支持排队,不会丢失(程序演示5.4,同一种信号,test后台启动) 可靠信号也被称为实时信号 不可靠信号被称为非实时信号 信号:软中断(中断程序,执行信号处理程序) 可靠信号与不可靠信号:在信号处理程序中,收到与正处理信号不同的信号时,会中断当前信号的处理,去执行新到信号的处理函数(示例5.13)34
  • 35. 示例5.13的启示当正在处理可靠信号A时,若收到另一可靠信号B,则中断信号A的执行,转去执行信号B的处理; 若此时又收到信号B,则该信号在B队列排队;若此时又收到信号A,则该信号在A队列排队 若信号B处理完毕,则检查B队列中有无信号尚需处理,处理完毕后,再返回中断点执行35
  • 36. 可靠信号与不可靠信号早期的信号安装和发送机制 signal kill 改进后的信号安装和发送机制 sigaction sigqueue 信号可靠与否,只与信号值有关,与采用何种信号安装、发送函数无关36
  • 37. 第五讲 信号信号的概念 signal函数 不可靠信号与可靠信号 信号的发送接收机制 信号集与可靠信号机制 sigsetjmp和siglongjmp函数 abort函数
  • 38. 信号的发送接收机制kill函数、raise函数 alarm函数、pause函数 sleep函数及实现 具有超时功能的API38
  • 39. 信号的发送接收机制kill函数、raise函数 alarm函数、pause函数 sleep函数及实现 具有超时功能的API39
  • 40. kill函数该函数将信号发送给其他进程或者进程组 函数原型 #include int kill(pid_t pid, int signo); 参数与返回值 signo:需要发送的信号编号 pid:发送的目的地 成功返回0,出错返回-140
  • 41. kill函数pid的取值 pid > 0:将信号发送给进程ID为pid的进程 pid == 0:将信号发送给起进程组ID等于发送进程的进程组ID,而且发送进程有许可权向其发送信号的所有进程 pid < 0 :将信号发送给其进程组ID等于pid绝对值,而且发送进程有许可权向其发送信号的所有进程 pid == -1 将信号发送给除进程0以外的所有进程,但发送进程必须有许可权41
  • 42. raise函数该函数将信号发送给自己 函数原型 #include int raise(int signo); 参数与返回值 signo:需要发送的信号编号 成功返回0,出错返回-142
  • 43. 发送信号的权限有发送信号许可权的基本规则是:1)超级用户可以将信号发送给任意进程;2)发送者的实际或有效用户ID,必须等于接收者实际或有效用户ID; 当signo为0时,则kill仍执行正常的错误检测,但不发送信号。这常被用来确定一个特定进程是否存在。如果向一个并不存在的进程发送空信号,则kill返回-1,errno则被设置为ESRCH。43
  • 44. 信号的发送接收机制kill函数、raise函数 alarm函数、pause函数 sleep函数及实现 具有超时功能的API44
  • 45. alarm函数该函数用于设置一个计时器,在将来某个指定的时间,该计时器将超时,产生SIGALRM信号 函数原型 #include unsigned int alarm(unsigned int seconds); 参数与返回值 seconds:经过seconds秒后,产生SIGALRM 返回0或以前设置的计时器时间的余留秒数45
  • 46. alarm函数注意:每个进程只有一个计时器 若在调用alarm时,以前已为该进程设置的计时器并未超时,则将该计时器的余留值作为本次alarm调用的值返回,以前登记的计时器,则被新计时器替代 取消定时器 若有以前为进城登记的尚未超时的计时器,而且本次调用seconds等于0,则取消计时器46
  • 47. alarm函数是先调用alarm函数,再调用signal函数设置SIGALRM处理函数? 还是先调用signal函数设置SIGALRM处理函数,再调用alarm函数?47
  • 48. alarm函数使用示例(5.14)48
  • 49. pause函数该函数使调用进程挂起,直到捕捉到一个信号 函数原型 #include int pause(); 返回值 只有执行了一个信号处理程序并从其返回时,pause才返回 返回-1,errno被设置为EINTR49
  • 50. 信号的发送接收机制kill函数、raise函数 alarm函数、pause函数 sleep函数及实现 具有超时功能的API50
  • 51. sleep函数该函数用于让调用进程挂起,直到 已经过了指定的时间,或者 调用进程捕捉到一个信号,并从信号处理程序返回 函数原型 #include unsigned int sleep(unsigned int seconds);51
  • 52. sleep函数返回值: 若已经过了指定的时间,则返回0 若调用进程捕捉到一个信号,并从信号处理程序返回,则sleep提前返回,返回值是未睡够的秒数52
  • 53. sleep函数的实现方式一: Solaris 9使用alarm实现sleep函数 方式二: FreeBSD、Linux使用nanosleep提供时间延迟53
  • 54. sleep的实现方式一static void sig_alrm(int signo) { return; //nothing to do, just return to wake up the pause } unsigned int sleep1(unsigned int nsecs) { if (signal(SIGALRM, sig_alrm) == SIG_ERR) return(nsecs); alarm(nsecs); /* start the timer */ pause(); //next caught signal wakes us up return( alarm(0) ); /turn off timer, return unslept time }There is a race condition54
  • 55. sleep1函数上述实现存在如下问题: 如果调用者此前曾设置了计时器,则它被sleep1函数中的第一次alarm调用擦去。(考虑解决方法) 该程序中修改了对SIGALRM的配置。如果编写一个函数供其他函数调用,则在该函数调用时要保存原配置,并在返回前恢复原配置; 在调用alarm和pause之间有一个竞争条件。(思考在什么情况下会发生?结果如何?)55
  • 56. sleep1函数对于问题1 检查第一次调用alarm的返回值,若其小于本次调用alarm的参数值,则只应等到上次设置的计时器超时; 若上次设置的计时器超时时间晚于本次设置值,则在sleep1函数返回前,复位此计时器,使其在上次计时器的设定时间再次发生超时 56
  • 57. sleep1函数对于问题2 保存signal函数的返回值,在返回前复位原配置 对于问题3 可以使用setjmp,或者 sigprocmask、sigsuspend sleep的早期实现,更正了问题1和问题257
  • 58. sleep2函数static jmp_buf env_alrm; static void sig_alrm(int signo) { longjmp(env_alrm, 1); } unsigned int sleep2(unsigned int nsecs) { if (signal(SIGALRM, sig_alrm) == SIG_ERR) return(nsecs); if (setjmp(env_alrm) == 0) { alarm(nsecs); /* start the timer */ pause(); /* next caught signal wakes us up */ } return( alarm(0) ); /* turn off timer, return unslept time */ }58
  • 59. sleep2函数这种实现解决了竞争条件, 即使pause从未执行,在发生SIGALRM时,sleep2也会返回 但却带来另一个难于察觉的问题: 它涉及到与其他信号的相互作用。如果SIGALRM中断了某个其他信号处理程序,则调用longjmp会提早终止该信号处理程序 下页PPT的例子59
  • 60. unsigned int sleep2(unsigned int); static void sig_int(int); int main(void) { unsigned int unslept; if (signal(SIGINT, sig_int) == SIG_ERR) err_sys("signal(SIGINT) error"); unslept = sleep2(5); printf("sleep2 returned: %u\n", unslept); exit(0); }static void sig_int(int signo) { int i; volatile int j; printf("\nsig_int starting\n"); for (i = 0; i < 2000000; i++) j += i * i; printf("sig_int finished\n"); return; }Example use of sleep260
  • 61. sleep2函数上述程序运行后,立即键入Ctrl+c 程序输出 Sig_int strarting Sleep2 returned: 0 问题 SIGINT的处理并未结束61
  • 62. sleep函数的实现方式一: Solaris 9使用alarm实现sleep函数 方式二: FreeBSD、Linux使用nanosleep提供时间延迟62
  • 63. sleep的实现方式二Linux中并未提供系统调用sleep,sleep是在库函数中实现的 nanosleep则是Linux的系统调用,提供高分辨率的时间延迟,该函数可以使sleep的实现与信号无关63
  • 64. nanosleep函数该函数挂起调用者线程,直到超时或者接收到信号 函数原型 #include int nanosleep(const struct timespec* rqtp struct timespec* rmtp);64
  • 65. nanosleep函数timespec结构体 struct timespec { time_t tv_sec; //秒数 long tv_nsec; //纳秒数 };65
  • 66. nanosleep函数int nanosleep(const struct timespec* rqtp struct timespec* rmtp); 参数与返回值 rqtp:指定需要等待的时间,秒数和纳秒数 rmtp:当nanosleep返回时,在rmtp指定的结构体中存储距离超时的时间;若不关心该时间,可设为NULL 返回值:超时返回0,被信号中断,则返回-1 示例5.15 66
  • 67. sleep函数实现的问题使用alarm实现的sleep函数,与alarm函数之间存在交互的可能 例如:首先调用alarm(10) 过了3秒,又调用了sleep(5) Sleep将在5秒后返回,但是否在2秒后又产生一个SIGALRM信号呢? POSIX.1标准未对这些交互做任何说明,因此,考虑到移植性,不应对sleep的实现做任何假设67
  • 68. 信号的发送接收机制kill函数、raise函数 alarm函数、pause函数 sleep函数及实现 具有超时功能的API68
  • 69. 中断的系统调用早期的UNIX系统的一个特性是 如果进程在执行一个低速的系统调用(究竟什么是系统调用?与一般函数调用有何区别?)期间,捕捉到了一个信号, 则该系统调用就被中断不再继续执行 该系统调用返回出错,errno被设置为EINTR 这样做的理由:进程可能被这些低速系统调用永远阻塞,例如读网络 69
  • 70. 低速系统调用常见的低速系统调用包括 读写(read、write)管道、终端设备、网络 打开(open)终端设备等 pause、wait函数 某些ioctl操作 某些进程间通信函数 注意:磁盘I/O并不属于这类低速系统调用。因为只要不是硬件错误,I/O操作总会返回。70
  • 71. 被中断的系统调用的处理被中断的系统调用,必须显式地处理出错返回,典型的代码: Again: if((n=read(fd, buf, BUFFSIZE)) < 0) { if(errno == EINTR) goto Again; ………………. }71
  • 72. 中断的系统调用4.2BSD引入了被中断的系统调用的自动重启动 不同的系统对自动重启动设置不一 示例5.16 函数系统是否自动重启?signalsolaris绝不4.2BSD总是Linux默认sigactionPOSIX.1未说明Linux可选72
  • 73. 带超时功能的read函数73
  • 74. 带超时功能的read函数上述程序从标准输入读一行,然后将其写到标准输出上 存在的两个问题 第一次alarm调用和read调用之间存在竞争关系,即在调用read前,alarm超时已经发生 在Linux中,通过signal注册的信号处理机制,被信号中断的系统调用是可以自动重启动的。这种情况下,SIGALRM信号对read的时间限制无作用74
  • 75. 带超时功能的read函数75
  • 76. 带超时功能的read函数上述方案存在的问题 它涉及到与其他信号的相互作用。如果SIGALRM中断了某个其他信号处理程序,则调用longjmp会提早终止该信号处理程序76
  • 77. 第五讲 信号信号的概念 signal函数 不可靠信号与可靠信号 信号的发送接收机制 信号集与可靠信号机制 sigsetjmp和siglongjmp函数 abort函数
  • 78. 信号集和可靠信号机制基本概念 信号集及基本操作 sigprocmask函数,sigpending函数 sigaction函数 sigsuspend函数 可重入函数和线程安全的函数78
  • 79. 可靠信号信号未决(pending):在信号产生和递送之间的时间间隔; 进程可以选用“信号递送阻塞”。如果为进程产生了一个选择为阻塞的信号,而且对该信号的动作是系统默认动作或捕捉该信号,则该信号保持未决状态,直到下述条件之一发生: 对此信号解除了阻塞; 或者将对此信号的动作更改为忽略。 当递送一个原来被阻塞的信号给进程时,才决定其处理方式。即进程在信号递送之前仍可改变对它的动作。 每个进程都有一个信号屏蔽字,它规定了当前要阻塞递送到该进程的信号集。79
  • 80. 信号集信号集:能表示多个信号的数据类型,即信号的集合 例如:信号屏蔽字使用的就是信号集 信号集的类型即 sigset_t 在signal.h中, typedef __sigset_t sigset_t;80
  • 81. 信号集在bits/sigset.h中 #define _SIGSET_NWORDS \ (1024 / (8 * sizeof(unsigned long int))) typedef struct { unsigned long int __val[_SIGSET_NWORDS]; }__sigset_t; sigset_t 是32个4字节81
  • 82. 信号集的处理函数与信号集相关的函数有: int sigemptyset( sigset_t *set ); int sigfillset( sigset_t *set ); int sigaddset( sigset_t *set, int signo); int sigdelset( sigset_t *set, int signo); 以上四个函数成功返回0,出错返回-1 int sigismember(const sigset_t *set, int signo); 若真返回1,若假返回0,出错返回-182
  • 83. sigemptyset函数int sigemptyset( sigset_t *set ); 该函数初始化由set指向的信号集,并清除其中的所有信号 实现(bits/sigset.h)中的关键代码片断 #define __sigemptyset(set) \ int _cnt = _SIGSET_NWORDS; \ sigset_t *_set = (set); \ while (--_cnt >= 0) _set->__val[_cnt] = 0;83
  • 84. sigfillset函数int sigfillset( sigset_t *set ); 该函数初始化由set指向的信号集,使其包括所有信号 实现(bits/sigset.h)中的关键代码片断 #define __sigfillset(set) \ int _cnt = _SIGSET_NWORDS; \ sigset_t *_set = (set); \ while (--_cnt >= 0) _set->__val[_cnt] = ~0UL; 84
  • 85. sigaddset函数int sigaddset( sigset_t *set, int signo); 该函数将一个信号添加到set中 查看bits/sigset.h中的108到120行 实现中的关键代码翻译 int __sigaddset(__sigset_t *__set, int __sig){ unsigned int __mask = 1 << ((__sig-1)%32); unsigned int __word = (__sig-1)/32; __set->__val[__word] |= __mask; ………………….. } 85
  • 86. sigaddset函数在sigset_t结构体中,第一个32比特,对应信号1到31,第二个32比特,对应可靠信号34到64 这一点与教材描述的结构不同 示例5.17 在gdb中,使用“x /12tb &st”查看地址空间。86
  • 87. sigdelset函数int sigdelset( sigset_t *set, int signo); 该函数从set中删除信号signo 查看bits/sigset.h中的108到120行 实现中的关键代码翻译 int __sigdelset(__sigset_t *__set, int __sig){ unsigned int __mask = 1 << ((__sig-1)%32); unsigned int __word = (__sig-1)/32; __set->__val[__word] &= ~__mask; ………………….. } 87
  • 88. sigismember函数int sigismember(const sigset_t *set, int signo); 该函数判断信号signo是否在set中 查看bits/sigset.h中的108到120行 实现中的关键代码翻译 int __sigismember(__const __sigset_t *__set, int __sig){ unsigned int __mask = 1 << ((__sig-1)%32); unsigned int __word = (__sig-1)/32; (__set->__val[__word] & __mask) ? 1 : 0, ………………….. } 88
  • 89. 信号集和可靠信号机制基本概念 信号集及基本操作 sigprocmask函数,sigpending函数 sigaction函数 sigsuspend函数 可重入函数和线程安全的函数89
  • 90. 信号阻塞当程序执行敏感任务时(比如更新数据库),不希望外界信号中断程序的运行。在这个阶段并不是简单地忽略信号,而是阻塞这些信号,当进程处理完关键任务后,还会处理这些信号。 sigprocmask函数可以检测或更改进程的信号屏蔽字90
  • 91. sigprocmask函数该函数检测或更改进程需要阻塞的信号集合,或者同时执行这两个操作 函数原型 #include int sigprocmask(int how, const sigset_t *set, sigset_t *oset); 返回值 成功返回0 出错返回-191
  • 92. sigprocmask函数参数 oset:进程的当前信号屏蔽字通过oset返回,若不关心当前信号屏蔽字,可以设为NULL 若set不为NULL,则 若how=SIG_BLOCK,则进程新的信号屏蔽字是当前信号屏蔽字和set指向信号集的并集,set包含了希望阻塞的附加信号 若how=SIG_UNBLOCK,则进程新的信号屏蔽字是当前信号屏蔽字和set指向信号集补集的交集,set包含了希望解除阻塞的信号 若how=SIG_SETMASK,则进程新的信号屏蔽字将被set指向的信号集的值代替92
  • 93. sigprocmask函数参数 若set为NULL,how也无意义 调用sigprocmask后,如果有任何未决的、不再阻塞的信号,则在sigprocmask返回前,至少会将其中一个信号递送给该进程。 该函数仅为单线程的进程定义的,对于多线程的进程,有另一套函数 示例5.1893
  • 94. sigpending函数该函数返回一个信号集,其中的各个信号对于调用进程是阻塞的而不能递送,因而也一定是当前未决的 函数原型 #include int sigpending ( sigset_t *set ); 参数与返回值 set:sigpending函数填充该结构体,返回未决信号 成功返回0,出错返回-1 程序演示(5.5)94
  • 95. sigprocmask if (sigismember(&pendmask, SIGQUIT)) { cout << "SIGQUIT pending" << endl; } if (sigprocmask(SIG_SETMASK, &oldmask, NULL) < 0) { cout << "SIG_SETMASK error" << endl; return 0; } cout << "SIGQUIT unblocked" << endl; pause();能否使用: SIG_UNBLOCK95
  • 96. 信号集和可靠信号机制基本概念 信号集及基本操作 sigprocmask函数,sigpending函数 sigaction函数 sigsuspend函数 可重入函数和线程安全的函数96
  • 97. sigaction函数该函数用于替换signal函数,即可以检查、修改信号的处理动作 函数原型 #include int sigaction(int signo, const struct sigaction *act, struct sigaction *oact); 返回值 成功返回0,出错返回-197
  • 98. sigaction函数int sigaction(int signo, const struct sigaction *act, struct sigaction *oact); 参数 signo:指定需要处理的特定信号。应该在该信号收到之前调用sigaction。除了SIGSTOP和SIGKILL之外,可以把signo设为任何类型的信号 若act非空,则要修改信号的处理动作 若oact非空,则系统经由oact指针返回该信号的上一个动作 act、oact可以为空98
  • 99. sigaction结构体struct sigaction { void (*sa_handler)(int); sigset_t sa_mask; int sa_flags; void (*sa_sigaction)(int, siginfo_t *, void *); }; sa_handler用于设置信号处理函数的地址,或SIG_IGN、SIG_DFL sa_mask说明了一个信号集,在调用该信号处理函数之前,这一信号集要附加到进程的信号屏蔽字中;仅当从信号处理函数返回时,再将进程的信号屏蔽字复原 在信号处理函数被调用时,操作系统建立的新信号屏蔽字包括了正在被处理的信号。因此,保证了在处理一个给定信号时,同种信号会被阻塞(可靠信号与不可靠信号)99
  • 100. 设置信号处理动作:sigactionsa_flags成员用来改变signo信号的行为属性。sa_flags的若干值可以用或运算组合。 比如把sa_flags设为SA_RESETHAND,使得在执行该信号处理函数时,信号处理函数重新设为SIG_DFL。 把sa_flags设为SA_RESTART,则如果该信号中断了系统调用,在处理完信号处理程序后,重新启动该系统调用。100
  • 101. Linux中常用的sa_flags值SA_INTERRUPT(0x20000000) 由此信号中断的系统调用不会自动重启(示例5.19) SA_NOCLDSTOP(1) 若signo是SIGCHLD,当子进程停止时,不产生此信号,但当其终止时,仍旧产生 SA_NOCLDWAIT(2) 若signo是SIGCHLD,则当调用进程的子进程终止时,不创建僵死进程; 若调用进程在后面调用了wait,则调用进程阻塞,直到其所有子进程都终止,此时返回-1,并将errno设置为ECHILD (示例5.20)101
  • 102. Linux中常用的sa_flags值SA_NODEFER(0x40000000) 在捕捉到此信号时,在执行其信号捕捉函数时,系统不自动阻塞此信号 除非sa_mask包括了此信号 此种类型的操作对应于早期的不可靠信号 SA_RESETHAND(0x80000000) 在执行该信号处理函数时,信号处理函数重新设为SIG_DFL,并清除SA_SIGINFO标志102
  • 103. Linux中常用的sa_flags值SA_RESTART(0x10000000) 由此信号中断的系统调用会自动重启动 SA_SIGINFO(4) 此选项对信号处理函数提供了附加信息:一个指向siginfo结构的指针以及一个指向进程上下文标识符的指针103
  • 104. sigaction结构体struct sigaction { void (*sa_handler)(int); sigset_t sa_mask; int sa_flags; void (*sa_sigaction)(int, siginfo_t *, void *); }; sa_sigaction字段:用于设置一个替代的信号处理函数 若sa_flag指定了SA_SIGINFO标志,则使用由sa_sigaction字段指定的信号处理函数,而不是由sa_handler指定的信号处理函数 注意sa_sigaction和sa_handler可能使用同一个存储区域,因此只能使用其中之一104
  • 105. 信号处理函数原来的信号处理函数原型: void handler(int signo); 现在的信号处理函数原型: void handler(int signo, siginfo_t *info, void *context); 查看siginfo_t结构(程序5.6) Context:可以转换为ucontext_t类型指针,用于标识信号传递时进程的上下文105
  • 106. sigqueue函数用于向进程发送一个带参数的信号 函数原型: int sigqueue(pid_t pid, int signo, const union sigval val); val: typedef union sigval { int sival_int; //该值可以传递给信号处理函数 void *sival_ptr; }sigval_t106
  • 107. 信号处理函数void handler(int signo, siginfo_t *info, void *context) { cout << info->si_value.sival_int << endl; } 程序演示5.7107
  • 108. 信号集和可靠信号机制基本概念 信号集及基本操作 sigprocmask函数,sigpending函数 sigaction函数 sigsuspend函数 可重入函数和线程安全的函数108
  • 109. 保护一段代码使其不被某种信号中断信号首先被阻塞,若在此处被处理,pause可能永远睡眠109
  • 110. sigsuspend函数针对上述问题,需要在一个原子操作中先恢复信号屏蔽字,然后使进程休眠;该功能可由sigsuspend函数实现。 函数原型 #include int sigsuspend(const sigset_t *sigmask); 将进程的信号屏蔽字设为sigmask指向的值,在捕捉到一个信号之前,该进程挂起; 从一个信号处理函数返回后,sigsuspend返回,并将信号屏蔽字恢复为调用sigsuspend之前的值; 函数总是返回-1,errno设置为EINTR110
  • 111. sigsuspend函数该函数可以用于保护临界区代码,使其不被指定的信号中断。 该函数也可以用于等待一个信号处理程序设置一个全局变量111
  • 112. sigsuspend保护临界区代码#include #include #include static void sig_int(int); int main(void) { sigset_t newmask, oldmask, zeromask; if (signal(SIGINT, sig_int) == SIG_ERR) err_sys("signal(SIGINT) error"); sigemptyset(&zeromask); sigemptyset(&newmask); sigaddset(&newmask, SIGINT); if (sigprocmask(SIG_BLOCK, &newmask, &oldmask) < 0) err_sys("SIG_BLOCK error");112
  • 113. /* critical region of code */ pr_mask("in critical region: "); /* allow all signals and pause */ if (sigsuspend(&zeromask) != -1) err_sys("sigsuspend error"); pr_mask("after return from sigsuspend: "); /* reset signal mask which unblocks SIGINT */ if (sigprocmask(SIG_SETMASK, &oldmask, NULL) < 0) err_sys("SIG_SETMASK error"); /* and continue processing ... */ exit(0); } static void sig_int(int signo) { pr_mask("\nin sig_int: "); return; }113
  • 114. volatile sig_atomic_t quitflag; /* set nonzero by signal handler */ int main(void) { void sig_int(int); sigset_t newmask, oldmask, zeromask; if (signal(SIGINT, sig_int) == SIG_ERR) err_sys("signal(SIGINT) error"); if (signal(SIGQUIT, sig_int) == SIG_ERR) err_sys("signal(SIGQUIT) error"); sigemptyset(&zeromask); sigemptyset(&newmask); sigaddset(&newmask, SIGQUIT); pr_mask("Before Sigprocmask:");sigsuspend: wait for global variable114
  • 115. /* block SIGQUIT and save current signal mask */ if (sigprocmask(SIG_BLOCK, &newmask, &oldmask) < 0) err_sys("SIG_BLOCK error"); pr_mask("Before critical code:"); while (quitflag == 0) sigsuspend(&zeromask); pr_mask("After critical code:"); /* SIGQUIT has been caught and is now blocked; do whatever */ quitflag = 0; /* reset signal mask which unblocks SIGQUIT */ if (sigprocmask(SIG_SETMASK, &oldmask, NULL) < 0) err_sys("SIG_SETMASK error"); pr_mask("After Sigprocmask:"); exit(0); }115
  • 116. void sig_int(int signo) { if(signo == SIGINT) printf(…..); else quitflag = 1; }116
  • 117. sleep的可靠实现(三)static void sig_alrm(void) { return; /* nothing to do, just returning wakes up sigsuspend() */ } unsigned int sleep(unsigned int nsecs) { struct sigaction newact, oldact; sigset_t newmask, oldmask, suspmask; unsigned int unslept; newact.sa_handler = sig_alrm; sigemptyset(&newact.sa_mask); newact.sa_flags = 0;这个程序涉及到信号处理的方方面面,希望大家认真阅读。117
  • 118. sigaction(SIGALRM, &newact, &oldact); /* set our handler, save previous information */ sigemptyset(&newmask); sigaddset(&newmask, SIGALRM); /* block SIGALRM and save current signal mask */ sigprocmask(SIG_BLOCK, &newmask, &oldmask); alarm(nsecs); suspmask = oldmask; sigdelset(&suspmask, SIGALRM); /* make sure SIGALRM isn't blocked */ sigsuspend(&suspmask); /* wait for any signal to be caught */ /* some signal has been caught, SIGALRM is now blocked */118
  • 119. unslept = alarm(0); sigaction(SIGALRM, &oldact, NULL /* reset signal mask, which unblocks SIGALRM */ sigprocmask(SIG_SETMASK, &oldmask, NULL); return(unslept); }119
  • 120. 信号集和可靠信号机制基本概念 信号集及基本操作 sigprocmask函数,sigpending函数 sigaction函数 sigsuspend函数 可重入函数和线程安全的函数120
  • 121. 可重入函数进程捕捉到信号并对其进行处理时,进程正在执行的指令序列就被信号处理函数临时中断 从信号处理函数返回后,则继续执行被中断的正常指令序列 假设进程正在调用malloc函数,信号将其中断,信号处理函数中也调用malloc函数,会有什么问题?121
  • 122. 可重入函数可重入函数是指: 某个函数被信号中断执行,信号处理函数中又调用了该函数,而不会带来任何问题的函数 不可重入的原因 使用静态的数据结构 调用malloc或free 标准I/O函数,例如printf等122
  • 123. 可重入函数每个线程只有一个errno。 当信号处理函数调用某个API后,errno可能被重新设置 这样,被信号中断的程序将看到被修改后的errno,而不是原来的值 所以,信号处理函数中,需要首先保存errno,在信号处理函数返回前,恢复errno123
  • 124. 线程安全的函数对于同一个函数,若两个或两个以上的线程同时执行,均得到正确的结果,则称该函数是线程安全的函数 可重入的函数一定是线程安全的函数? 线程安全的函数,不一定是可重入的? 例如:互斥变量保护的临界区 例如malloc函数124
  • 125. 信号集和可靠信号机制基本概念 信号集及基本操作 sigprocmask函数,sigpending函数 sigaction函数 sigsuspend函数 可重入函数和线程安全的函数125
  • 126. 第五讲 信号信号的概念 signal函数 不可靠信号与可靠信号 信号的发送接收机制 信号集与可靠信号机制 sigsetjmp和siglongjmp函数 abort函数
  • 127. sigsetjmp和siglongjmp函数P57在分析sleep2时,使用到了setjmp和longjmp 在信号处理中,当捕捉到信号并进入信号处理函数时,当前信号被自动加入到进程的信号屏蔽字中 当退出信号处理函数时,当前信号又会从进程信号屏蔽字中删除 当使用longjmp跳出信号处理函数时,信号屏蔽字会有何改动?(示例5.21)127
  • 128. sigsetjmp和siglongjmp函数从示例5.21可知,longjmp并未恢复进程的信号屏蔽字 POSIX.1定义了sigsetjmp和siglongjmp,以解决上述问题。 调用sigsetjmp时,除了保存上下文外,还将保存当前的信号屏蔽字 调用siglongjmp执行跳转后,将恢复调用sigsetjmp时的信号屏蔽字128
  • 129. sigsetjmp和siglongjmp函数函数原型 #include int sigsetjmp(sigjmp_buf env, int savemask); 参数和返回值 若savemask非0,则sigsetjmp在env中保存进程的当前信号屏蔽字 返回值:若直接调用则返回0,若从siglongjmp调用返回则返回siglongjmp的第二个参数129
  • 130. sigsetjmp和siglongjmp函数函数原型 #include void siglongjmp(sigjmp_buf env, int val); 参数 val作为sigsetjmp的返回值130
  • 131. static void sig_usr1(int), sig_alrm(int); static sigjmp_buf jmpbuf; static volatile sig_atomic_t canjump; int main(void) { if (signal(SIGUSR1, sig_usr1) == SIG_ERR) err_sys("signal(SIGUSR1) error"); if (signal(SIGALRM, sig_alrm) == SIG_ERR) err_sys("signal(SIGALRM) error"); pr_mask("starting main: "); /* {Prog prmask} 10.10*/ if (sigsetjmp(jmpbuf, 1)) { pr_mask("ending main: "); exit(0); } canjump = 1; /* now sigsetjmp() is OK */ for ( ; ; ) pause(); }131
  • 132. static void sig_usr1(int signo) { time_t starttime; if (canjump == 0) return; /* unexpected signal, ignore */ pr_mask("starting sig_usr1: "); alarm(3); /* SIGALRM in 3 seconds */ starttime = time(NULL); for ( ; ; ) /* busy wait for 5 seconds */ if (time(NULL) > starttime + 5) break; pr_mask("finishing sig_usr1: "); canjump = 0; siglongjmp(jmpbuf, 1); /* jump back to main, don't return */ …… }保护机制132
  • 133. static void sig_alrm(int signo) { pr_mask("in sig_alrm: "); return; } 133
  • 134. 执行序列mainsignal() signal() pr_mask() sigsetjmp() pause()sig_usr1递送SIGUSR1pr_mask() alarm() time() time()sig_alrmpr_mask() return()递送SIGALRM从信号处理程序返回pr_mask() siglongjmp()sigsetjmp() pr_mask() exit()134
  • 135. If we take sigsetjmp with non-zero value for parameter savesigs135
  • 136. If we take sigsetjmp with zero value for parameter savesigs136
  • 137. 示例程序-几点注意该程序演示两种信号特性: 当执行信号处理程序时,自动阻塞该信号; 信号能嵌套发生。 sigsetjmp和siglongjmp的联合使用方式; sigsetjmp和siglongjmp联合使用必须要有保护机制,即程序中的canjmp变量的判断;保护变量定义为sig_atomic_t是为了保证在写该变量时不会被中断。137
  • 138. 示例程序示例程序中,信号有可能中断sigsetjmp的调用,即jmpbuf并未正确地保存现场;这样在信号处理函数中,调用siglongjmp将可能出错 针对这种情况,引入了canjump变量。只有当canjump变量被设为1后,信号处理函数才能调用siglongjmp138
  • 139. 示例程序主程序中设置canjmp时,会被信号中断,信号处理函数读canjmp,而引起错误吗? canjmp的类型是sig_atomic_t,在写这种类型的变量时,不会被中断 在具有虚拟存储器的系统上,这种变量不会垮页 可以用一条机器指令对其进行访问 为什么又使用volatile?139
  • 140. 第五讲 信号信号的概念 signal函数 不可靠信号与可靠信号 信号的发送接收机制 信号集与可靠信号机制 sigsetjmp和siglongjmp函数 abort函数
  • 141. abort函数该函数可以使异常程序终止 函数原型 #include void abort(); 此函数将SIGABRT信号发送给调用进程 ISO C要求若捕捉到此信号而且相应信号处理程序返回,abort仍不会返回到其调用者(示例5.22)141
  • 142. abort函数POSIX.1说明了abort并不理会进程对SIGABRT的阻塞和忽略 让进程捕捉该信号的意图 在进程终止前由其执行所需的清理操作 若信号处理函数不终止自己,abort终止该进程 142
  • 143. 第五讲 信号信号的概念 signal函数 不可靠信号与可靠信号 信号的发送接收机制 信号集与可靠信号机制 sigsetjmp和siglongjmp函数 abort函数