进程间通信之信号

概念:信号(signal)是一种软件中断,是UNIX系统中最为古老的进程之间的通信机制。用于在一个或多个进程之间传递异步信号。它提供了一种处理异步事件的方法,也是进程间惟一的异步通信方式。在Linux中,根据POSIX标准扩展以后的信号机制,不仅可以用来通知某种程序发生了什么事件,还可以给进程传递数据。

作用:用于进程之间的通信;通知进程发生了异步事件

来源:信号可以由各种异步事件产生,如键盘中断等,也可以由内核产生,也可以由系统中的其他进程产生。信号的来源可以有很多种方式,按照产生条件的不同可以分为硬件方式和软件方式两种。

硬件方式:当用户在终端上按下某键或某种组合键时,将产生信号;硬件异常产生信号。这些事件通常由硬件检测到,并将其通知给Linux操作系统内核,然后内核生成相应的信号,并把信号发送给该事件发生时的正在运行的程序。

软件方式:用户在终端下调用kill命令向进程发送任务信号;进程调用kill或sigqueue函数发送信号;当检测到某种软件条件已经具备时发出信号,如SIGALARM信号。

一般情况下信号来源于:

         程序的错误:如非法访问内存、除0操作(硬件方式)

         外部信号(终端按键):如按下ctrl+c等(硬件方式)

         通过kill或sigqueue向另一个进程发送信号(软件方式)

产生:

键盘事件:ctrl+c,ctrl+\等

非法内存:如果内存管理出错,系统便会发送一个信号进行处理

硬件故障:如果硬件发生故障,系统也会发送一个信号进行处理

环境切换:如从用户态切换到其他状态时,也会产生一个信号,此信号会告知系统状态发生改变

信号的种类:

在终端上通过命令kill –l查询可知,Linux下信号共有62种

*没有32号和33号信号

         在这些信号中,1~31号信号是普通信号,都是继承自UNIX系统,是不可靠信号,34~64号信号是实时信号,也称可靠信号。当导致产生信号的事件发生时,内核就产生一个信号。信号产生后,内核通常会在进程表中设置某种类型的标志,当内核设置了这个标志后,即向一个进程传递了一个信号。信号产生和传递之间的时间间隔称为主信号未决。

         进程可以调用sigpending(主信号未决)将信号设为阻塞,如果为进程产生一个阻塞信号,而对信号的动作是捕捉该信号(即不忽略),则内核将为该进程的此信号保持为未决状态,直到该进程对此信号解除阻塞或者对此信号的响应更改为忽略。如果在进程解除对某个信号的阻塞之前,这种信号发生了多次,那么如果信号被传递多次(即信号在未决信号队列中排队)则称为可靠信号,只被传递一次的信号称为不可靠信号。

         信号的优先级:信号的实质是软中断,既然软中断有优先级,那么信号也有优先级。如果一个进程有多个未决信号,则对于同一个未决的实时信号,内核将按照发送的顺序来传递信号。如果存在多个未决信号,则信号对应的值越小的越先被传递。如果既存在不可靠信号,又存在可靠信号,则Linux系统规定先传递不可靠信号。

         信号的含义:

通过命令man 7 signal查看信号的具体描述

         第一列为信号的宏定义名称,第二列为信号的编号,第三列为默认的处理动作,第四列是简要介绍,说明产生该信号的条件

Term表示终止当前进程

Ign表示忽略该信号

Core表示终止当前进程并且生成Core文件

Stop表示停止当前进程

Cont表示继续执行先前停止的进程

1)SIGHUP:当用户退出shell时,由该shell启动的所有进程将收到这个信号,默认动作为终止进程

2)SIGINT:当用户按下了<Ctrl+C>组合键时,用户终端向正在运行中的由该终端启动的程序发出此信号。默认动作为终止里程

3)SIGQUIT:当用户按下<ctrl+\>组合键时产生该信号,用户终端向正在运行中的由该终端启动的程序发出些信号。默认动作为终止进程

4)SIGILL:CPU检测到某进程执行了非法指令。默认动作为终止进程并产生core文件

5)SIGTRAP:该信号由断点指令或其他 trap指令产生。默认动作为终止里程 并产生core文件

6)SIGABTRT:调用abort函数时产生该信号。默认动作为终止进程并产生core文件

7)SIGBUS:非法访问内存地址,包括内存对齐出错,默认动作为终止进程并产生core文件

8)SIGFPE:在发生致命的运算错误时发出。不仅包括浮点运算错误,还包括溢出及除数为0等所有的算法错误。默认动作为终止进程并产生core文件

9)SIGKILL:无条件终止进程。本信号不能被忽略,处理和阻塞。默认动作为终止进程。它向系统管理员提供了可以杀死任何进程的方法

10)SIGUSR1:用户定义的信号。即程序员可以在程序中定义并使用该信号。默认动作为终止进程

11)SIGSEGV:指示进程进行了无效内存访问。默认动作为终止进程并产生core文件

12) SIGUSR2:这是另外一个用户自定义信号 ,程序员可以在程序中定义 并使用该信号。默认动作为终止进程

13)SIGPIPE:Broken pipe向一个没有读端的管道写数据。默认动作为终止进程

14)SIGALRM:定时器超时,超时的时间 由系统调用alarm设置。默认动作为终止进程

15)SIGTERM:程序结束信号,与SIGKILL不同的是,该信号可以被阻塞和终止。通常用来表示程序正常退出。执行shell命令Kill时,缺省产生这个信号。默认动作为终止进程

17)SIGCHILD:子进程结束时,父进程会收到这个信号。默认动作为忽略这个信号

18)SIGCONT:停止进程的执行。信号不能被忽略,处理和阻塞。默认动作为终止进程

19)SIGSTOP:非终端来的停止信号。默认动作为停止进程

20)SIGTSTP:停止进程的运行。按下<ctrl+z>组合键时发出这个信号。默认动作为暂停进程

21)SIGTTIN:后台进程读终端控制台。默认动作为暂停进程

22)SIGTTOU:该信号类似于SIGTTIN,在后台进程要向终端输出数据时发生。默认动作为暂停进程

23)SIGURG:套接字上有紧急数据时,向当前正在运行的进程发出些信号,报告有紧急数据到达。如网络带外数据到达,默认动作为忽略该信号

24)SIGXCPU:CPU时限超时信号。默认动作为终止进程

25)SIGXFSZ:进程执行时间超过了分配给该进程的CPU时间 ,系统产生该信号并发送给该进程。默认动作为终止进程

26)SIGVTALRM:拟时钟超时时产生该信号。类似于SIGALRM,但是该信号只计算该进程占用CPU的使用时间。默认动作为终止进程

27)SIGPROF:类似于SIGVTALRM,它不公包括该进程占用CPU时间还包括执行系统调用时间。默认动作为终止进程

28)SIGWINCH:窗口变化大小时发出。默认动作为忽略该信号

29)SIGIO:此信号向进程指示发出了一个异步IO事件。默认动作为忽略

30)SIGPWR:关机。默认动作为终止进程

31)SIGSYS:无效的系统调用。默认动作为终止进程并产生core文件

32)SIGRTMIN~SIGRTMAX:LINUX的实时信号,它们没有固定的含义(可以由用户自定义)。所有的实时信号的默认动作都为终止进程

信号的处理:

         信号通常是由终端驱动程序、进程或系统发出,由进程接收。事实上,给进程发一个信号就是修改目标进程中PCB结构体中的关于信号的字段。而进程收到信号与否本身就是一个原子问题,即要么收到,要么没收到。所以再存储信号的时候通常使用为徒来表示信号是否收到,若收到,就将一个比特位置为1。

         进程收到信号后的处理方法有以下三种 :

1.忽略。大多数信号可以给它忽略掉,除了SIGKILL和SIGSTOP,此两个信号向内核和超级用户提供了进程终止和停止的可靠方法,如果忽略,那么进程就变成了没人管理的进程。

2.执行该信号的默认处理动作。对于每个信号来说,系统都对应有默认的处理动作,当该信号发生时,系统会自动执行。不过,系统对信号的默认处理方式一般都比较简单粗暴,就是直接杀死该进程。

3.用户自定义处理函数。用户自己编写一个函数来处理信号,并把这个函数告诉内核,当该信号产生时,由内核来调用用户自定义的函数,以此来实现某种信号的处理。

某些信号的默认处理方式:

程序不可捕获、阻塞或忽略的信号(不能自定义处理函数、不能忽略):SIGKILL、SIGSTOP

不能恢复至默认动作的信号:SIGILL、SIGTRAP

默认会导致进程流产的信号:SIGABRT、SIGBUS、SIGFPE、SIGILL、SIGIO、SIGQUIT、SIGSEGV、SIGTRAP、SIGXCPU、SIGXFSZ

默认会导致进程退出的信号:SIGALRM、SIGHUP、SIGINT、SIGKILL、SIGPIPE、SIGPOLL、SIGPROF、SIGSYS、SIGTERM、SIGUSR1、SIGUSR2、SIGVTALRM

默认会导致进程停止的信号:SIGSTOP、SIGTSTP、SIGTTIN、SIGTTOU

默认进程忽略的信号:SIGCHLD、SIGPWR、SIGURG、SIGWINCH

信号处理函数:

1.信号截取函数signal()

typedef void (*sighandle_t) (int);
sighandle_t signal(int signum,sighandle_t handler);

功能:截取系统信号,对此信号挂接用户自己的处理函数

参数:第一个参数指定信号的值,第二个参数指定针对前面信号值的处理方式,函数指针类型。

返回值:如果调用成功,返回最后一次为安装信号signum而调用signal()时的handle值,失败则返回SIG_ERR。

2.改变进程接收到特定信号后的行为的函数sigaction()

int sigaction(int signum,const sigaction *act,strict sigaction *oldact);

功能:用于改变某个进程接收到指定信号后的行为

参数:第一个参数为指定信号的值,可以为除SIGKILL和SIGSTOP外的任何一个特定的有效的信号。第二个参数是指向结构sigaction的一个实例指针,在结构sigaction的实例中,指定了对特定信号的处理,可以为空,进程会以缺省方式对信号进行处理。第三个参数oldact指向的对象用来保存原来对相应信号的处理,可指定为空。如果第二个和第三个参数都设为NULL,则用于检查信号的有效性。

3.对当前进程发送指定信号函数

int raise(int signo);

 4.将进程挂起等待信号函数

int pause(void);

5.通过进程编号发送信号函数

int kill(pid_t pid,int signo);

6.指定时间发送SIGALRM信号函数

unsigned int alarm(unsigned int seconds);

7.

int sigqueue(pid_t pid,int signo,const union sigval val);

         类似于kill()函数,多了共用体union sigval参数,将共用体中的成员int sival_int或*sigval_ptr的值传递给信号处理函数中的定义类型siginfo_t中的int si_int或void *si_ptr。

8.定时发送信号函数

int settimer(int which,const struct itimerval *value,struct itimerval *oldvalue);

         which可指定三种信号类型:SIGALRM、SIGVTALRM、SIGPROF,作用时间也因which的值不同而不同,struct itimerval的成员value定义时间间隔,value为0时,,计时器失效

9.造成进程终止函数,除非捕获SIGABORT信号

void abort(void);

10.设置所有的信号到set信号集中

sigfillset(sigset_t *set);                       

11.从set信号集中清空所有信号

 sigemptyset(sigset_t *set);               

12.在set信号集中加入sig信号

 sigaddset(sigset_t *set,int sig);                

13. 在set信号集中删除sig信号

sigdelset(sigset_t *set,int sig);                 

14.

int sigprocmask(int how,const sigset_t *set,sigset_t *oldset); 

              对信号集当中的信号进行操作

how:可取值SIG_BLOCK,表示屏蔽信号集中的信号;SIG_UNBLOCK,表示解除信号集中的信号;SIG_SETMASK,表示设置信号集中的信号操作

15.获取在阻塞中的所有信号

int sigpending(sigset_t *set);            

16.类似于pause()函数

int sigsuspend(const sigset_t *set);                           

信号处理函数的处理过程:

1.注册信号处理函数

         信号的处理是由内核来代理的,首先程序通过signal()函数或sigaction()函数为每个信号注册处理函数,而内核中维护一张信号向量表,对应信号处理机制。这样,在信号在进程中注销完毕之后回调用相应的处理函数进行处理。

2.信号的检测与响应时机

         在系统调用或中断返回用户态的前夕,内核会检查未决信号集,进行相应的信号处理。

3.处理过程

         程序运行在用户态时,进程由于系统调用或中断进入内核,然后转向用户态执行信号处理函数,信号处理函数完毕后进入内核,然后返回用户态继续执行程序。

首先程序执行在用户态,在进程陷入内核并从内核返回的前夕,会去检查有没有信号被处理,如果有且没有被阻塞就会调用相应的信号处理函数去处理。首先,内核在用户栈上创建一个层,该层中将返回地址设置成信号处理函数的地址,这样,从内核返回到用户态时,就会执行这个信号处理函数。当信号处理函数执行完,会再次进入内核,主要是检测有没有信号没有处理,以及恢复原先程序中断执行点,恢复内核栈等工作,这样,当从内核返回后便返回到原先程序执行的地方了。

案例1.SIGINT捕获

signal1.c

#include <stdio.h>
#include <signal.h>
#include <string.h>

//信号处理函数
void handle(int signo)
{
    printf("接收到信号:%d\n",signo);
}

int main()
{
    signal(SIGINT,handle);
    printf("SININT信号捕捉到!\n");
    char buffer[1024] = {0};
    printf("进程号为:%d\n",getpid());
    while(1)
    {
        printf("input:");
        scanf("%s",buffer);
        //如果输入的内容是"quit"
        if(strcmp(buffer,"quit") == 0)
        {
            break;
        }
    }
    printf("exit......\n");
    return 0;
}

Ctrl + C,即为SIGINT信号,表示程序终止。

案例2.设置信号传送闹钟

函数alarm()来设置信号SIGALRM,在经过参数seconds指定的秒数后传送给目前的进程。如果参数seconds设置为0,则之前设置的闹钟会被取消,并将剩下的时间返回。

Signal_Alarm.c

案例3:使用kill()函数发送信号

int kill(pid_t pid,int signo);

功能:给指定进程发送指定信号

参数:pid:指定进程的进程ID,当pid > 0时,发送信号给指定进程;当pid = 0时,发送信号给与调用kill()函数进程属于同一进程组的所有进程;当pid < 0时,发送信号给pid绝对值对应的进程;当pid = -1时,发送信号给进程有权限发送的系统中的所有进程。

signo:一般理解为信号编号,但建议使用信号名,因为涉及到跨平台的程序时,很可能会因为不同平台信号编号不同而出现错误。

返回值:成功,返回0;失败,返回-1,可通过errno查看错误原因

int pause(void);

功能:使目前的进程暂停,即进入睡眠状态,直到被信号中断。

返回值:只返回-1

Signal_Alarm1.c

 

#include <stdio.h>
#include <signal.h>
#include <unistd.h>
static int i = 0;
void handle(int signo)
{
    i = 1;
}

int main()
{
    //程序通过fork()启动新进程,子进程睡眠5秒后,向父进程发送一个SIGALRM信号
    int pid;
    printf("程序启动......\n");
    pid = fork();
    if(pid == -1)       //创建失败
    {
        perror("fork:");
        return -1;
    }
    //子进程
    else if(pid == 0)
    {
        sleep(5);       //先睡眠5秒
        kill(getppid(),SIGALRM);    //5秒后向其父进程发送SIGALRM信号
    }
    //父进程
    else
    {
        //父进程捕获SIGALRM信号后暂停执行,直到接收到下一个信号
        signal(SIGALRM,handle);
        //挂起当前进程直到有一个信号出现
        pause();
        if(i)
        {
            printf("ding......ding......\n");
        }
        printf("Done......\n");
    }
    return 0;
}

案例4:使用sigaction()函数来代替signal()函数

int  sigaction(int signo,cinst struct sigaction *act,struct sigaction *oldact);

功能:用于改变进程接收到特定信号后的行为

参数:signo:信号的值,可以处理除SIGKILL和SIGSTOP之外的任何一个特定有效的信号

act:指向sigaction的一个实例指针,在sigaction实例中,指定了对特定信号的处理,可以为NULL,进程会以缺省方式对信号进行处理

oldact:指向的对象用来保存对原来相应信号的处理,可以为NULL

返回值:成功,返回0;失败,返回-1

         sigaction()函数用来检查或修改与指定信号相关联的处理动作,该函数取代了signal()函数。因为signal()函数在信号未决时接收信号可能出现问题,所以sigaction()函数比signal()函数更安全。

         在本案例中使用sigaction()函数来代替signal()函数设置Ctrl + C组合键(SIGINT信号)的处理函数为handle。

SigactionDemo.c

#include <stdio.h>
#include <signal.h>
void handle(int signo)
{
    printf("Got Signal %d\n",signo);
}

int main()
{
    struct sigaction act;       //声明sigaction结构体
    act.sa_handler = handle;    //设置信号处理函数
    sigemptyset(&act.sa_mask);  //将信号集合设置为空
    act.sa_flags = 0;
    sigaction(SIGINT,&act,0);
    while(1)
    {
        printf("Hello World !\n");
        sleep(2);
    }
}

 

案例5:sigqueue()函数实例

int sigqueue(pid_t pid,int signo,const union sigval val);

功能:和kill()函数类似,给指定进程发送指定信号

参数:pid:指定接收信号的进程ID

signo:指定要发送的信号

val:联合数据结构union sigval,指定了信号传递的参数

typedef union sigval
{
     int sival_int;
     void *sival_ptr;
}sigval_t;

返回值:成功,返回1;失败,返回-1

         sigqueue()函数比kill()函数传递了更多的附加信息,但sigqueue()只能向一个进程发送信号,而不能发送信号给一个进程组。如果signo = 0,函数将会执行错误检查,不发送任何信号,0值信号可用于检查pid的有效性以及当前进程是否有权限向目标进程发送信号。

         sigqueue()函数发送非实时信号时,第三个参数包含的信息仍然能够传递给信号处理函数,同时,不支持排队,即在信号处理函数执行过程中到来的所有相同的信号都被合并为一个信号。

SigqueueDemo1.c

#include <stdio.h>
#include <signal.h>
#include <stdlib.h>
void handle(int signo,siginfo_t *s,void *p)
{
    printf("signo = %d\n",signo);
    printf("recv message is %d\n",s->si_int);
}
int main()
{
    pid_t pid;
    struct sigaction act;
    act.sa_sigaction = handle;
    //设置了标志位,表示信号附带的参数可以被传递到信号处理函数中
    act.sa_flags = SA_SIGINFO;
    sigaction(SIGINT,&act,NULL);
    sigemptyset(&act.sa_mask);
    pid = fork();
    if(pid == 0)
    {
        //子进程中发送信号
        union sigval sv;
        sv.sival_int = 666;
        sigqueue(getppid(),SIGINT,sv);
        exit(0);
    }
    while(1)
    {
        pause();
    }
    return 0;
}

SigqueueDemo2.c

#include <stdio.h>
#include <signal.h>
void handle(int signo,siginfo_t *info,void *content)
{
    printf("recv singo:%d,value:%d\n",signo,info->si_int);
    printf("value:%d\n",info->_sifields._rt.si_sigval.sival_int);
}

int main()
{
    struct sigaction act;
    union sigval sv;
    sigemptyset(&act.sa_mask);
    act.sa_flags = SA_SIGINFO;
    act.sa_sigaction = handle;
    sigaction(SIGUSR1,&act,NULL);
    int i = 0;
    while(1)
    {
        sleep(1);
        sv.sival_int = i++;
        sigqueue(getpid(),SIGUSR1,sv);
    }
    return 0;
}

案例7:信号集处理函数

int sigemptyset(sigset_t *set);    

将信号集初始化为空

int sigfillset(sigset_t *set); 

把信号集初始化为包含所有已定义的信号

int sigaddset(sigset_t *set,int signo);  

把信号signo添加到信号集set中,成功返回0,失败时返回-1

int siddelset(sigset_t,int signo);             

把信号signo从信号集set中删除,成功返回0,失败返回-1

int sigismember(sigset_t *set,int signoo);   

判断给定的信号signo是否是信号集中的一员,如果是,返回1,如果不是,返回0,如果给定的信号无效,返回-1

int sigpromask(int how,const sigset_t *set,sigset_t *oldest);    

根据函数指定的方法修改进程的信号屏蔽字。新的信号屏蔽字由set指定,而原先的信号屏蔽字将保存在oldset中,如果set为空,则how就没有意义。成功返回0,如果how取值无效,返回-1,并设置errno

为EINVAL。

         how的取值有3个:SIG_BLOCK,表示把参数set添加到信号屏蔽字中;SIG_SETMASK,表示把信号屏蔽字设置为set中的信号;SIG_UNBLOCK,表示把信号屏蔽字从set中删除

int sigpending(sigset_t *set);

将被阻塞的信号中停留在待处理状态的一组信号写到set指向的信号集合中,成功返回0,否则返回-1。

int sigsuspend(const sigset_t *sigmask);

通过将进程的屏蔽字替换为由参数sigmask给出的信号集,然后挂起进程的执行。程序在信号处理函数执行完毕后继续执行。如果接收到信号终止了程序,sigsuspend就不会返回,如果接收到的信号没有终止程序,sigsuspend就会返回-1,并将errno设置为EINTR。

SignalSet1.c

#include <stdio.h>
#include <signal.h>
void handle(int signo)
{
    printf("signo = %d\n",signo);
}
int main()
{
    sigset_t set;       //用于记录屏蔽字
    sigset_t block;     //用于记录被阻塞的信号集
    struct sigaction act;
    //清空信号集
    sigemptyset(&set);
    sigemptyset(&block);
    //向信号集中添加信号SIGINT
    sigaddset(&set,SIGINT);
    //设置处理函数和信号集
    act.sa_handler = handle;
    sigemptyset(&act.sa_mask);
    act.sa_flags = 0;
    sigaction(SIGINT,&act,0);
    printf("等待信号SIGINT......\n");
    //挂起程序,等待信号
    pause();
    //设置进程屏蔽字,屏蔽SIGINT
    sigprocmask(SIG_SETMASK,&set,0);
    printf("请按下Ctrl + C......\n");
    sleep(10);
    //测试SIGINT信号是否被屏蔽
    sigpending(&block);
    if(sigismember(&block,SIGINT))
    {
        printf("信号SIGINT已被忽略......\n");
    }
    //在信号集中删除SIGINT信号
    sigdelset(&set,SIGINT);
    printf("等待信号SIGINT......\n");
    //将进程的屏蔽字重新设置,即取消对SIGINT的屏蔽
    sigsuspend(&set);
    printf("程序会在5秒后退出......\n");
    sleep(5);
    return 0;
}

结果分析:首先我们通过sigaction()函数改变了SIGINT信号的默认xi能够为,使之执行指定的函数handle(),所以输出signo = 2。然后,通过sigprocmask()设置了进程的信号屏蔽字,把SIGINT信号屏蔽起来,所以过了10秒之后用sigpending()函数去获取被阻塞的信号集时,检测到了被阻塞的信号SIGINT,输出“信号SIGINT已被忽略……”。最后,用函数sigdelset()函数去除先前用sigaddset()函数加在set上的信号SIGINT,再调用函数sigsuspend(),把进程的屏蔽字再次修改为set,此时,不再包含SIGINT,并挂起该进程。由于之前的SIGINT信号停留在待处理的状态,而现在进程已经不再阻塞该信号,所以进程马上对该信号进行处理,从而在最后,不用输入Ctrl + C也会出现后面的处理语句。

 

如果在10秒内输入Ctrl + C信号,则程序等待对Ctrl + C的处理:

如果10秒之内未输入Ctrl + C

 

  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值