Linux系统编程之进程间通信(信号)

信号(signal):
信号的基本概念:
信号是事件发生时对进程的通知机制,有时也叫软件中断,信号可以让一个正运行进程被另一个运行进程异步进程中断,转而处理某突发事件。

注意:信号的产生和处理都是由内核完成

简单说一下使内核为进程产生信号的事件有:

1 终端发特殊字符,如CTRL+C 对前台进程发送中断信号
2 硬件发生异常,如被0除等异常机器语言指令。
3 系统状态变化:如alarm定时器到期引起SIGALRM信号,进程某个子进程退出
4 运行kill 命令/函数。

信号使用的目的:
1.让进程知道发生了一件特定的事情。
2.强迫进程执行自己的信号处理程序。

特点:
简单,但不能携带大量的信息,满足特定条件才发送信号,其优先级比较高。

命令:
在linux下用kill -l 查看信号列表 前31为常规信号,后面的是实时信号。

信号列表:
linux下的信号专用名、定义值和默认反映
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

信号的默认动作:
Term终止
lgn忽略
Core终止并生成一个Core文件(为了调试错误)
Stop暂停当前进程
Cont继续执行当前被暂停的进程

信号的几种状态:
产生未处理未决),递达被处理

信号的处理方法:
信号有三种处理方法,分别是:忽略捕捉默认动作

忽略信号
大多数信号可以使用这个方式来处理,但是有两种信号不能被忽略(分别是 SIGKILLSIGSTOP)。因为他们向内核和超级用户提供了进程终止和停止的可靠方法,如果忽略了,那么这个进程就变成了没人能管理的的进程,显然是内核设计者不希望看到的场景
捕捉信号:
需要告诉内核,用户希望如何处理某一种信号,说白了就是写一个信号处理函数,然后将这个函数告诉内核。当该信号产生时,由内核来调用用户自定义的函数,以此来实现某种信号的处理。
系统默认动作:
对于每个信号来说,系统都对应由默认的处理动作,当发生了该信号,系统会自动执行。不过,对系统来说,大部分的处理方式都比较粗暴,就是直接杀死该进程。

温馨提示:
SIGKILLSIGSTOP信号不能被捕捉,阻塞或忽略,只能执行默认动作

上面唠叨了那么多,那么信号到底是怎么来使用的呢
1.信号处理函数的注册(接收)
信号处理函数分为入门版和高级版

1.入门版:signal
2.高级版:sigaction

函数原型:
入门版:signal

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

高级版:sigaction

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

2.信号处理发送函数
信号处理发送函数同样分为入门版和高级版

1.入门版:kill
2.高级版:sigqueue

函数原型:
入门版:kill

 int kill(pid_t pid, int sig);

高级版:sigqueue

 int sigqueue(pid_t pid, int sig, const union sigval value);

3.信号处理函数的注册demo
信号注册函数入门版—signal

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

根据函数原型可以看出由两部分组成,一个是真实处理信号的函数,另一个是注册函数了。 对于sighandler_t signal(int
signum, sighandler_t handler);函数来说,signum 显然是信号的编号,handler 是中断函数的指针。
同样,typedef void (*sighandler_t)(int);中断函数的原型中,有一个参数是 int
类型,显然也是信号产生的类型,方便使用一个函数来处理多个信号。

signaldemo12.c

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

// typedef void (*sighandler_t)(int);
//sighandler_t  signal(int  signum, sighandler_t handler);

void handler(int signum)
{
    
    
        switch(signum)
        {
    
    
                case 1:
                        printf("signum is :%d\n",signum);
                        break;
                case 3:
                        printf("signum is :%d\n",signum);
                        break;
                case 2:
                        printf("signum is :%d\n",signum);
                        break;
        }

        printf("never quit!\n");
}

int main()
{
    
    
		printf("the pid is:%d\n",getpid());
        signal(SIGINT,handler);
        signal(SIGQUIT,handler);
        signal(SIGHUP,handler);

        while(1);
        return 0;
}

signaldemo14.c

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

// typedef void (*sighandler_t)(int);
//sighandler_t  signal(int  signum, sighandler_t handler);

void handler(int signum)
{
    
    
        switch(signum)
        {
    
    
                case 1:
                        printf("signum is :%d\n",signum);
                        break;
                case 3:
                        printf("signum is :%d\n",signum);
                        break;
                case 2:
                        printf("signum is :%d\n",signum);
                        break;
        }

        printf("never quit!\n");
}

int main()
{
    
    
        printf("the pid is:%d\n",getpid());
        signal(SIGINT,SIG_IGN);
        signal(SIGQUIT,handler);
        signal(SIGHUP,handler);
        signal(SIGKILL,SIG_IGN);
        
        while(1);
        return 0;
}

发信号: ps -aux|grep a.out
例如:kill -2 进程id

简单的总结一下,我们通过 signal 函数注册一个信号处理函数,分别注册了两个信号(SIGIO 和
SIGUSER1);随后主程序就一直“长眠”了。 通过 kill 命令发送信号之前,我们需要先查看到接收者,通过 ps
命令查看了之前所写的程序的 PID,通过 kill 函数来发送。 对于已注册的信号,使用 kill
发送都可以正常接收到,但是如果发送了未注册的信号,则会使得应用程序终止进程。

那么,已经可以设置信号处理函数了,信号的处理还有两种状态,分别是默认处理和忽略,这两种设置很简单,只需要将 handler 设置为SIG_IGN(忽略信号)或 SIG_DFL(默认动作)即可。

在此还有两个问题需要说明一下:
1.当执行一个程序时,所有信号的状态都是系统默认或者忽略状态的。除非是 调用exec进程忽略了某些信号。exec 函数将原先设置为要捕捉的信号都更改为默认动作,其他信号的状态则不会改变 。
2.当一个进程调动了 fork 函数,那么子进程会继承父进程的信号处理方式。

我们看到以上有三个demo,signaldemo12.c 是用signal来捕获信号SIGINT,SIGQUITSIGHUP并做相应的处理,记住这是接收信号函数,发送的还是kill,我们可以在终端命令行用kill指令向demo12.c发送信号,signaldemo14.c 是执行忽略信号的处理,但记住上面说过的SIGKILLSIGSTOP是不能被忽略,捕捉的,只能执行默认动作

那么入门版的信号注册函数看完了,那么现在我们来看一下入门版的信号的发送函数
signaldemo13.c

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

// int kill(pid_t pid, int sig);

int main(int argc,char **argv)
{
    
    
        int pid;
        int signum;
        char cmd[128];

        printf("the argc is:%d\n",argc);

        pid = atoi(argv[1]);
        signum = atoi(argv[2]);

        printf("pid = %d,signum = %d\n",pid,signum);

        sprintf(cmd,"kill -%d %d",pid,signum);
//      kill(pid,signum);
        system(cmd);
        printf("kill ok!\n");

        return 0;
}

从代码中可以看出,向一个进程发送信号是用kill函数来发送,或者是用system来对进程发送信号

信号注册函数高级版----sigaction
函数原型:

 #include <signal.h>
 int sigaction(int signum, const struct sigaction *act,struct sigaction *oldact);
 
 struct sigaction {
    
    
   void       (*sa_handler)(int);
   void       (*sa_sigaction)(int, siginfo_t *, void *);
   sigset_t   sa_mask;
   int        sa_flags;
 };

参数说明:

void(*sa_handler)(int);
信号处理函数,不接受额外数据,SIG_IGN 为忽略,SIG_DFL 为默认动作,如果 sa_flags 中
存在SA_SIGINFO 标志,那么 sa_sigaction 将作为 signum 信号的处理函数,否则用 sa_handler 
	
void(*sa_sigaction)(int, siginfo_t *, void *);
信号处理函数,能够接受额外数据和 sigqueue 配合使用

sigset_t sa_mask;
阻塞关键字的信号集,可以在调用捕捉函数之前把信号添加到信号阻塞字,信号捕捉函数返回之前恢复为原先的值

int sa_flags;
影响信号的行为,SA_SIGINFO 表示能够接受数据

sigaction 函数是一个系统调用,根据这个函数原型,我们不难看出,在函数原型中,第一个参数 signum应该就是注册的信号的编号;第二个参数 act如果不为空说明需要对该信号有新的配置;第三个参数 oldact如果不为空,那么可以对之前的信号配置进行备份,以方便之后进行恢复。

struct sigaction 结构体中的 sa_mask成员,设置在其的信号集中的信号,会在捕捉函数调用前设置为阻塞,并在捕捉函数返回时恢复默认原有设置。这样的目的是,在调用信号处理函数时,就可以阻塞默写信号了。在信号处理函数被调用时,操作系统会建立新的信号阻塞字,包括正在被递送的信号。因此,可以保证在处理一个给定信号时,如果这个种信号再次发生,那么他会被阻塞到对之前一个信号的处理结束为止。

sigaction 函数的时效性:当对某一个信号设置了指定的动作的时候,那么,直到再次显式调用sigaction 函数并改变动作之前都会一直有效。

void(*sa_sigaction)(int, siginfo_t *, void *);处理函数中,第一个参数 int是信号;第三个参数 void *是接收到信号所携带的额外数据,不为空则说明有额外数据;而第二个参数 struct siginfo_t这个结构体主要适用于记录接收信号的一些相关信息

siginfo_t {
    
    
               int      si_signo;    /* Signal number */
               int      si_errno;    /* An errno value */
               int      si_code;     /* Signal code */
               int      si_trapno;   /* Trap number that caused
                                        hardware-generated signal
                                        (unused on most architectures) */
               pid_t    si_pid;      /* Sending process ID */
               uid_t    si_uid;      /* Real user ID of sending process */
               int      si_status;   /* Exit value or signal */
               clock_t  si_utime;    /* User time consumed */
               clock_t  si_stime;    /* System time consumed */
               sigval_t si_value;    /* Signal value */
               int      si_int;      /* POSIX.1b signal */
               void    *si_ptr;      /* POSIX.1b signal */
               int      si_overrun;  /* Timer overrun count; POSIX.1b timers */
               int      si_timerid;  /* Timer ID; POSIX.1b timers */
               void    *si_addr;     /* Memory location which caused fault */
               long     si_band;     /* Band event (was int in
                                        glibc 2.3.2 and earlier) */
               int      si_fd;       /* File descriptor */
               short    si_addr_lsb; /* Least significant bit of address
                                        (since kernel 2.6.32) */
           }

我们可以通过这个结构体获取到信号相关的信息,si_signosi_code是必须实现的两个成员。
关于发送过来的数据存在两个地方:si_value保存了发送过来的信息,同时,在 si_int或者 si_ptr成员中也保存了对应的数据

好,以上就是对sigaction的相关信息介绍完了,那么现在我们就来看一下demo
sigactiondemo15.c

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

//int sigaction(int signum, const struct sigaction *act,struct sigaction *oldact);

void handler(int signum, siginfo_t *info, void *context)
{
    
    
        printf("the signum is:%d\n",signum);

        if(context != NULL)
        {
    
    
                printf("get the data is:%d\n",info->si_int);
                printf("get the data is:%d\n",info->si_value.sival_int);
                printf("from the:%d\n",info->si_pid);
        }
}

int main()
{
    
    
        struct sigaction act;

        printf("the pid is:%d\n",getpid());
        act.sa_sigaction = handler;
        act.sa_flags = SA_SIGINFO;

        sigaction(SIGUSR1,&act,NULL);

        while(1);
        return 0;
}

信号发送函数高级版——sigqueue

#include <signal.h>
int sigqueue(pid_t pid, int sig, const union sigval value);

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

温馨提示:
1.使用sigqueue 函数前,sigaction 函数需要制定 SA_SIGINFO标志
2.sigaction 结构体中的 sa_sigaction成员提供了信号捕捉函数。如果实现的是 sa_handler成员,那么将无法获取额外携带的数据
3.sigqueue 函数只能把信号发送给单个进程,可以使用 value参数向信号处理函数传递整数值或者指针值
4.sigqueue 函数不但可以发送额外的数据,还可以让信号进行排队(操作系统必须实现了POSIX.1的实时扩展),对于设置了阻塞的信号,使用 sigqueue发送多个同一信号,在解除阻塞时,接受者会接收到发送的信号队列中的信号,而不是直接收到一次;但是,信号不能无限的排队,信号排队的最大值受到 SIGQUEUE_MAX的限制,达到最大限制后,sigqueue会失败,errno会被设置为 EAGAIN

那么现在我们直接来看一下demo
sigqueuedemo16.c

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

int main(int argc,char **argv)
{
    
    
//      int sigqueue(pid_t pid, int sig, const union sigval value);

        int pid;
        int signum;

        pid = atoi(argv[1]);
        signum = atoi(argv[2]);

        union sigval value;
        value.sival_int = 1234536;

        sigqueue(pid,signum,value);

        printf("the pid is:%d\n",getpid());
        printf("ok the send is done!\n");

        return 0;
}

这个demo的意思就是向某个进程发送数字为1234536的内容。
那么现在我们就来看一下结果:
接收端(即sigactiondemo15.c )

CLC@Embed_Learn:~/SECOND/IPC$ ./r 
the pid is:22930
the signum is:10
get the data is:1234536
get the data is:1234536
from the:22932

发送端(即sigqueuedemo16.c):

CLC@Embed_Learn:~/SECOND/IPC$ ./s 22930 10 
the pid is:22932
ok the send is done!

以上就是对进程间通信之信号的学习笔记,仅供参考

猜你喜欢

转载自blog.csdn.net/weixin_51976284/article/details/124741261