c 进程间的通信
在上篇讲解了如何创建和调用进程
c 进程和系统调用
这篇文章就专门讲讲进程通信的问题
先来看一段下边的代码,这段代码的作用是根据关键字调用一个Python程序来检索RSS源,然后打开那个URL
1 #include <stdio.h> 2 #include <unistd.h> 3 #include <stdlib.h> 4 #include <errno.h> 5 #include <string.h> 6 7 void error(char *msg) { 8 fprintf(stderr, "Error: %s %s", msg, strerror(errno)); 9 exit(1); 10 } 11 12 void open_url(char *url) { 13 char launch[255]; 14 // windows 15 sprintf(launch, "cmd /c start %s",url); 16 system(launch); 17 // linux 18 sprintf(launch, "x-www-browser ‘%s’ &",url); 19 system(launch); 20 // mac 21 sprintf(launch, "open '%s'",url); 22 system(launch); 23 } 24 int main(int argc, char * argv[]) { 25 26 // 取出要搜索的参数 27 char *phrase = argv[1]; 28 // 设置RSS源 29 char *vars[] = {"RSS_FEED=http://news.baidu.com/n?cmd=1&class=civilnews&tn=rss&sub=0",NULL}; 30 31 // 开启一条管道,用于连接下边的父进程和子进程 32 int fd[2]; 33 if (pipe(fd) == -1) { 34 error("Can not create a pipe"); 35 } 36 37 // 开启一个进程 38 pid_t pid = fork(); 39 if (pid == -1) { 40 error("Can not fork process"); 41 } 42 43 44 // 接下来让子进程去源内查找结果 pid==0 为子进程 45 if (pid == 0) { 46 47 // 因为子进程不需要使用管道的输出端,关闭它 48 close(fd[0]); 49 // 把标准输出设置为管道的输入端,之所以这么设置,是因为子集成当查询到数据的时候就会调用标准输出函数,然后把数据流入管道中 50 dup2(fd[1], 1); 51 52 if (execle("/usr/bin/python", "/usr/bin/python","./rssgossip.py","-u",phrase,NULL,vars) == -1) { 53 error("Can't run script"); 54 } 55 } 56 57 // 上边的if中的代码是子进程的代码,下边的代码是父进程的 58 // 父进程中不使用管道的写入端,关闭 59 close(fd[1]); 60 // 把标准输入 定向为管道的读取端 61 dup2(fd[0], 0); 62 63 char line[255]; 64 65 // 以\t 开头的就是url 66 while (fgets(line, 255, stdin)) { 67 if (line[0] == '\t') { 68 open_url(line+1); 69 FILE *file = fopen("te.txt", "w"); 70 fclose(file); 71 } 72 } 73 74 75 return 0; 76 }
我们先看看进程内部是什么样子的
进程含有它内部运行的程序,还有栈和堆的数据空间。除此之外,它还要记录数据流的连向,比如标准输出连接到哪里。进程用文件描述符来表示数据流,所谓的描述符其实就是一个数字,进程会把文件描述符和对应的数据流保存在描述符表中,就像下边的这张图一样
文件描述表的一列是文件描述符号,另外一列是他们对应的数据流。虽然名字叫文件描述符,但他们不一定连接硬盘上的某个文件,也有可能连接键盘,屏幕,文件指针,网络等等。
描述符表的前三项万年不变:0代表标准输入 ,1代表标准输出 ,2代表标准错误,其他项要么为空,要么连接进程打开的数据流。比如程序在打开文件进行读写的时候,就会打开一项。
每当创建进程后,默认的0(标准输入)指向键盘,1(标准输出)和2(标准错误)指向屏幕。
那么问题来了,前三项不是万年不变的吗?那么我应该怎么控制数据流呢?
其实,0/1/2在描述符表中的位置虽然是不可变的,但是他们指向的数据流确实可以改变的。
举个简单的例子,我想打开一个文件,进程是怎么做呢?
首先我们先打开一个文件
FILE *file = fopen("quitar.mp3", "r");
当系统执行完上边这行代码的时候,系统会打开quitar.mp3这个文件,并且会返回一个指向这个文件的指针,系统还会表里描述符表的空项,并把新文件注册在其中
但是当我们做了很多操作,描述符表中有很多项的时候,我们如何才能找到我们想要的那一项呢 ?
答案是fileno() 函数,成功会返回文件指针的文件描述符,错误了不会返回-1,只要你把打开文件的指针传给了它,就一定会返回描述符。
int desc = fileno(file);
我们现在已经能够通过fileno()函数拿到描述符了,那我们应该如何修改该项的数据流呢?
答案是dup2() 函数。他会复制当前描述符的数据流到制定的描述符的数据流。
dup2(4,3);
好了,我们已经知道如何改变数据流了,完全可以把标准输出指向一个文件,然后把数据写入这个文件中。
只知道这些还不够,有点开发经验的人都知道进程和进程之间执行任务所需要的时间是不一样的,往往需要等待一个进程完成后再进行下边的任务,比如父进程和子进程,父进程需要等待子进程完成后再继续,这个该怎么办呢?
答案就是waitpid()函数。它会等待子进程结束后才返回。
现在终于说到重点内容了,进程之间传递数据靠的就是管道,在进程之间创建一条管道,
比如我们要把子进程的数据传给父进程。
使用pipe()函数打开两条数据流。
因为子进程需要把数据发送到父进程,所以要用管道连接子进程的标准输出和父进程的标准输入。你将用pipe()函数建立管道,还记得吗?我们说过,每当打开数据流的时候,它都会加入描述符表中,pipe()函数也是如此,它创建两条相连的数据流,并把他们加入到表中,然后你只需要往其中一条数据流中写数据,就能从另一条数据流中读取。
pipe()在描述符表中创建这两项时,会把他们的文件描述符保存在一个包含两个元素的数组中:
好了,本片文章中开头给出的代码所使用到的知识点都已经讲解清楚了,原理大概就是这样的,
我们已经知道了进程是怎么一回事?知道了如何在进程中利用管道做一些事情了,然而,这些还不够,虽然我们知道了如何创建进程,配置环境,进程间通信,但是进程是怎么结束的呢?
我们带着这个小小的疑问继续往下看
加入我们写了一个从键盘数据的小程序,像这样
1 #include <stdio.h> 2 int main () { 3 4 char name[30]; 5 printf("请输入一个名字:"); 6 fgets(name, 30, stdin); 7 printf("你输入的名字是: %s",name); 8 return 0; 9 }
当我们按下Ctrl+C 的时候程序就结束了,也就是进程就结束了。但这其中到底发生了什么呢?
printf("你输入的名字是: %s",name);
printf并没有调用,是fgets调用了exit()吗?
其实这涉及到了操作系统是如何控制程序的问题
当调用fgets函数时,操作系统会从键盘读取数据,当它发现用户按了Ctrl-C 后就会向程序发送中断信号,就像这样:
信号是一个短消息,也就是一个整型。当信号到来时,进程必须停止手中一切工作来处理信号,进程会查看信号映射表,表中每一个信号都对应着一个信号处理函数,中断信号的默认处理函数就是exit()函数。
那么系统为什么不直接结束程序呢?而是在信号表中查找处理函数?就是为了让我们可以自定义信号处理函数。
使用sigaction函数
sigaction函数是一个函数包装器,本质上是一个结构体
/* * Signal vector "template" used in sigaction call. */ struct sigaction { union __sigaction_u __sigaction_u; /* signal handler */ sigset_t sa_mask; /* signal mask to apply */ int sa_flags; /* see signal options below */ };
它有一个函数指针 __sigaction_u
/* union for signal handlers */ union __sigaction_u { void (*__sa_handler)(int); void (*__sa_sigaction)(int, struct __siginfo *, void *); };
但是在平时开发中一般这么使用
/* if SA_SIGINFO is set, sa_sigaction is to be used instead of sa_handler. */ #define sa_handler __sigaction_u.__sa_handler #define sa_sigaction __sigaction_u.__sa_sigaction
sigaction告诉操作系统收到信号时应该调用哪个函数,加入我们想在收到中断信号的时候调用我们自定义的my_custom_fun(),就要把我们自定义的这个函数包装成sigaction。
// 创建一个新动作 struct sigaction action; // 想让计算机调用哪个函数,这个被包装的my_custom_fun函数就叫做处理器 action.sa_handler = my_custom_fun; // 使用掩码过滤信号,通常会用一个空的掩码 sigemptyset(&action.sa_mask); // 一些附加的标志位,置为0就行了 action.sa_flags = 0;
当然这个被包装的函数也需要以特定的方式创建,这个函数我们下边就称之为处理器了,处理器必须接受信号参数,信号是一个整形值,如果你自定义一个信号处理函数,就需要接受一个整型参数,像这样:
void my_custom_fun(int sig) { exit(1); }
由于我们以参数的形式传递信号,所以多个信号可以共用一个处理器,也可以每个信号写一个处理器。
要点: 处理器的代码应该短而快,刚好能处理接受到的信号就好。
那么系统怎么知道我们偷偷的更换了处理器呢?
因此要使用sigaction() 函数来注册sigaction,让系统知道它的存在
sigaction(signal_no, &new_action, &old_action);
我们来看看这个函数的样子:
int sigaction(int, const struct sigaction * __restrict, struct sigaction * __restrict);
它几首3个参数:
1. 信号编号,这个整型值代表了你希望处理的信号,通常会传递类似SIGINT/SIGQUIT这样的标准信号。
2. 新动作,你想注册的新sigaction的地址
3. 旧动作, 如果你想保存被替换的信号处理器,可以再传一个sigaction指针,如果不想保存,可以传一个NULL。
注意:如果sigaction() 的函数失败,会返回-1,并设置errno变量。
当然,为了能够快速的使用这些功能,我们可以把创建这个sigaction的过程封装起来,我们只需要告诉函数需要捕捉的信号是什么,设置的处理器是什么就够了。
我们写了下边的函数:
int catch_signal(int sig, void (*handler)(int)) { // 创建一个新动作 struct sigaction action; // 想让计算机调用哪个函数,这个被包装的my_custom_fun函数就叫做处理器 action.sa_handler = handler; // 使用掩码过滤信号,通常会用一个空的掩码 sigemptyset(&action.sa_mask); // 一些附加的标志位,置为0就行了 action.sa_flags = 0; return sigaction(sig, &action, NULL); }
我们先用一个简单的例子来演示下上边讲到的sigaction,代码如下
1 #include <stdio.h> 2 #include <signal.h> 3 #include <stdlib.h> 4 5 int catch_signal(int sig, void (*handler)(int)) { 6 // 创建一个新动作 7 struct sigaction action; 8 // 想让计算机调用哪个函数,这个被包装的my_custom_fun函数就叫做处理器 9 action.sa_handler = handler; 10 // 使用掩码过滤信号,通常会用一个空的掩码 11 sigemptyset(&action.sa_mask); 12 // 一些附加的标志位,置为0就行了 13 action.sa_flags = 0; 14 15 return sigaction(sig, &action, NULL); 16 } 17 18 void my_custom_fun(int sig) { 19 printf("一切都结束了"); 20 exit(1); 21 } 22 23 int main () { 24 25 if (catch_signal(SIGINT, my_custom_fun) == -1) { 26 fprintf(stderr, "替换不成功"); 27 exit(2); 28 } 29 30 char name[30]; 31 printf("请输入一个名字:"); 32 fgets(name, 30, stdin); 33 printf("你输入的名字是: %s",name); 34 return 0; 35 }
运行程序,当我Ctrl-C 的时候,结果如下
请输入一个名字:^C一切都结束了bogon:02- machao$
SIGINT | 进程被中断 |
SIGQUIT | 有人要求停止进程,并把存储器中的内容保存到核心转存文件中 |
SIGFPE | 浮点错误 |
SIGTRAP | 调试人员询问进程执行到了哪里 |
SIGSEGV | 进程试图访问非法存储器地址 |
SIGWINCH | 终端窗口的大小发生变化 |
SIGTERM | 有人要求系统的内核终止进程 |
SIGPIPE | 进程在向一个没有人读的管道写数据 |
那么问题又来了,我们应该如何发送系统信号呢?
最后我们使用一个稍微复杂点的例子来掩饰上边讲的内容
说明:
SIGALRM 是一个定时器信号,使用alarm()函数可以设置一个定时器,设置一个时间,当时间结束的时候,会发出SIGALRM信号,如果在定时器时间还未结束的情况下,再次调用了alarm()函数,定时器将重新计时
这个程序测试用户的数学水平,要求用户做乘法,程序的条件如下:
1.用户Ctrl-C
2.回答时间超过5秒
程序在结束时会显示总得分,并把退出状态设为0
代码如下
1 #include <stdio.h> 2 #include <signal.h> 3 #include <stdlib.h> 4 #include <string.h> 5 #include <errno.h> 6 #include <time.h> 7 #include <unistd.h> 8 9 int score = 0; 10 11 int catch_signal(int sig, void (*handler)(int)) { 12 // 创建一个新动作 13 struct sigaction action; 14 // 想让计算机调用哪个函数,这个被包装的my_custom_fun函数就叫做处理器 15 action.sa_handler = handler; 16 // 使用掩码过滤信号,通常会用一个空的掩码 17 sigemptyset(&action.sa_mask); 18 // 一些附加的标志位,置为0就行了 19 action.sa_flags = 0; 20 21 return sigaction(sig, &action, NULL); 22 } 23 24 // 结束游戏的处理器 25 void end_game(int sig) { 26 printf("\n 总得分: %i \n", score); 27 exit(0); 28 } 29 30 // 时间到了的处理器 31 void times_up(int sig) { 32 printf("\n 时间到了"); 33 // 当倒计时结束的时候,引发SIGINT信号,调用end_game函数 34 raise(SIGINT); 35 } 36 37 void error(char *msg) { 38 fprintf(stderr, "Error: %s %s", msg, strerror(errno)); 39 exit(1); 40 } 41 42 int main () { 43 44 // 中断信号 45 if (catch_signal(SIGINT, end_game) == -1) { 46 fprintf(stderr, "结束不成功"); 47 exit(2); 48 } 49 // 定时信号 50 if (catch_signal(SIGALRM, times_up) == -1) { 51 fprintf(stderr, "闹钟不成功"); 52 exit(3); 53 } 54 // srandom函数利用一个时间因子产生一个不同的队列给random函数调用,这样random函数每次运行时就不会产生一样的伪随机输出了 55 srandom (time (NULL)); 56 57 while (1) { 58 59 // 生成两个 0 ~ 10 的随机数 60 int a = random() % 11; 61 int b = random() % 11; 62 63 char txt[4]; 64 65 // 定时5秒 66 alarm(5); 67 68 printf("%i * %i = ?",a,b); 69 70 fgets(txt, 4, stdin); 71 72 int answer = atoi(txt); 73 74 if (answer == a * b) { 75 score++; 76 }else { 77 printf("错误!得分: %i \n",score); 78 } 79 } 80 81 return 0; 82 }
首先我们回答两个问题,然后等待5秒倒计时结束 , 再次运行程序,我们Ctrl-C
运行结果为
好了,关于进程间的通信的内容,和进程的操作就写到这里了