Linux进程信号
信号是一个软件中断。操作系统通过信号告诉进程发生了某个事件,打断进程当前的操作,去处理这个事件。
1.信号的查看
kill -l //查看系统中的信号种类
在Linux操作系统中,一共有62中信号。
1~31号信号:从unix借鉴而来的,每个信号都有具体对应的系统事件------非可靠信号。
34~64号信号:后期扩充的,因为没有具体对应事件,因此命名比较草率------可靠信号。
信号的生命周期:产生->在进程中注册->在进程中注销->处理
2.信号的产生
硬件条件产生信号:
ctrl+c / ctrl+z / ctrl+|
软件条件产生信号:
kill -signum pid//命令
kill杀死一个进程的原理就是向进程发送一个信号,信号有对应的事件,进程放下手头工作去处理这个事件,然而事件的处理结果就是让进程退出。kill默认发送15号信号。
信号产生函数
int main()
{
//kill(getpid(), SIGHUP);//给指定进程发送指定的信号
//raise(SIGINT);//给进程自己发送指定的信号
//abort();//给自己发送SIGABRT信号-通常用于异常通知
alarm(3);//三秒钟之后给进程自己发送SIGALRM信号
while(1)
{
printf("hello world\n");
sleep(1);
}
return 0;
}
3.信号在进程中的注册
怎么让进程知道自己收到了某个信号。pcb->struct sigpending->struct sigset_t。
sigset_t这个结构体中有一个数组成员,这个数组用于实现一个位图。给进程发送一个信号,就会将这个位图中对应位置1,表示当前进程收到了这个信号。位图只能表示是否接收到了这个信号,不能表示收到了多少个这样的信号。
新号的注册不仅会修改位图,还会为信号组织一个sigqueue节点添加到pcb的sigqueue链表中。
1~31号非可靠信号的注册:若信号注册的时候位图为0,则会创建一个sigqueue节点并修改位图为1,但若是位图为1,则什么都不做。
34~64号可靠信号的注册:不管位图当前是多少,都会创建一个节点,添加到链表中,并修改位图为1。
4.信号在进程中的注销
为了保证一个信号不会被多次处理,因此信号是先注销再处理,在pcb中删除当前信号信息。
非可靠信号的注销:因为非可靠信号只会有一个节点,因此删除节点后,位图直接置0。
可靠信号的注销:因为可靠信号有可能多次注册,有多个节点,因此删除节点后,需要判断是否还有相同节点,若没有才会将位图置0。
5.信号的处理
信号表示一个事件的到来,处理事件就是完成功能,在c语言中完成一个功能的最小模块–函数。
其实每一个信号都对应有自己的事件处理函数,信号到来去处理这个事件就是去执行这个处理函数。
信号的处理方式
1.默认处理方式:操作系统中原定义好的每个信号的处理方式。
2.忽略处理方式:处理方式就是忽略,什么都不做。
3.自定义处理方式:自己定义一个事件函数,使用这个函数替换内核中默认的处理函数,信号到来就会调用我们所定义的函数了。
信号处理函数
void sigcb(int signo)
{
printf("signal no:%d\n", signo);
}
int main()
{
//signal(SIGINT, SIG_DFL);//当前信号到来的时候,采用默认处理方式
//signal(SIGINT, SIG_IGN);//当前信号到来的时候,采用忽略处理方式
signal(SIGINT, sigcb);//当前信号到来的时候调用sigcb这个函数,并且通过参数传入触发回调函数的信号值
while(1)
{
printf("hello world\n");
sleep(1);
}
return 0;
}
6.信号的阻塞
并不是不接收信号,信号依然可以注册,只是标识哪些信号暂时不处理。
在pcb中有一个位图,位图叫block位图—阻塞信号集合,这个集合中的信号如果来了则暂时不处理。
void sigcb(int signo)
{
printf("signal no:%d\n", signo);
}
int main()
{
signal(SIGINT, sigcb);
signal(SIGRTMIN+4, sigcb);
sigset_t set;
sigemptyset(&set);//清空集合,防止位置数据造成影响
sigfillset(&set);//向集合中添加所有信号
sigprocmask(SIG_BLOCK, &set, NULL);//阻塞set集合中的所有信号
printf("press enter continue");
getchar();//等待一个回车,不按回车一直卡在这里
sigprocmask(SIG_UNBLOCK, &set, NULL);//解除set集合中的信号阻塞
while(1)
{
sleep(1);
}
return 0;
}
在所有的信号中,有两个信号比较特殊:SIGKILL-9/SIGSTOP-19,这两个信号不可被阻塞,不可被忽略,不可被自定义。
7.SIGCHLD和SIGPIPE信号
子进程退出后会向父进程发送SIGCHLD信号通知父进程。
但是因为SIGCHLD信号默认的处理方式是忽略,因此之前的程序中若不进行进程等待则不知道子进程的退出。
学了信号之后,如果进行进程等待,而且还不想让父进程阻塞,就可以自定义SIGCHLD信号的处理方式。
void sigcb(int signo)
{
int pid;
while((pid = waitpid(-1, NULL, WNOHANG) > 0))
//SIGCHLD信号是一个非可靠信号,如果有多个子进程同时退出,有可能造成信号丢失
//非阻塞循环在一个回调中将所有的僵尸进程全部处理
{
printf("%d\n", pid);
}
}
int main()
{
signal(SIGCHLD, sigcb);
pid_t pid = fork();
if(pid == 0)
{
sleep(5);
exit(66);
}
pid = fork();
if(pid == 0)
{
sleep(10);
exit(66);
}
while(1)
{
printf("忙碌\n");
sleep(1);
}
return 0;
}
在进程间通信中,我们使用管道时,所有读端被关闭,则继续写入和套接字tcp断开连接会触发异常—SIGPIPE信号。也可以通过自定义SIGPIPE信号的处理方式来解决。
8.volatile关键字
用于修饰一个变量,保持变量的内存可见性,(cup在处理的时候每次都重新从内存中获取数据),防止编译器过度优化。
在编译程序的时候,如果使用了代码优化,发现使用某个变量的频率特别高,为了提高效率会将变量的值设置为某个寄存器的值,以后访问数据的时候直接从寄存器访问,减少了内存访问的过程,提高效率。
但是这会造成代码的逻辑混乱,因为读取变量的值时是从内存中读取,但是变量的值设置为寄存器的值,改变这个变量的值,只会让寄存器中的值改变,内存中的值并没有改变。
9.函数的重入
在多个执行流程中,同时进入一个函数运行。
函数可重入:指的是函数重入之后,不会造成数据二义或者逻辑混乱。
函数不可重入:指的是函数重入之后,有可能造成数据二义或逻辑混乱。
函数是否可重入的判断基准:这个函数是否对全局变量进行了非原子的操作,若有则不可重入。
操作的原子性:操作一次性完成,中间不会被打断。
原子操作:操作要么一次完成,要么就不做。
一个函数若根本没有操作全局数据,或者操作全局数据但是操作是原子性的,则是可以重入的。