1. 进程间通讯
各进程间在虚拟内存空间上是独立的,以32位的操作系统为例,虚拟空间是4G。程序开发过程中,创建进程一般都会涉及到进程间的通讯。前面学的,子进程可以通过exit()返回错误码(0-255)返回到父进程,父进程用wait()来接收。exit()返回错误码存在局限性,通讯不仅要是数据交互,还可以互相影响,如杀死进程命令”kill -9 进程PID”,kill是发信号之意,即给进程发送9信号。
进程间的4G虚拟空间是独立的,但是开头1G内核空间是共用。借助内核空间提供的API,就可以实现进程间通讯。进程间通讯方法(IPC)有6个:
(1) 信号
(2) 管道
(3) 信号量
(4) 共享内存
(5) 消息队列
(6) 套接字(socket)
2. 信号
常见的kill命令,以及子进程终止时,父进程可以通过wait()函数获知,都是信号机制。信号也称为异步通知(异步就是各干各的,需要通讯时候再交互),信号是通过软件模拟硬件的中断机制。类比于硬件中断机制,中断源 = 信号源,信号源来源于操作系统或者手动发生、程序发送。
kill -l 可以看到操作系统为我们提供的信号源:
34-64属于我们不可用部分。几个比较常用的信号源有
1) SIGHUP: 挂起进程
2) SIGINT: 中断进程(ctrl + c),当我们对正在运行的程序键入(ctrl + c),实质就是通过bash向前台进程组发送2号信号SIGINT。若ctrl + c不管用,说明该进程并不捕捉SIGINT
3) SIGQUIT: 让进程退出
4) SIGILL: 指令异常
5) SIGUSR1和SIGUSR2: 属于用户自定义信号
6) SIGSEGV: 段错误(内核监测到某进程访问都非法地址,就会给该进程发送SIGSEGV)
7) SIGCHLD: 子进程退出信号,父进程用wait()接收
信号源及其对应的操作函数在内核的实现可以理解为:
信号源的对应操作有: 默认操作(放到后台停止或者结束本进程)、忽略操作、自定义操作。说白了,这些操作都是函数,都是作为上述函数指针数组的成员。要想改变信号源的对应的操作函数,无非就是去设置这个函数指针数组,调用的API是signal(),原型如下:
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
参数二是sighandler_t类型的handler,它是一个函数指针,取值可以为自定义的符合类型的函数,也可以为SIG_DFL(默认操作)、SIG_IGN(忽略操作)。注意函数的返回值,执行成功返回上次的handler值,失败则返回SIG_ERR。执行成功返回上次的handler值,方便我们对于临时修改信号处理函数的场景进行恢复,因为信号源是属于进程片空间的,一旦修改,所在进程片空间的程序若使用到该信号源其操作函数都会修改。
signal()函数的使用十分简单:
void signal_handle(int sig)
{
printf("signal_handle, pid = %d, sig = %d\n", getpid(), sig);
}
int main(void)
{
signal(SIGINT, signal_handle);
printf("main, pid = %d\n", getpid());
getchar();
return 0;
}
这样,对该进程发送SIGINT信号或者在程序运行终端键入ctrl+c,都能够执行signal_handle()函数的打印内容。
关于信号源及其对应操作,需要注意的还有:
(1) signal返回上一次注册函数的地址(若临时修改处理函数,可以通过此功能恢复原先处理函数)
(2) 同一个信号源不可以注册多次,否则以最后一次为准(函数指针数组成员赋值,肯定如此)
(3) 执行信号处理函数过程中会忽略此信号源, 但会记录此信号是否来过(处理函数若占用时间太长,此时又有多个同一信号触发,那么该进程只会记录一次,处理函数退出后再执行一次)
(4) 执行信号处理函数不会忽略其它信号
(5) 子进程复制父进程信号操作情况
(6) 子进程执行execX家族函数后,父进程自定义信号捕捉不会继承, SIG_IGN和SIG_DFL会继承(execX函数家族都会覆盖原先进程片空间,那么自定义函数肯定会被覆盖)
3. pause()函数
pause()函数的功能是等待信号来临,没有信号时将发生阻塞。