HeadFirstC笔记_10 进程间通信:沟通的艺术

输入输出重定向
在命令行运行程序时,可以用“>”运算符把标准输出 重定向到文件:
    
     
     
  1. 命令:python ./rssgossip.py Snooki > stories.txt
标准输出是三大默认数据流之一。顾名思义,数据流就 是流动的数据,数据从一个进程流出,然后流入另一个 进程。除了标准输入、标准输出和标准错误,还有其他 形式的数据流,例如文件连接和网络连接也属于数据流。 重定向进程的输出,相当于改变进程发送数据的方向。 原来标准输出会把数据发送到屏幕,现在可以让它把数 据发送到文件。 在命令行中,重定向是非常有用的命令。
但有没有办法 让进程重定向自己呢?

进程内部一瞥
进程含有它正在运行的程序,还有栈和堆数据空间。除此之
外,进程还需要记录数据流的连向,比如标准输出连到了哪
里。进程用文件描述符表示数据流,所谓的描述符其实就是
一个数字。进程会把文件描述符和对应的数据流保存在描述符
表中。
文件描述符是一个数字,它代表一条数据流。
文件描述符 数据流  
键盘  
1 屏幕  
2 屏幕  
描述符表的前三项万年不变:0号标准输入,1号标准输出,2
号标准错误。其他项要么为空,要么连接进程打开的数据流。
比如程序在打开文件进行读写时,就会占用其中一项。
创建进程以后,标准输入连到键盘,标准输出和标准错误连
到屏幕。它们会保持这样的连接,直到有人把它们重定向到
了其他地方。

重定向即替换数据流
如果想重定向标准输出,只需要修改表
中1号描述符对应的数据流就行了,标准输入标准错误等流同理。
所有向标准输出发送数据的函数会先查看描述符表, 看1号描述符指向哪条数据流,然后再把数据写到这 条数据流中 ,printf() 便是如此。只要修改描述符表,进程也能 重定向它们自己。

难怪要用“2>”
你可以在命令行用“>”运算符重定向标准输
出,用“2>”重定向标准错误:
               
                
                
  1. 命令:./myprog > output.txt 2> errors.log
现在,知道为什么标准错误要用“2 >”来
重定向了吧,因为2 是标准错误在描述符
表中的编号 。在很多操作系统中,也可
以用“1 >”来重定向标准输出。而在类
Unix操作系统中,可以用以下命令把标
准错误和标准输出重定向到一个地方:
                   
                    
                    
  1. 命令: ./myprog 2>&1
  2. // “2>”表示“重定向标准错误”。 &1”表示“到标准输出"

fileno()返回描述符号
每打开一个文件,操作系统都会在描述符表中新注册一项。
假设你打开了某个文件:
                         
                          
                          
  1. FILE *my_file = fopen("guitar.mp3", "r");
操作系统会打开guitar.mp3文件,然后返回一个指向它的
指针,操作系统还会遍历 描述符表寻找空项,把新文件注
册在其中,如下图的3
文件描述符 数据流  
键盘  
1 屏幕  
2 屏幕  
3 guitar.mp3
那么如何根据文件指针知道它是几号描述符呢?答案是 用 fileno() 函数:
                                      
                                       
                                       
  1. int descriptor = fileno(my_file);
在失败时不返回-1的函数很少, fileno() 就是其中之一。只
要你把打开文件的指针传给 fileno() ,它就一定会返回描述符
编号。

dup2()复制数据流
每次打开文件都会使用描述符表中新的一项。但如果你想修 改某个已经注册过的数据流,比如想让3号描述符重新指向 其他数据流,该怎么做?
可以用 dup2() 函数, dup2() 可以复 制数据流。假设你在3号描述符中注册了log.txt文件指针, 下面这行代码就能同时把它连接到2号描述符:
                                          
                                           
                                           
  1. dup2(3, 2); // 把2重定向成了和3一样

错误处理代码的抽取
下面两段代码一看头就大:
                                           
                                            
                                            
  1. pid_t pid = fork();
  2. if (pid == -1) {
  3. fprintf(stderr, " 无法克隆进程: %s\n", strerror(errno));
  4. return 1;
  5. }
  6. if (execle(...) == -1) {
  7. fprintf(stderr, " 无法运行脚本: %s\n", strerror(errno));
  8. return 1;
  9. }
首先, 需要把处理代码放到一个单独的error()函数中,然后把return语句换成exit()系统调用。
                                            
                                             
                                             
  1. #include <stdlib.h> //为了使用exit系统调用,必须包含stdlib.h头文件
  2. void error(char *msg)
  3. {
  4. fprintf(stderr, "%s: %s\n", msg, strerror(errno));
  5. exit(1); //此句会立刻终止程序,并把退出状态置1。
  6. }
现在就可以把那些烦人的错误检查代码换成:
                                             
                                              
                                              
  1. pid_t pid = fork();
  2. if (pid == -1) {
  3. error(" 无法克隆进程 ");
  4. }
  5. if (execle(...) == -1) {
  6. error(" 无法运行脚本 ");
  7. }
注意:exit()系统调用是结束程序的最快方式。完全不用操心怎么返回主函数,直接调用exit(),你 的程序就会灰飞烟灭!

运行结果:

 rssgossip.py 确实把数据发往标准输出时,数据应该出现在 stories.txt 文件中。但有时又是空的.这是为何?

有时需要等待......
ewshound2 程序启用独立的进程运行rssgossip.py脚本,而子进程一创建就和父进程没关系了。rssgossip.py还没有完成任务, newshound2 程序就结束了,所以stories.txt还是空的。也就是说,操作系统必须提供一种方式,让你等待子进程完成任务。

waitpid() 函数
waitpid() 函数会等子进程结束以后才返回,也就是说可以在程序中加几行代码,让它等rssgossip.py脚本运行结束以后才退出。
新代码加到newshound2程序底部:
                                                 
                                                  
                                                  
  1. #include <sys/wait.h>
  2. ...
  3. int pid_status; //这个变量用来保存进程信息
  4. if (waitpid(pid, &pid_status, 0) == -1) {
  5. error(" 等待子进程时发生了错误 ");
  6. }
  7. return 0;
waitpid()的三个参数聚焦
1.Pid:父进程在克隆子进程时会得到子进程的ID。
2.pid_status: 用来保存进程的退出信息。因为 waitpid() 需要修改pid_status ,因此它必须是个指针。
3.选项: waitpid() 有一些选项,详情可以输入 man waitpid查看, 若把选项设为 0 ,函数将等待进程结束。

什么是 pid_status ?
waitpid() 函数结束等待时会在 pid_status 中保存一个值,它告诉你进程的完成情况。为了得到子进程的退出状态,可以把 pid_status 的值传给 WEXITSTATUS() 宏:
                                               
                                                
                                                
  1. if (WEXITSTATUS(pid_status)) //如果退出状态不是 0
  2. Puts("Error status non-zero");
为什么要用宏来查看?
因为 pid_status 中保存了好几条信息,只有前8位表示进程的退出状态,可以用宏来查看这8位的值。

运行 newshound2 程序,它会在退出前检查rssgossip.py脚本是否完成:

 重定向输入、输出,然后让进程相互等待,进程间通信就这么简单。

问: 为了防止调用失败,调用exit()时需要检查它的返回值是否为-1吗?
答: 不需要,exit()不会失败,因此也就没有返回值。exit()是唯一没有返回值而且不会失败的函数。
 
问: 描述符表有多大?
答: 从0号到255号.
 
问: 我能用waitpid()等待其他进程吗?还是只有我启动的那些?
答: 你可以用waitpid()等待任何进程。
 
问 :为什么不能根据wait_pid(..., &pid_status, ...)中的pid_status直接判断退出状态?
答: 因为pid_status中还包含了其他信息。比如,如果一个进程自死亡 WIFSIGNALED(pid_status)就为假,若为他杀则为真.
 
问: pid_status明明是整型变量,怎么可以包含多条信息?
答: 用不同的位来保存不同的信息。pid_status的前8位保存了退出状态,而其他信息保存在了剩余那些位中。
 
问 : 是不是只要自行提取出pid_status的前8位,就可以不用WEXITSTATUS()?
答: 最好还是用WEXITSTATUS(),它不但可以提高代码的可读性,而且无论int在你的平台上有多大,程序都能正确工作。
 
问: 为什么WEXITSTATUS()要大写?
答: 因为WEXITSTATUS()是宏,不是函数。编译器运行时会把宏替换为一小段代码。
 
家书抵万金
你已经学会了用 exec() 和 fork() 运行独立进程,也知道怎么把子进程的输出重定向到文件,但如果你想从子进程直接获取数据呢?在进程运行时实时读取它生成的数据,而不
是等子进程把所有数据都发送到文件,再从文件中把数据读取出来,有这个可能吗?

实例:从 rssgossip 读取新闻链接
为了举这个例子,rssgossip.py脚本提供了一个 -u 选项,可以用它显示找到新闻的URL:
                                              
                                               
                                               
  1. linux命令:export RSS_FEED=http://syndication.eonline.com/syndication/feeds/rssfeeds/topstories.xml
  2. linux命令:python rssgossip.py 'plan'
运行结果:

 你完全可以运行脚本,然后把它的输出保存在文件中,不过这样很慢。如果父子进程能够在子进程运行期间通信,速度会快很多。

用管道连接进程
你曾用过某样东西实时连接两个进程,那就是管道。可以在命令行用管道把一个进程的输出连接到另一个进程的输入。在下面这个例子中,你手动运行了rssgossip.py脚本,然后把它的输出传给了 grep 命令, grep 找出了包含 http 的那些行:
 

管道两侧的命令是父子关系
当你在命令行用管道连接两条命令时,实际把它们当成了父子进程来连接,在上面的例子中, grep 命令是rssgossip.py脚本的父进程,过程如下:
1.命令行创建了父进程。
2.父进程在子进程中克隆出了rssgossip.py脚本。
3.父进程用管道把子进程的输出连接到自己的输入。
4.父进程运行了grep命令。  
管道常用来在命令行中连接两个进程。下面看在C代码中如何连接两个进程.
 
案例研究:在浏览器中打开新闻
假设你想在浏览器中打开rssgossip.py脚本找到的新闻链接,你将在父进程中运行程序,在子进程中运行rssgossip.py。需要创建管道,把rssgossip.py的输出和程序的输入连接起来。
 
如何创建管道?---pipe() 打开两条数据流
每当打开数据流时,它都会加入描述符表。 pipe() 函数也是如此,它创建两条相连的数据流,并把它们加到表中,然后只要你往其中一条数据流中写数据,就可以从另一条数据流中读取。
pipe() 在描述符中创建这两项时,会把它们的文件描述符保存在一个包含两个元素的数组中:
                                                 
                                                  
                                                  
  1. int fd[2];
  2. if (pipe(fd) == -1) { //把数组名传递给pipe()函数
  3. error("Can't create the pipe");
  4. }
 
pipe() 函数创建了管道,并返回了两个描述符:
fd[1]用来向管道写数据,
fd[0]用来从管道读数据。
 
你将在父、子进程中使用这两个描述符:
子进程
在子进程中,需要关闭管道的 fd[0] 端,然后修改子进程的标准输出,让它指向描述符 fd[1] 对应的数据流。
                                                 
                                                  
                                                  
  1. close(fd[0]);
  2. dup2(fd[1], 1);
如此一来,子进程发送给标准输出的数据都会写到管道中。
 
父进程
在父进程中,需要关闭管道的 fd[1] 端(不需要写),然后重定向父进程的标准输入,让它从描述符 fd[0] 对应的数据流中读取数据:
                                                 
                                                  
                                                  
  1. dup2(fd[0], 0);
  2. close(fd[1]);
如此一来,子进程写到管道的数据将由父进程的标准输入读取。

在浏览器中打开网页 示例代码:
                                                
                                                 
                                                 
  1. #include <errno.h>
  2. #include <stdio.h>
  3. #include <stdlib.h>
  4. #include <string.h>
  5. #include <unistd.h>
  6. // 打印错误
  7. void error(char *msg) {
  8. fprintf(stderr, "%s: %s\n", msg, strerror(errno));
  9. exit(1); // from -->stdlib.h //exit(1)会立刻终止程序,并把退出状态置1。
  10. }
  11. // 调用系统浏览器打开url
  12. void open_url(char *url) {
  13. char launch[255];
  14. //sprintf(launch, "cmd /c start %s", url); // windows
  15. //system(launch);
  16. sprintf(launch, "x-www-browser '%s'", url); // linux
  17. system(launch);
  18. //sprintf(launch, "open '%s'", url); // mac
  19. //system(launch);
  20. }
  21. int main(int argc, char *argv[]) {
  22. char *phrase = argv[1];
  23. char *vars[] =
  24. {
  25. "RSS_FEED=http://syndication.eonline.com/syndication/feeds/rssfeeds/topstories.xml",
  26. // "RSS_FEED=http://www.msn.com/rss/MsnEntertainment.aspx",
  27. NULL };
  28. int fd[2];
  29. if (pipe(fd) == -1) {
  30. error("Can't create the pipe");
  31. }
  32. pid_t pid = fork();
  33. if (pid == -1) {
  34. error("Can't fork process");
  35. }
  36. printf("pid=%i\n", pid);
  37. printf("!pid=%i\n", !pid);
  38. if (!pid) {
  39. // puts(phrase);
  40. //把标准输出设为管道的写入端
  41. dup2(fd[1], 1);
  42. close(fd[0]);
  43. if (execle("/usr/bin/python", "/usr/bin/python", "rssgossip.py", "-u",
  44. phrase, NULL, vars) == -1) {
  45. error("Can't run script");
  46. }
  47. }
  48. dup2(fd[0], 0);
  49. close(fd[1]);
  50. char line[255];
  51. while (fgets(line, 255, stdin)) {
  52. if (line[0] == '\t')
  53. open_url(line + 1);
  54. }
  55. return 0;
  56. }
运行结果:

 浏览器打开了:

 管道是连接进程的好办法。现在你不但能够运行进程,控制它们的环境,而且还能获取进程的输出,这样就可以实现很多功能。任何一个能够在命令行中运行的程序你都可以在C代码中调用并控制它。

问: 管道是文件吗?
答: 这取决于操作系统创建管道的方式,通常用 pip e() 创建的管道都不是文件。
 
问: 就是说也有可能是文件?
答 : 你可以创建基于文件的管道,它们通常叫有名管道或FIFO(First In First Out,先进先出)文件。
 
问: 它能干嘛?
答: 因为基于文件的管道有名字,所以两个进程只要知道管道的名字也能用它来通信,即使它们非父子进程。
 
问: 太好了!怎么使用有名管道?
答: 使用mkfifo()系统调用,详情请见http://tinyurl.com/cdf6ve5。
 
问: 如果不用文件来实现管道,那用什么?
答:通常用存储器。数据写到存储器中的某个位置,然后再从另一个位置读取。
 
问: 如果我试图读取一个空的管道会怎么样?
答: 程序会等管道中出现东西。
 
问: 父进程如何知道子进程什么时候结束?
答: 子进程结束时,管道会关闭。fgets()将收到EOF(End Of File,文件结束符),于是fgets()函数返回0,循环就结束了。
 
问: 父进程能对子进程说话吗?
答: 当然可以。你完全可以反向连接管道,让数据从父进程发送到子进程。
 
问: 管道能够双向通信吗?这样父子进程不就可以边听边讲了?
答: 管道只能单向通信。不过可以创建两个管道,一个从父进程连到子进程,另一个从子进程连到父进程。
 
进程之死
比如你的程序正在从键盘读取数据,这时用户按了Ctrl+C,整个程序就停止运行了,这是为何?
 
操作系统用信号控制程序
当操作系统看到用户按了Ctrl+C,就会向程序发送中断信号。信号是一条短消息,即一个整型值。当信号到来时,进程必须停止手中一切工作去处理信号。进程会查看信号映射表,表中每个信号都对应一个信号处理器函数。中断信号的默认信号处理器会调用 exit() 函数。
操作系统为什么不直接结束程序,而是要在信号表中查找信号?
因为这样就可以在进程接收到信号时运行你自己的代码。
信号 处理函数
SIGURG 不做事情
SIGINT(值为2) 调用exit()

捕捉信号然后运行自己的代码
有时你希望在别人打断你的程序时运行自己的代码。假设进程打开了一些文件连接或网络连接,你希望在退出之前把它们关闭,并且做一些清理工作。当计算机在向你发送信号时,如何让它运行你的代码呢?可以用 sigaction 。
 
sigaction 是一个函数包装器
sigaction 是一个结构体,它有一个函数指针。 sigaction告诉操作系统进程收到某个信号时应该调用哪个函数。如果想在某人向进程发送中断信号时让操作系统调用 diediedie() 函数,就需要把 diediedie() 函数包装成 sigaction。
sigaction 的创建方法如下:
                                             
                                              
                                              
  1. struct sigaction action; // 创建新动作
  2. action.sa_handler = diediedie; // 想让计算机调用哪个函数
  3. sigemptyset(&action.sa_mask); //用掩码来过滤 sigaction 要处理的信号,通常会用一空的掩码
  4. action.sa_flags = 0; // 一些附加标志位,将他们置0即可.
sigaction 包装起来的那个函数就叫处理器,因为它将用来处理发送给它的信号,而处理器必须以特定的方式创建。

处理器必须接收信号参数
信号是一个整型值,如果你创建了一个自定义处理器函数,就需要接收一个整型参数,像这样:
                                              
                                               
                                               
  1. void diediedie(int sig){ //sig是处理函数捕捉到的信号编号
  2. puts ("Goodbye cruel world....\n");
  3. exit(1);
  4. }
因为我们以参数的形式传递信号,所以多个信号可以共用一个处理器;也可以为每个信号写一个处理器,完全由你做主。处理器的代码应该短而快,刚好能处理接收到的信号就好。

注意:在处理器函数中使用标准输出和标准错误时要小心。
虽然示例代码在标准输出中显示了文本,但在更复杂的程序中这么做时千万要小心。之所以会有信号就是因为程序中发生了故障,而故障可能就是标准输出无法使用,因此要小心。
 
用 sigaction() 来注册 sigaction
sigaction(signal_no, &new_action, &old_action);
1.signal_no,信号编号:这个整型值代表了你希望处理的信号。通常会传递 SIGINT或SIGQUIT这样的标准信号。
2.new_action,新动作:你想注册的新 sigaction 的地址.
3.old_action,旧动作:如果你想保存被替换的信号处理器, 可以再传一个sigaction 指针;如果不想保存,可以设置为NULL。
如果 sigaction() 函数失败,会返回-1,并设置 errno 变量。
 
代码示例:
                                             
                                              
                                              
  1. #include <signal.h> // 需要包含 signal.h 头文件。
  2. #include <stdio.h>
  3. #include <stdlib.h>
  4. // 捕捉到信号后的处理函数
  5. void diediedie(int sig) {
  6. puts("Goodbye cruel world....\n");
  7. exit(1);
  8. }
  9. //注册处理器的函数
  10. int catch_signal(int sig, void (*handler)(int)) {
  11. // struct sighandler_t action;//windows中
  12. struct sigaction action; // linux系统中的
  13. action.sa_handler = handler;
  14. sigemptyset(&action.sa_mask);
  15. action.sa_flags = 0;
  16. return sigaction(sig, &action, NULL);
  17. // return signal(sig, &action);
  18. }
  19. int main() {
  20. // SIGNIT--->表示中断信号
  21. if (catch_signal(SIGINT, diediedie) == -1) {
  22. fprintf(stderr, "Can't map the handler");
  23. exit(2);
  24. }
  25. char name[30];
  26. printf("Enter your name:");
  27. fgets(name, 30, stdin);
  28. printf("Hello %s\n", name);
  29. return 0;
  30. }
运行结果:

  操作系统收到了Ctrl-C以后向进程发送 SIGINT 信号,然后进程运行了你的 diediedie() 函数。
 
操作系统向进程发送的各种信号,及对应引起它们的原因:
SIGINT 进程被中断。
SIGQUIT
有人要求停止进程,并把存储器中的内容保
存到核心转储文件。
SIGFPE 浮点错误
SIGTRAP 调试人员询问进程执行到了哪里。
SIGSEGV 进程企图访问非法存储器地址。
SIGWINCH 终端窗口的大小发生改变。
SIGTERM 有人要求内核终止进程。
SIGPIPE 进程在向一个没有人读的管道写数据。

问: 如果中断信号的处理器不调用exit(),程序还能结束吗?
答: 不会。
 
问: 也就是说,我可以写一个忽略中断信号的程序?
答: 可以是可以,但这可不是什么好主意。程序收到错误信号以后最好还是退出,即使之前你运行了自己的代码。
 
用 kill 发送信号
怎么测试你写的信号处理代码呢?在类Unix操作系统中有一个叫 kill 的命令,之所以叫 kill 是因为这个命令通常用来“杀死”进程。事实上, kill 只是向进程发送了一个信号, kill 默认会向进程发送 SIGTERM 信号,你也可以用它发送其他信号。
我们打开两个终端试试。在一个终端运行程序,在另一个终端用 kill 向程序发送信号:

 
以上 kill 命令将向进程发送信号,然后运行进程中配置好的处理函数。但有一个例外,代码捕捉不到 SIGKILL 信号,也没法忽略它。也就是说,即使程序中有一个错误导致进程对任何信号都视而不见,还是能用 kill –KILL 结束进程.
也就是说:
命令kill -KILL <进程号> 一定可以送你的程序上西天!
另外,SIGSTOP 信号也是无法忽略的,它可以用来暂停进程.
 
用 raise() 发送信号
有时你想让进程向自己发送信号,这时就可以用 raise()函数。
                                                
                                                 
                                                 
  1. raise(SIGTERM);
通常会在自定义的信号处理函数中使用 raise() ,这样程序就能在接收到低级别的信号时引发更高级别的信号。这叫 信号升级。
 
打电话叫程序起床
当计算机中发生了进程需要知道的事情时,操作系统就会向进程发送信号。比如用户想中断进程或“杀死”进程,或进程企图做一件它不应该做的事情,比如访问受限存储器。除了在发生错误时使用,有时进程也需要产生自己的信号,比如闹钟信号 SIGALRM 。闹钟信号通常由进程的间隔定时器创建。间隔定时器就像一台闹钟:你可以定一个时间,其间程序就会去做
其他事情:
                                              
                                               
                                               
  1. alarm(120); //把闹钟调到120秒以后闹铃。

定时器发出 SIGALRM 信号
当进程收到信号以后就会停止手中一切工作来处理信号。进程在收到闹钟信号以后默认会结束进程,但通常情况下使用定时器不是为了让它帮你“杀死”程序,而是为了利用闹钟信号的处理器去做另一件事:
                                              
                                               
                                               
  1. catch_signal(SIGALRM, pour_coffee);
  2. alarm(120);

闹钟信号可以实现多任务。如果需要每隔几秒运行一个任务,或者想限制花费在某个任务上的时间,就可以用闹钟信号让程序打断自己。
 
注意:不要同时使用alarm()和sleep()。
sleep()函数会让程序沉睡一段时间 。和alarm() 函数一样,它也使用了间隔计时器,因此
同时使用这两个函数会发生冲突。
 
重置信号
你已经见过如何设置自定义信号处理器了,但如果你想还原默认的信号处理器怎么办?signal.h头文件中有一个特殊的符号 SIG_DFL ,它代表以默认方式处理信号。
                                               
                                                
                                                
  1. catch_signal(SIGTERM, SIG_DFL); 
忽略信号
你还可以用 SIG_IGN 符号让进程忽略某个信号。
                                               
                                                
                                                
  1. catch_signal(SIGINT, SIG_IGN);
在你决定忽略某个信号前一定要慎重考虑,信号是控制进程和终止进程的重要方式,如果忽略了它们,程序就很难停下来。
 
问: 我能把闹钟定在几分之一秒后响铃吗?
答: 可以是可以,但很复杂。需要用另一个函数setitimer() ,它可以把进程间隔计时器的单位设为几分之一秒。详情请见http://tinyurl.com/3o7hzbm。
 
问: 为什么一个进程只有一个定时器?
答: 定时器由操作系统的内核管理,如果一个进程有很多定时器,内核就会变得很慢,因此操作系统需要限制进程能使用的定时器个数。
 
问: 定时器可以实现多任务?太好了,也就是说我能同时做几件事?
答: 非也。别忘了,进程在处理信号时会停止一切工作,也就是说一次只能做一件事,稍后会看到如何让代码同时做多件事。
 
问: 重复设置定时器会怎么样?
答: 每次调用alarm()函数都会重置定时器,也就是说如果把闹钟调到10秒,但过一会儿又把它设为了10分钟,那么闹钟信号10分钟以后才会触发,第一个10秒计时就失效了。
练习:
这个程序用来测试用户的数学水平,它要求用户做乘法。程序的结束条件如下:
1. 用户按了Ctrl-C。
2. 回答时间超过5秒。
程序在结束时会显示最终得分并把退出状态设为0。

代码:
                                             
                                              
                                              
  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. #include <unistd.h>
  4. #include <time.h>
  5. #include <string.h>
  6. #include <errno.h>
  7. #include <signal.h>
  8. int score = 0; //记录总分
  9. //退出程序的处理函数
  10. void end_game(int sig) {
  11. printf("\nFinal score: %i\n", score);
  12. exit(0);
  13. }
  14. //注册信号处理器函数
  15. int catch_signal(int sig, void (*handler)(int)) {
  16. struct sigaction action;
  17. action.sa_handler = handler;
  18. sigemptyset(&action.sa_mask);
  19. action.sa_flags = 0;
  20. return sigaction(sig, &action, NULL);
  21. }
  22. // 时间到了的处理函数
  23. void times_up(int sig) {
  24. puts("\nTIME'S UP!");
  25. //引发SIGINT信号,让程序调用 end_game()显示最后得分。
  26. raise(SIGINT);
  27. }
  28. void error(char* msg) {
  29. fprintf(stderr, "%s: %s\n", msg, strerror(errno));
  30. exit(1);
  31. }
  32. int main() {
  33. catch_signal(SIGALRM, times_up);
  34. catch_signal(SIGINT, end_game);
  35. srandom(time(0)); //确保每次都得到不同的随机数
  36. while (1) {
  37. int a = random() % 11; //a是0~10的随机数
  38. int b = random() % 11;
  39. char txt[4];
  40. // 将闹钟信号设为5秒后触发。
  41. // 只要能在5秒内再次回到到这个地方,定时器就会重置,闹钟信号也不会触发。
  42. alarm(5);
  43. printf("\nWhat is %i times %i? ", a, b);
  44. fgets(txt, 4, stdin);
  45. int answer = atoi(txt);
  46. if (answer == a * b)
  47. score++;
  48. else
  49. printf("\nWrong! Score: %i\n", score);
  50. }
  51. return 0;
  52. }
运行结果:
 
第一次没有回答等了5秒,程序自动结束,第二次按了ctr+c,程序也结束了,都算了总分.
虽然信号有些复杂,但很好用。信号可以让程序从容结束,而间隔定时器可以帮助处理一些超时任务。
 
问: 信号按什么顺序发送,程序就会按什么顺序接收吗?
答: 如果两个信号发送间隔很短就不会,操作系统会先发送它认为更重要的信号。
 
问: 总是如此吗?
答: 取决于你的平台。例如在Cygwin的很多版本中,信号会按发送的顺序接收,但通常不应该做这样的假设。
 
问: 如果一个信号发送了两次, 进程都会接收到吗?
答: 还是要看情况,在Linux和Mac中,如果一个信号在很短的时间里发送了两次,内核只会发送其中的一个;而在Cygwin中两个信号都会发送,但不应该做这样的假设。

猜你喜欢

转载自blog.csdn.net/woshiwangbiao/article/details/53788250