Fork me on GitHub

IPC——信号

延伸阅读:Linux命令——trap    Linux命令——killall 、kill 、pkill、xkill

什么是信号

  信号是一种通知进程某件事情发生了的一种通信机制,通过向进程发送某个信号,可以告诉进程发生了什么事情,进程收到这个信号后,就知道某事情发生了,进程可以做出相应的响应(处理)。与IPC中其他进程通信方式不同的是,信号属于不精确通信,信号只能告诉进程大概发生了什么事情,但是不能准确的告诉进程详细的细节信息。Linux下边定义了很多的信号,所有的信号都是一个整数编号,不过为了好辨识,Linux系统给这些整数编号都定义了对应的宏名,宏名都是以SIG开头,比如SIGABRT。

谁会向进程发信号

总结起来,会有三个“人”会向进程发送信号,分别是“另一进程”、“OS内核”、“硬件”。

另一个进程发送信号

eg:在命令行终端窗口通过kill命令向某个进程发送一个信号将其终止。

kill   PID

内核发送信号

发生了某个事件,Linux内核可能会发送该事件对应的信号给某个进程

eg管道通信中,当所有读文件描述符被关闭,进程会被内核发送一个SIGPIPE信号,提示读管道出错了。

这个过程再详细一点?

计算机内部所有硬件连接在总线上。所有部件按照仲裁总线 或 中断总线上给出的信号来判断这个时刻总线可以由哪个部件来使用。产生仲裁总线 或 者中断点位的可以是CPU,也可以是总线上的其他设备。如果CPU要向某个设备做输出操作,那么就由CPU主动做中断。如果某个设备请求向CPU发信号,则由这个设备主动产生中断信号来通知CPU 。CPU运行操作系统内核的设备管理程序,从而产生了这些信号。

底层硬件发送信号

底层硬件发生了某个事件,会向进程发送对应的某个信号。

eg:按下ctrl+c按键终止进程时,内核收到ctrl+c按键后,会向正在运行的进程发送SIGINT信号,将其异常终止。

 注意:不管进程是被哪一个信号给终止了,只要是被信号终止的,都是异常终止。

一般来说,大多数发送信号的原因,都是因为内核、硬件发生了某些事件时,才会向某个进程发送该事件专用的信号,告诉该进程这个事件发生了。对于我们自己写的进程来说,其实更多是接收信号,而不是发送信号。

 

收到信号后进程如何应对

忽略

“鸵鸟策略”的做法,进程就当信号从来没有发生过。

捕获

进程会调用相应的处理函数,进行相应的处理。

默认

如果不忽略也不捕获的话,此时进程会使用系统设置的默认处理方式来处理信号。

 

Linux系统有哪些信号

查看Linux系统下有哪些信号

[root@localhost ~]# kill -l
 1) SIGHUP     2) SIGINT     3) SIGQUIT     4) SIGILL     5) SIGTRAP
 6) SIGABRT     7) SIGBUS     8) SIGFPE     9) SIGKILL    10) SIGUSR1
11) SIGSEGV    12) SIGUSR2    13) SIGPIPE    14) SIGALRM    15) SIGTERM
16) SIGSTKFLT    17) SIGCHLD    18) SIGCONT    19) SIGSTOP    20) SIGTSTP
21) SIGTTIN    22) SIGTTOU    23) SIGURG    24) SIGXCPU    25) SIGXFSZ
26) SIGVTALRM    27) SIGPROF    28) SIGWINCH    29) SIGIO    30) SIGPWR
31) SIGSYS    34) SIGRTMIN    35) SIGRTMIN+1    36) SIGRTMIN+2    37) SIGRTMIN+3
38) SIGRTMIN+4    39) SIGRTMIN+5    40) SIGRTMIN+6    41) SIGRTMIN+7    42) SIGRTMIN+8
43) SIGRTMIN+9    44) SIGRTMIN+10    45) SIGRTMIN+11    46) SIGRTMIN+12    47) SIGRTMIN+13
48) SIGRTMIN+14    49) SIGRTMIN+15    50) SIGRTMAX-14    51) SIGRTMAX-13    52) SIGRTMAX-12
53) SIGRTMAX-11    54) SIGRTMAX-10    55) SIGRTMAX-9    56) SIGRTMAX-8    57) SIGRTMAX-7
58) SIGRTMAX-6    59) SIGRTMAX-5    60) SIGRTMAX-4    61) SIGRTMAX-3    62) SIGRTMAX-2
63) SIGRTMAX-1    64) SIGRTMAX  

总共62个信号,也就是说每个进程可以接收的信号种类有62种,1~64为信号的编号,SIG***为信号的宏名。每个信号代表着某种事件,一般情况下,当进程收到某个信号时,就表示该信号所代表的事件发生了。

1~34:也不是所有的信号都要掌握,我们只需要关心其中常用的信号。

35~64:这些信号是Linux后期增设的信号,这些个信号不需要关心,所以不用了解。

常用信号

 

常见由用户发出终止进程的信号

Ctrl + C 发送SIGINT

Ctrl + \ 发送SIGQUIT

kill pid 发送SIGTERM

只有当进程有占用命令行终端时,才能Ctrl + C、Ctrl + \ 来终止。当无法使用Ctrl + C、Ctrl + \ 来终止进程时,往往就使用kill命令来终止进程。

 

kill 和 pkill

Linux命令——killall 、kill 、pkill、xkill

kill这个命令名字很吓人。其实kill只是发送信号,至于进程会不会被终止,这就看信号的处理方式,处理方式如果是终止,那么就会终止进程。如果把kill起名为send估计更好理解些,因为kill所起到的作用只是发送信号。

发送信号的完整格式:kill  -信号编号  PID

信号编号写数字和宏名都可以。如果不写明信号编号的话:kill PID,默认发送的是15(SIGTERM)信号,等价于kill -SIGTERM PID或者kill -15 PID。只有发送15这个信号时才能省略信号编号,发送其它信号时必须写明信号编号。

 

pkill用法与kill差不多,只不过kill是按照PID来识别进程的,pkill是按照名字来识别进程的。

发送信号的完整格式:pkill -信号编号 名字

同样,如果不写明信号编号的话,默认发送的是15(SIGTERM)这个信号。

 

core文件

  用于保存程序(进程)在当前结束的这一刻,进程在内存中的代码和数据,core文件可以用于分析进程在结束时的状况,不过由于进程代码和数据都是二进制的,所以把core文件直接打开后我们是看不懂的,一般需要特殊软件翻译后才能看懂。并不是所有的信号在终止进程时都会产生core文件,只有某个些信号在终止进程时才会产生core文件,不过一般情况下并不会创建这个文件,因为系统默认将产生core的设置给关闭了,只有打开后这个设置后才会保存core文件。所以当你看到提示core dumped,这就表示这个信号终止进程时,会产生core文件,只不过由于关闭了设置,因此core文件被丢弃了,dumped就是丢弃的意思。

[root@localhost ~]# ./a.out 
^\Quit (core dumped)

 

Linux信号处理API

signal函数

函数原型

#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);

功能

设置某个信号的处理方式。处理方式可以被设置为忽略,捕获,默认。

进程的进程表(task_struct)中会有一个“信号处理方式登记表”,专门用于记录信号的处理方式,调用signal函数设置某个信号的处理方式时,会将信号的处理方式登记到该表中。每个进程拥有独立的task_struct结构体变量,因而每个进程的“信号处理方式登记表”都是独立的,所以每个进程对信号的处理方式自然也是独立的,互不干扰。

参数

signum:

信号编号。handler个函数指针类型变量,函数指针类型是void (*)(int)

handler:

可以取值

#define SIG_DFL    ((void (*)(int))0)    
#define SIG_IGN    ((void (*)(int))1)
#define SIG_ERR    ((void (*)(int))-1) 

这几个宏定义在了<signal.h>头文件中。

①忽略:SIG_IGN

除了SIGKILL(无条件终止一个进程)这个信号外,其它所有的信号都可被忽略和捕获。

②默认:SIG_DFL

③捕获:填写类型为void (*)(int)的捕获函数的地址,当信号发生时,会自动调用捕获函数来进行相应的处理。当然这个捕获函数需要我们自己来实现,捕获函数的int参数,用于接收信号编号。捕获函数也被称为信号处理函数

void signal_fun1(int signo){...}                     
void signal_fun2(int signo){...}
                
int main(void)
{
    signal(SIGINT, signal_fun1);
    signal(SIGSEGV, signal_fun2);                        
    return 0;
}

信号捕获函数调用时机:

进程接收到信号时就调用,调用时会中断进程的正常运行,当调用完毕后再会返回进程的正常运行。

更正前面的一个说法

在一开始提到:不管进程是被哪一个信号给终止了,只要是被信号终止的,都是异常终止。

这种表达方式不严谨,应该说如果是在信号处理函数里面调用exit、_exit来终止进程的这种方式的话,准确来讲不应该算是“”信号异常终止进程”的情况。信号异常终止进程,准确来讲指的是信号默认的终止方式,这种情况才是“异常终止”。

返回值

成功:返回上一次的处理方式

失败:返回宏值SIG_ERR,并且设置errno。

 

kill

函数原型

#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);

功能

向PID所指向的进程发送指定的信号。

返回值

成功返回0,失败返回-1,errno被设置。

 raise

函数原型

#include <signal.h>
int raise(int sig);

功能

向当前进程发送指定信号。

返回值

成功返回0,失败返回非0。

 

alarm

alarm函数不会阻塞

函数原型

#include <unistd.h>
unsigned int alarm(unsigned int seconds);

功能

设置一个定时时间,当所设置的时间到后,内核会向调用alarm的进程发送SIGALRM信号。SIGALRM的默认处理方式是终止。

测试代码

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include<signal.h>
int main(int argc,char**argv,char**environ)
{
    alarm(5);
    while(1);
    return 0;
}

返回值

返回上一次调用alarm时所设置时间的剩余值。如果之前没有调用过alarm,又或者之前调用alarm所设置的时间早就到了,那么返回的剩余值就是0。

 

pause

函数原型

#include <unistd.h>
int pause(void);

功能

调用该函数的进程会永久挂起(阻塞或者休眠),直至被信号(任意一个信号)唤醒为止。

返回值

只要一直处于休眠状态,表示pause函数一直是调用成功的。当被信号唤醒后会返回-1,表示失败了,errno的错误号被设置EINTR(表示函数被信号中断)。

 

sleep

函数原型

#include <unistd.h>
unsigned int sleep(unsigned int seconds);

功能

调用该函数的进程会休眠若干seconds,休眠期间可以被信号唤醒

返回值 

如果sleep时间到了,返回0。如果sleep期间被信号中断了,返回剩余秒数。

 

abort

函数原型

#include <stdlib.h>
void abort(void);

功能

向当前进程发一个SIGABRT信号,这个信号的默认处理方式是终止,因此如果不忽略和捕获的话,会将当前进程终止掉。abort相当于raise的特例,只发送SIGABRT信号自当前进程。这个函数有个绰号:自杀函数

返回值

 

进程的休眠与唤醒

调用sleep、pause等函数时,这些函数会使进程进入休眠状态。借助信号机制可以实现唤醒功能 ,这需要我们创建空捕获函数(给信号登记一个空捕获函数,函数内容一般都是空的)。其唤醒过程如下:

当信号发送给进程后,会中断当前休眠的函数,然后去执行捕获函数,捕获函数执行完毕返回后,不再调用休眠函数,而是执行休眠函数之后的代码,这样函数就被唤醒了。

如果唤醒后想继续休眠怎么办?

借助goto语句形成循环

学C的时候不是很反对用goto吗?  

pause休眠唤醒后继续休眠

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>
#include <errno.h>

void signal_fun(int signo)
{
    printf("!!!!!!!\n");
}

int main(int argc, char **argv, char **environ)
{
    int ret = 0;    
    signal(SIGINT, signal_fun);

lable:    ret = pause();
    if(ret == -1 && errno == EINTR)
    {
        goto lable;
    }

    char buf[100] = {0};
    read(0, buf, sizeof(buf));
    printf("hello\n");
    while(1);
    return 0;
}
View Code

sleep休眠唤醒后继续休眠

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>
#include <errno.h>

void signal_fun(int signo)
{
    printf("!!!!!!!\n");
}

int main(int argc, char **argv, char **environ)
{
    int ret = 0;    
    signal(SIGINT, signal_fun);

    ret = 10;
lable1:    ret =sleep(ret);
    if(ret != 0)
    {
        printf("ret = %d\n", ret);
        goto lable1;
    }

    char buf[100] = {0};
    read(0, buf, sizeof(buf));
    printf("hello\n");
    while(1);
    return 0;
}
View Code

pause、sleep导致的休眠,唤醒后想再次休眠需要手动重启,所有函数(会导致休眠的)都需要手动重启吗?

并不是,对于绝大多数休眠函数来说,被信号中断后,如果你想继续休眠的话,需要自己去手动重启,否则就会继续向后运行。这里说一个比较特殊的函数read

read

函数原型 

#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count); 

功能

从fd指向的文件中,将数据读到应用缓存buf中

参数

fd:指向打开的文件

buf:读取到数据后,用于存放数据的应用缓存的起始地址

count:缓存大小(字节数)

返回值

成功:返回读取到的字符的个数

失败:返回-1,并自动将错误号设置给errno。

 

 

信号的发送、接收和处理的过程

信号屏蔽字

作用

用来屏蔽信号,有点像公司前台,信号来了先问前台(屏蔽字),我能被立即处理不,能就立即处理,不能处理就去未处理集呆着

每个进程能够接收的信号有62种,信号屏蔽字的每一位记录了每个信号是被屏蔽的还是被打开的。

如果是打开的就立即处理。

如果是屏蔽的就暂不处理

信号屏蔽字放在那里

每一个进程都有一个信号屏蔽字,它被放在了进程表(task_struct结构体变量)中。

如何修改信号屏蔽字

工作原理

定义一个64位的 与屏蔽字类似的 变量,将该变量设置为要的值,再通过sigprocmask函数将某信号对应的位设置为0或者为1。

如何设置这么个变量

函数原型

#include <signal.h>
            
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset(sigset_t *set, int signum);
int sigdelset(sigset_t *set, int signum);

set就是我们前面说的变量,至于变量名也可以定义为其它的名字,不一定非要叫set。

功能

设置变量的值
1)sigemptyset:将变量set的64位全部设置为0。
2)sigfillset:将变量set的64位全部设置为1。
3)sigaddset:将变量set中,signum(信号编号)对应的那一位设置为1,其它为不变。
4)sigdelset:将变量set的signum(信号编号)对应的那一位设置为0,其它位不变。

返回值

调用成功返回0,失败返回-1,并且errno被设置。

sigprocmask

函数原型

#include <signal.h>      
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);

功能

使用设置好的变量set去修改信号屏蔽字。

参数

①how:修改方式,有三种。

1、SIG_BLOCK:屏蔽某个信号
屏蔽字=屏蔽字 | set

2、SIG_UNBLOCK:打开某个信号(不要屏蔽),实际就是对屏蔽字的某位进行清0操作。
屏蔽字=屏蔽字&(~set)

3、SIG_SETMASK:直接使用set的值替换掉屏蔽字

②set:set的地址

③oldset:保存修改之前屏蔽字的值
如果写为NULL的话,就表示不保存。

返回值

函数调用成功返回0,失败返回-1。

代码演示

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>

void signal_fun(int signo)
{
    sigset_t set;
    printf("hello\n");
    sigemptyset(&set);
    sigaddset(&set, SIGINT);
    sigprocmask(SIG_UNBLOCK, &set, NULL);
    sleep(3);
    printf("world\n");    
}

int main(int argc, char **argv, char **environ)
{
    pid_t ret = 0;
    signal(SIGINT, signal_fun);
    while(1);
    return 0;
}
View Code

执行结果,按下Ctrl+C时触发信号处理函数。这个进程杀不掉,必须用kill pid的方式

root@ubuntu:~# ./a.out 
^Chello
world
^Chello
world
^Chello
world
Terminated

 

未处理信号集

跟屏蔽字一样,也一个64位的无符号整形数,专门用于记录未处理的信号。“未处理信号集”同样也是被放在了进程的进程表中(task_struct)。

信号来了,当进程的信号处理机制,检查该信号在屏蔽字中的对应位时发现是1,表示该信号被屏蔽了,暂时不能被处理,此时就会将“未处理信号集”中该信号编号所对应的位设置为1,这个记录就表示,有一个信号未被处理。如果该信号发送了多次,但是每一次都因为被屏蔽了而无法处理的话,在“未处理信号集”中只记录一次。当屏蔽字中该信号的位变成0时(被打开了),此时就回去检查“未处理信号”,看该信号有没有未决的情况,有的话就处理它。

posted @ 2018-07-29 21:26  克拉默与矩阵  阅读(2148)  评论(0编辑  收藏  举报