上篇笔记中提到,信号在到达进程之后,最后由信号处理函数来处理。信号处理函数是可以被子进程继承的。
下面通过一段代码验证:
#include<stdio.h>
#include <signal.h>
#include <unistd.h>
#include <sys/types.h>
void doit(int num){
printf("pid %d recv...%d\n",getpid(),num);
return;
}
int main(void){
//向进程注册信号处理函数
signal(2,doit);
//创建子进程
pid_t pid = fork();
if(pid == -1){
perror("fork");
return -1;
}
if(pid == 0){//子进程执行的程序
}
else{//父进程执行的程序
//wait(0);
}
while(1);
return 0;
}
执行这段代码的时候,从键盘输入Ctrl+c,会发现两个进程都打印了内容(一个中断发给了两个进程,是因为信号是可以针对进程组发送的),但是都没有结束。说明子进程继承了父进程的信号处理函数。
介绍一个暂停程序的函数:
#include <unistd.h>
int pause(void);
功能:等待一个信号的到来
参数:
void
返回值:
只有等到信号,调用信号处理函数以后,才返回-1,errno被设置
当进程执行到这个函数的时候,就会挂起,直到有信号到达该进程,它才会继续向下执行。
通过alarm(2)函数和pause(2)函数,可以实现一个sleep(3)的功能,代码如下:
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
void doit(int num){
return;
}
unsigned int t_sleep (unsigned int seconds){
//设置闹钟
alarm(seconds);
//暂停
pause();
return alarm(0);
}
int main(void){
signal(14,doit);
while(1){
t_sleep(2);
printf("hehe\n");
}
return 0;
}
信号的阻塞和未决信号
当一个信号到达进程之后,先和blocking进行判断,blocking是进程的信号掩码集,在这个集合里的信号会被阻塞不会被进程的信号处理函数处理。pending中存放的是未决信号。无论信号是否被阻塞,都会有未决状态,只是不被阻塞的信号的未决状态极短,基本无法捕捉。而被阻塞的信号,到达进程后,会被存放在未决信号集中。直到进程对该信号的阻塞状态解除,才会调用相应的信号处理函数处理它。如果直到进程结束都没有解除阻塞状态。那么进程永远都不会处理这个信号。
在系统中,blocking和pending被称为信号集类型,这个类型是一个结构体类型,封装了一个长整型,如下:
typedef struct
{
unsigned long int __val[(1024 / (8 * sizeof (unsigned long int)))];
} __sigset_t;
typedef __sigset_t sigset_t;
系统提供了一系列的函数来支持用户,可以自定义信号掩码集(blocking),然后再通过另外的函数将信号掩码集注册给当前的进程。函数如下:
#include <signal.h>
int sigemptyset(sigset_t *set);
功能:将set信号集设置为空,set里不包含任何信号
参数:
set:指定要初始化的信号集
返回值:
success:0
error:-1,errno被设置
int sigfillset(sigset_t *set);
功能:将set信号集合设置为满,set里包含所有的信号
参数:
set:指定要初始化的信号集
返回值:
success:0
error:-1,errno被设置
int sigaddset(sigset_t *set, int signum);
功能:将信号signum,添加到信号集set中
参数:
set:要操作的信号集
signum:要操作的信号编号
返回值:
success:0
error:-1,errno被设置
int sigdelset(sigset_t *set, int signum);
功能:将信号signum,从信号集set里删除
参数:
set:要操作的信号集
signum:要操作的信号编号
返回值:
success:0
error:-1,errno被设置
int sigismember(const sigset_t *set, int signum);
功能:测试信号是否是信号集的一个成员
参数:
set:执行信号集
signum:指定信号
返回值:
1 信号signum是信号集set里的一个成员
0 不是
-1 错误,errno被设置
使用函数sigprocmask(2)将信号集设置成进程的信号掩码集。
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
功能:检测或改变多个阻塞信号(信号集)
参数:
how:
SIG_BLOCK:将set信号集和当前的信号集的并集作为新的信号集
SIG_UNBLOCK:将set信号集里的信号从当前信号集里删除,解除阻塞
SIG_SETMASK:将set信号集设置成当前进程的信号集(掩码)
set:如果是NULL,当前的信号集不改变,并将现在的信号集保存到oldset(如果oldset不为空)。如果不是NULL,根据how来操作。
oldset:如果不是NULL,就用于保存原来的信号集
返回值:
success:0
error:-1 errno
sigpending(2)
#include <signal.h>
int sigpending(sigset_t *set);
功能:检测未决信号
参数:
set:未决信号集存储到set指定的地址空间里
返回值:
success:0
error:-1 errno
通过一段代码来演示上述函数:
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
void doit(int num){
return;
}
unsigned int t_sleep(unsigned int num){
alarm(num);
pause();
return alarm(0);
}
int main(void){
//用于存储信号集
sigset_t set;
sigset_t oldset;
//初始化信号集
int flag = sigemptyset(&set);
if(flag == -1){
perror("sigemptyset");
return -1;
}
//将14号和2号信号加入到信号集
//sigaddset(&set,14);
sigaddset(&set,2);
//将信号集注册给当前进程
sigprocmask(SIG_SETMASK,&set,NULL);
//注册信号处理函数
signal(14,doit);
//保持进程运行
while(1){
//睡眠两秒
t_sleep(2);
//将进程当前的未决信号放在oldset集合中
sigpending(&oldset);
//在进程当前的未决信号集中寻找是否存在信号编号2的信号
int s = sigismember(&oldset,2);
//如果有就打印yes,如果没有就打印no
s ? printf("yes\n") : printf("no\n");
}
return 0;
}
不可靠信号 也称为时时信号
信号在阻塞的时候,向进程多次发送信号,进程解除对信号的阻塞的时候,只处理一次,造成了信号的丢失,这样的信号成为不可靠信号1-31号。
可靠信号
信号在阻塞的时候,向进程多次发送信号,进程解除对信号的阻塞的时候,处理多次,信号没有丢失,这样的信号成为可靠信号34-64号。
一个信号从发生到处理
以2号信号为例。
2号信号可以由键盘输入触发(Ctrl+c)。
按下按键的时候,发生一个硬件中断,这个时候无论进程是在内核态还是在用户态都会切换到内核态。驱动程序会将按键组合解释成2号信号发送给进程,将信号记录到进程的PCB之后,进程切换到用户态,然后查询自己的PCB看看是否有信号,如果有就调用相应的信号处理函数,在信号处理函数结束后,调用sigreturn(2)函数,释放信号处理函数的栈帧,并返回内核态,清楚处理过的信号。再切换到用户态,检查PCB是否含有信号。直到信号处理完毕。
进程间的通讯
首先理一下,之前学习过管道的方式用于进程通讯,这个是进程通讯的原理。市面上有公司(system v ipc)基于原理做了一些优秀的用于进程间通讯的接口。我们直接使用这些安全高效的接口就可以了。
上述公司对于进程间的通讯主要有以下三种方式:
消息队列,共享内存,信号量集
先介绍一个指令,ipcs,可以列出当前系统内的上述三种方式下的内存产物
linxin@ubuntu:~/UC/day10$ ipcs
--------- 消息队列 -----------
键 msqid 拥有者 权限 已用字节数 消息
------------ 共享内存段 --------------
键 shmid 拥有者 权限 字节 连接数 状态
0x00000000 327680 linxin 600 524288 2 目标
0x00000000 294913 linxin 600 16777216 2 目标
0x00000000 393218 linxin 700 16472 2 目标
0x00000000 688131 linxin 700 205128 2 目标
--------- 信号量数组 -----------
键 semid 拥有者 权限 nsems
上述三种方式,是有唯一id的,例如msqid(消息队列)、shmid(共享内存)、semid(信号量集)
由于进程间的通讯使用的内存是在内核态(再讲管道的时候解释过),所以进程的用户态是无法直接操作这些内容,只能通过system call 或者库函数来完成。
当程序想要对其中一种方式的内存进行操作的时候,比如向队列写入数据,从队列读取数据这样的操作的时候,必须要知道这个队列的id。
获取队列的id也需要通过system call。而获取这种id要通过进程的key(键)来获取。
进程的key具有唯一性,key和队列进行绑定。系统提供了以下函数,来创建一个进程的key
获取进程的键值(唯一性)
ftok(3)
#include <sys/types.h>
#include <sys/ipc.h>
key_t ftok(const char *pathname, int proj_id);
功能:将pathname,proj_id的值转换为system v ipc key
参数:
pathname:指定一个有效的路径
proj_id:必须为非0,指定一个整数,使用该整数的低有效8位。
返回值:
success:返回生成的key_t类型的键值
error:-1,errno被设置
如果两个参数的值,完全一样,多次调用ftok的返回值一样。
现在进程已经有唯一的key了,通过key来绑定并访问消息队列
消息队列
通过进程的key,来绑定并返回消息队列的ID,通过以下函数:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgget(key_t key, int msgflg);
功能:获取一个消息队列的id
参数:
key:指定键值,如果没有和key相关的消息队列,就创建消息队列,并返回消息队列的ID,如果有消息队列,仅返回该消息队列的ID
msgflag:
IPC_CREAT,创建
IPC_EXCL,和IPC_CREAT连用的时候,如果已经存在和key值相关的消息队列,函数会失败,errno被设置
mode:指定了消息队列的权限,这个权限和文件的权限一样
返回值:
success:返回消息队列的ID,一个非负的整数
error:-1,errno被设置
到这一步,消息队列才被创建!