进程间的通讯(ipc)-----信号

一、信号的定义

        信号是一个通讯方式,正在运行的进程可以收到各种信号,可以是终端发送的信号,可以是父子进程间发送结束信号,也可以是程序运行错误时发送的终止信号。

二、信号的种类

在linux终端下使用kill -l命令可以查看所有信号的种类,如下:

         常用的信号有在中断使用ctrl+c而产生的的信号SIGINT,会使得程序终止;子进程结束回向父进程发送的信号SIGCHLD等。

三、信号的发送

        发送信号一般是有用户、内核、进程和操作系统 等等来发送的:

        用户可以在终端使用ctrl+c(SIGINT),ctrl+z(SIGTSTP)、kill+pid(SIGKILL)(杀死进程)等来向进程发送信号。

        那么在进程中,c语言也向我们提供了可以手动向进程发送信号的函数,如下:

1.kill()

函数头文件                 #include <sys/types.h>
                                   #include <signal.h>
函数原型                     int kill(pid_t pid, int sig);
函数功能                     向指定的进程发送一个信号
函数参数                     pid:进程的id;
                                    sig:信号的id;
函数返回值                   成功返回 0;
                                     失败返回 -1,并设置 errno;

2.raise()

函数头文件                 #include <sys/types.h>
                                   #include <signal.h>
函数原型                     int raise(int sig);
函数功能                     向调用者发送一个信号;
函数参数                     sig:信号编号
        上面这两个函数,kill()函数是一个可以向其他进程发送信号的函数,raise()函数是一个只能向当前进程发送信号。实际上raise是对kill函数的一个封装,使用起来的话kill函数是比较灵活,而raise函数通常是程序发生某些错误或事件,才会调用该函数,使用信号处理。

四、信号的处理

1.linux中信号有三种处理方式

  1. 默认
  2. 忽略
  3. 自定义函数处理

        进程接收到信号后,会交给内核进行处理,内核会判定这个信号将使用哪种处理方式;在内核接收信号的处理中有一个task_struct的结构体,这个结构体会去判定信号的处理方式,这个结构体属于内核源码,保存管理进程的所有信息,内容非常多,这个结构体就不过多说明,下面一些进程的默认的处理方式:

  • 进程退出 SIGALRM,SIGHUP,SIGINT,SIGKILL,SIGPIPE,SIGPOLL,SIGPROF,SIGSYS,SIGTERM,SIGUSR1,SIGUSR2,SIGVTALRM
  • 进程忽略 SIGCHLD,SIGPWR,SIGURG,SIGWINCH
  • 进程暂停 SIGSTOP,SIGTSTP,SIGTTIN,SIGTTOU

        默认处理方式:就是我们不做设置的情况下,进程接收到信号的执行的操作。

        忽略:这种方式需要我们通过signal()函数手动设置。                                                                   

函数原型

#include <signal.h>

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

参数

  •  signum:要处理的信号的编号,例如 SIGINTSIGALRM 等。
  •  handler:指向信号处理函数的指针。这个函数接受一个 int 参数(信号编号),并且没有返回值。

返回值

  •  如果成功,signal 函数返回指向之前信号处理函数的指针。
  •  如果失败,返回 SIG_ERR。可以使用 perror 或 strerror 函数来打印错误信息。

        自定义处理函数:经常会使用,使用自定义函数,通常可以使用有两个函数来进行处理:一种是通过signal()函数来进行设置,另一种是通过sigaction()函数。

2.signal()函数处理方式

        下面是signal()函数的一个示例:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>
#include <sys/wait.h>
#include <signal.h>
typedef void (*sighandler_t)(int);
//也可以写成typedef void (*)(int) sighandler_t
void function(int SIG){                     //当子进程结束时会发送一个SIGCHLD信号;
    printf("%d:%s\n",SIG,strsignal(SIG));   //strsignal()函数是用来获取信号的信息;
}
int main()
{
    sighandler_t SIG =  signal(SIGCHLD,function);   //设置信号处理函数;
    if(SIG == SIG_ERR){
        perror("signal");
        exit(EXIT_FAILURE);
    }   
    pid_t pid = fork();     //创建一个子进程;
    if(pid == -1){
        perror("fork");
        exit(EXIT_FAILURE);
    }else if(pid ==0 ){     //子进程开始;
        printf("cpid=%d start\n",getpid());    //getpid是获取当前进程的ID号;
        printf("cpid=%d end\n",getpid());
        exit(EXIT_SUCCESS);
    }else if(pid > 0){      //主进程;
        printf("parent start\n");
        wait(NULL);          //等待子进程结束,并释放资源;
        printf("parent end\n");
        exit(EXIT_SUCCESS);
    }   
}        

         结果如下:

         可以看到,当子进程结束时,成功发出了SIGCHLD信号,并执行了自定义信号处理函数, 打印出了信号的编号,相关信息。

3.sigaction()函数处理方式

         signal函数,是一个比较旧的处理方法,不同于signal函数,sigaction处理函数,并且可以替代掉signal()函数,它提供了更多的控制,同时它也不需要声明typedef void (*sighandler_t)(int),因为使用这个函数会定义结构体,直接去设置结构体成员,但正因如此,该函数也显的复杂,来看一下sigaction()函数:

函数原型

#include <signal.h>
​
int sigaction(int sig, const struct sigaction *act, struct sigaction *oldact);

参数

  • sig:指定要操作的信号。
  • act:指向 struct sigaction 结构体的指针,它指定了信号的处理方式。如果这个参数是 NULL,sigaction 将不改变当前的信号处理,而是将当前的信号处理方式填充到 oldact 指向的结构体中。
  • oldact:如果非 NULL,它指向一个 struct sigaction 结构体,sigaction 将把当前的信号处理方式填充到这个结构体中。

返回值

  • 如果成功,sigaction 返回 0。
  • 如果出错,返回 -1,并设置 errno 以指示错误原因 

       这里面出现了参数为act和oldact两个结构体指针,这两个结构体正如其名,一个是旧的结构体,也就是旧的信号处理方式,一个是新的信号处理函数。通过提供两个结构体参数,sigaction函数允许用户同时设置新的行为并检索旧的行为。这种设计提供了灵活性,使得用户可以在更改信号处理行为之前了解当前的行为,并在需要时恢复它。当然还有其他优点,这里不过多叙述。后面会又关于这个的示例。

3.1.struct sigaction结构体

        可以看到在参数中出现了结构体,那来看一下这个结构体:

struct sigaction {
    void     (*sa_handler)(int);        /* 信号处理函数 */
    void     (*sa_sigaction)(int, siginfo_t *, void *); /* 另一种信号处理函数 */
    sigset_t sa_mask;                   /* 信号集,指定在信号处理函数执行时需要被屏蔽的信号 */
    int      sa_flags;                        /* 信号处理选项 */
    void     (*sa_restorer)(void);     /* 已废弃,保留为 NULL */
};

        在这个结构体中,出现了五个变量,但因为void     (*sa_restorer)(void); 已经废弃,所以这个结构体中就只有四个变量需要关心,分开来看一下。

1.void     (*sa_handler)(int); 

         sigaction()函数也是支持使用void (*sa_handler)(int)的,只需要将函数的地址赋给这个指针就可以。没有特别的需要或者情况,sa_flags和sa_mask可以不用设置,下面是一段简单的示例:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <signal.h>
void function(int SIG){                     //自定义处理函数;
    printf("%d:%s\n",SIG,strsignal(SIG));   //strsignal()函数是用来获取信号的信息;
}
int main()
{
    struct sigaction sa;      //定义一个结构体变量;
    memset(&sa, 0, sizeof(sa));//初始化;
    sa.sa_handler = &function;//将地址给函数指针;
    int res = fork();          //创建一个子进程;
    if(res == -1){
        perror("fork");
        exit(EXIT_FAILURE);
    }else if(res == 0){       //子进程部分;
        printf("child start\n");
        sleep(2);
        printf("chlid end\n");
        exit(EXIT_SUCCESS);
    }else{                    //主进程部分
        printf("parent start\n");
        if(sigaction(SIGCHLD, &sa, NULL) == -1){ //调用自定义处理函数;
            perror("sigaction");
            exit(EXIT_FAILURE);                                                                                                                                                                                                                
        }
        wait(NULL);            //等待子进程,同时释放资源;
        printf("parent end\n");
    }   
    return 0;
}    
~           

        结果如下:

        可以发现运行结果相同,说明sigaction()函数确实可以代替signal()函数,但 sigaction()函数却不仅于此,继续往下看。

2.void     (*sa_sigaction)(int, siginfo_t *, void *);

        void     (*sa_sigaction)(int, siginfo_t *, void *),这个函数指针相较于第一种增加了一些参数,可以接收额外的信息。这里函数的第二个参数是sigset_t*的一个指针,第三个参数是用来打印信息,信号处理函数中并不常用,它是一个指向上下文信息的指针,通常是一个ucontext_t结构体的指针。这个上下文结构体包含了信号发生时的进程上下文,如寄存器状态、栈信息等。

        我们来主要看sigifno_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 */
               union sigval 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 Linux 2.6.32) */
               void    *si_lower;     /* Lower bound when address violation
                                         occurred (since Linux 3.19) */
               void    *si_upper;     /* Upper bound when address violation
                                         occurred (since Linux 3.19) */
               int      si_pkey;      /* Protection key on PTE that caused
                                         fault (since Linux 4.6) */
               void    *si_call_addr; /* Address of system call instruction
                                         (since Linux 3.5) */
               int      si_syscall;   /* Number of attempted system call
                                         (since Linux 3.5) */
               unsigned int si_arch;  /* Architecture of attempted system call
                                         (since Linux 3.5) */
}   

       因为在不同的内核的版本中,对它的定义都不大相同,所以就在linux下man手册中找,毕竟是做嵌入式还是得站在linux上,可以看到信息非常多,但这些我们只需要打印对我们有帮助的信息好,下面是一个sigaction()函数的示例 :

#include <string.h>                                                                                                                                                                                    
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <signal.h>
void function(int SIG, siginfo_t* info,void* p){       //自定义信号处理函数;第三个参数并没有使用;
    if(info == NULL){
        printf("info is NULL\n");
        exit(EXIT_FAILURE);
    }
    printf("Signal sent by process %d\n", info->si_pid);//打印调用者的pid号;
    printf("Signal code: %d\n", info->si_code);         //打印通信编码;
}
int main()
{
    struct sigaction sa;          //这里定义了两个结构体变量;                  
    struct sigaction old_sa;      //一个是新的信号处理函数,一个用来保存旧的信号处理方式;
    memset(&sa, 0, sizeof(sa));   //初始化;
    sa.sa_sigaction = &function;  //将地址赋给函数指针;
    if(sigaction(SIGINT, &sa, &old_sa) == -1) {//调用信号处理函数;注意这里old_sa
        perror("sigaction");                   //需要用来接收旧的信号处理方式;
        exit(EXIT_FAILURE);
    }   
    pause();                       // 使程序暂停,等待信号;
    if(sigaction(SIGINT, &old_sa, NULL) == -1) {//将信号的处理方式恢复;
        perror("sigaction");
        exit(EXIT_FAILURE);
    }   
    while(1);                     //死循环,可以由SIGINT信号终止,
    return 0;    
}    

        下面是运行结果:

        简述一下这段代码,SIGINT信号是一个可以终止当前程序的信号,由用户在终端输入ctrl+c产生,在这段代码中我将SIGINT的处理方式更改为了自定义函数处理,所以进程接收到了SIGINT信号并会结束,而时执行对应的自定义信号处理函数,但代码中有一段whlie()死循环,所以不能将SIGINT一直设定为自定义,需要回复默认的处理方法,于是我们定义了old_sa,它可以将旧的处理方式保存下来,便于我们去恢复。

        从运行结果可以看到,程序在第一次受收到SIGINT信号并没有终止,而时打印出了相关信息,但在第二次接收的SIGINT信号,程序终止了,没有问题,是我们要得到的结果。

3.sigset_t sa_mask;

        结构体的第三个成员是sa_mask,sa_masksa_mask成员是一个信号集,用于指定在信号处理函数执行期间应该被临时阻塞的信号集。这是一个sigset_t类型的成员,它允许你指定一组信号。

        先看一下siget_t类型:

typedef struct {
    unsigned long sig[_NSIG_WORDS];
} sigset_t;

        是一个结构体类型的数组,在使用的过程中,我们并不需要关心如何向里面存放,因为有对应的函数供我们去使用。 

        sa_mask简单来说,是为了防止在执行信号处理函数时,有别的信号发出,打断当前操作,所以我们可以将要发生的信号设置为阻塞,使用的时候需要先进行初始化:

 sigemptyset(&sa.sa_mask);

        然后进行添加操作:

sigaddset(&sa.sa_mask, SIGINT);
sigaddset(&sa.sa_mask, SIGQUIT);

         这样在进行自定义处理函数,这两个信号就不会打断当前操作了。方式还时比较简单的。

        需要注意的自定义处理函数花费事件尽量要短,防止错过重要的信号,或着是采用其他方式来保证信号的接收。

4.int   sa_flags; 

sa_flags时结构体的第四个成员,它用于指定信号处理的行为选项。以下是一些常用的 sa_flags 标志及其作用,如下:

  • SA_RESTART:使被信号打断的系统调用自动重新发起。
  • SA_ONSTACK:系统将在调用[[sigalstack|sigalstack]]替代信号栈上运行信号句柄;否则使用用户栈来交付信号。
  • SA_NOCLDSTOP:使父进程在它的子进程暂停或继续运行时不会收到 SIGCHLD 信号。
  • SA_NOCLDWAIT:使父进程在它的子进程退出时不会收到 SIGCHLD 信号,这时子进程如果退出也不会成为僵尸进程。
  • SA_NODEFER:使对信号的屏蔽无效,即在信号处理函数执行期间仍能发出这个信号。
  • SA_RESETHAND:信号处理之后重新设置为默认的处理方式。
  • SA_SIGINFO:使用 sa_sigaction 成员而不是 sa_handler 作为信号处理函数。

         这里主要说一下SA_RESTART,这个标志位可以使被中断的系统调用,在执行完信号处理后自动重新执行,例如read,write系统调用的过程中被中断了,那么在执行完处理函数后将会自动重新发起,不需要手动在进行设置。

使用的话直接进行赋值就可以:

sa.sa_flags = SA_RESTART;

五、自定义信号处理函数的结构图 

        因为函数中涉及到了多个结构体,所以就画了一张结构图,可以帮助理解


总结

以上就是关于信号的内容,本文仅仅介绍了有关信号的相关知识和简单使用,更多的还需要自己不断摸索,不断学习。若发现错误,请指正,共同进步。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值