进程间的通信
想必大家都知道多进程的模型,可以这样想象,两个进程一直同时运行,而且步调一致,在fork之后,他们分别作不同的工作,也就是分岔了。这也是fork为什么叫fork的原因。事实也正是如此,当我们要用fork时,一般都是需要去做一个不同的工作。
但是如果多个进程之间需要协同处理某个任务时(每个进程间的工作分工都不同,最终却需要所有进程间的处理结果整合),这时就需要进程间的同步和数据交流了。
每个进程的用户地址空间都是独立的,一般而言是不能互相访问的,但内核空间是每个进程都共享的,所以进程之间要通信必须通过内核。
而常用的进程间通信(IPC,Inter-Process Communication)的方法有如下六种:
-
信号(Signal )
-
管道(Pipe)
-
socket
-
信号量(Semaphore)
-
共享内存(Shared Memory)
-
消息队列(Message Queue)
这里先讲信号,关于其它的通信方式,后面再介绍
什么是信号?
信号是一种软件中断,提供了一种处理异步事件的方法,也是进程间通信的唯一一个异步的通信方式。Unix中定义了很多信号,有很多条件可以产生信号,对于这些信号有不同的处理方式。
信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。
信号是进程间通信机制中唯一的异步通信机制,可以在任何时候发送信号给某一进程,一旦有信号产生,我们就有下面这几种,用户进程对信号的处理方式。
-
执行默认操作。Linux 对每种信号都规定了默认操作,例如,下面列表中的 SIGTERM 信号,就是终止进程的意思。Core 的意思是 Core Dump,也即终止进程后,通过 Core Dump 将当前进程的运行状态保存在文件里面,方便程序员事后进行分析问题在哪里。
-
捕捉信号。我们可以为信号定义一个信号处理函数(安装信号)。当信号发生时,我们就执行相应的信号处理函数。
-
忽略信号。当我们不希望处理某些信号的时候,就可以忽略该信号,不做任何处理。
在Linux下我们可以通过 kill -l 来查看所有信号的定义。
如果我们要使用信号,man 7 signal 可以查看信号在什么条件下触发,以及它的默认处理动作是什么,了解了这些才能更好的运用
信号示例介绍
从键盘输入的信号
如果我们的代码内包含了一个死循环的逻辑并运行起来,那么我们通常是直接 ctrl+c 来终止它的,而ctrl+c本质上也是信号的一种。
注意:shell可以同时运行一个前台进程和任意多个后台进程,只有前台进程才能接收到由键盘键入的 ctrl+c 信号。
在运行进程的命令后加上&即可将进程放在后台运行,这时shell不必等待进程就可以接受新的命令,启动新的进程,但是后台进程无法使用ctrl+C结束,但是可以通过kill命令来结束进程。
kill -[信号值]【要终止进程的PID]
例如:kill -9 5678
接下来上实例:
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
int g_stop = 0;
void sig_handle(int signum) //信号执行动作
{
printf("Catch signal [%d]\n", signum);
g_stop = 1;
}
int main(int argc, char *argv[])
{
signal(SIGINT, sig_handle); //安装信号2,ctrl+c 触发
signal(SIGTERM, sig_handle); //安装信号15,kill -15 pid 触发
while(!g_stop)
{
sleep(2);
printf("running...\n");
}
printf("Ending...\n");
}
结果分析:
ctrl+c
running...
running...
^CCatch signal [2]
running...
Ending...
kill -15 pid
... //此处省略
running...
Catch signal [15]
running...
Ending...
了解过SIGINT和SIGTERM这两个信号之后,我们知道这两个信号的默认动作都是直接结束进程的,注意是直接结束。
但是经过我们信号的安装和程序的设计,更改了它们默认的执行动作。并发现当程序收到这两个信号之后,并没有马上退出,而是去执行我们为它安排的”任务“,像上面这样的程序设计,我们发现可以让程序”优雅”的退出,即它自己一步一步执行到程序的结束,只不过是由“他人”来通知它结束的。并没有给它立马宣判死刑
注意:在这里,我们必须要知道有两个信号(SIGKILL、SIGSTOP)是不能被应用进程捕捉到的,它们两个也不能被忽略和阻塞。这是什么意思呢?也就是说,进程在任何时候收到这两个信号,就会立马终止(SIGKILL)或停止(SIGSTOP)进程的运行。
进程间信号是如何通信的?
在了解进程间信号的通信原理之前,我们必须先要了解一个函数,那就是kill()函数
kill()是一个计算机编程语言函数,Kill函数可以对进程发送signal();在linux里使用的Kill命令,实际上是对Kill()函数的一个包装。
函数原型:
int kill(pid_t pid, int sig);
函数功能:
给指定进程(pid)发送指定信号(sig)
tip:上面 kill -l 显示的信号中,有两个是用户自定义信号,一般使用这两个SIGUSR1(10)、SIGUSR2(12)信号
返回值
成功返回 0; 否则,返回 -1
进程间信号的简单通信
我们知道,父进程在创建子进程之后,究竟是父进程还是子进程先运行没有规定,这由操作系统的进程调度策略决定,而如果在某些情况下我们需要确保父子进程运行的先后顺序,则可以使用信号来实现进程间的同步。
下面是一个父子进程之间使用信号进行同步的例程。在下面的这个程序中,如果父进程先执行则进入到循环休眠等待状态,直到子进程给他发送信号之后才能跳出循环继续运行,这就可以确保子进程先执行它的任务。同样子进程在执行完成任务之后,就等待父进程给他发送信号之后才能退出,而父进程则通过调用wait()系统调用等待子进程退出后,父进程再退出。
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <signal.h>
int g_child_stop = 0;
int g_parent_run = 0;
void sig_child(int signum)
{
if(SIGUSR1 == signum)
{
g_child_stop = 1;
}
}
void sig_parent(int signum)
{
if(SIGUSR2 == signum)
{
g_parent_run = 1;
}
}
int main(int argc, char *argv[])
{
int pid;
int w_status;
signal(SIGUSR1, sig_child); //安装信号
signal(SIGUSR2, sig_parent);
pid = fork();
if(pid < 0)
{
printf("fork() failure: %s\n", strerror(errno));
return -1;
}
else if(0 == pid) //子进程
{
printf("child process start running and send a signal...\n");
kill(getppid(), SIGUSR2); //获取父进程的PID,并给其发信号SIGUSR2,通知其可以开始运行了
while(!g_child_stop) //等待父进程给子进程发送信号
{
sleep(1);
}
printf("child process recieve signal and finish running...yeah\n");
}
else //父进程
{
printf("parent process run, but not work...\n");
while(!g_parent_run) //等待子进程发送信号
{
sleep(1);
}
printf("parent process start working ^_^\n");
sleep(2);
printf("parent process still working and send a signal...\n");
kill(pid, SIGUSR1); //给子进程发送信号SIGUSR1
wait(&w_status);
printf("parent wait child process die die die\n");
}
return 0;
}
运行结果如图所示:
parent process run, but not work...
child process start running and send a signal...
parent process start working ^_^
parent process still working and send a signal...
child process recieve signal and finish running...yeah
parent wait child process die die die