【操作系统】进程间的通信——信号

11

进程间通信-信号

信号

什么是信号?

信号是给程序提供一种可以处理异步事件的方法,它利用软件中断来实现。

我们无法自定义信号,所有信号都是系统预定义的。


信号由谁产生?

  1. 由shell终端根据当前发生的错误(例如:段错误、非法指令等),Ctrl+C而产生相应的信号。

比如: socket通信或管道通信,如果读端已经关闭,再执行写操作(或者进行发送数据),将导致执行写操作的进程收到SIGPIPE信号(表示管道破裂)。

该信号的默认行为——终止该进程。

  1. 在shell终端,使用kill或killall命令产生信号

示例:

kill -HUP pid # 向PID为pid的进程发送SIGHUP信号
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
void myhandle(int sig) {
    printf("Catch a signal : %d\n", sig);
}
int main(void) {
    //设置信号处理函数
    signal(SIGINT, myhandle);//如果收到SIGINT信号,就执行myhandle,如果未设置信号处理函数,则进行默认处理——终止。
    while (1) {
            sleep(1);
    }
    return 0;
}
  1. 在代码中调用kill函数来产生信号——详见-kill()函数 Unix/Linux
int kill(pid_t pid, int sig); //进程pid,信号类型

有哪些信号?

信号名称 & 说明


​ SIGABORT—— 进程异常终止

​ SIGALRM ——超时告警

​ SIGFPE —— 浮点运算异常

​ SIGHUP ——连接挂断

​ SIGILL——非法指令

​ SIGINT ——终端中断 (Ctrl+C将产生该信号)

​ SIGKILL ——*终止进程

​ SIGPIPE ——向没有读进程的管道写数据

​ SIGQUIT——终端退出(Ctrl+\将产生该信号)

​ SIGSEGV ——无效内存段访问

​ SIGTERM ——终止

​ SIGUSR1——*用户自定义信号1

​ SIGUSR2 ——*用户自定义信号2

​ -------------------------------------->以上信号如果不被捕获,则进程接受到后都会终止!

​ SIGCHLD——子进程已停止或退出

​ SIGCONT ——*让暂停的进程继续执行

​ SIGSTOP ——*停止执行(即“暂停")

​ SIGTSTP——断挂起

​ SIGTTIN —— 后台进程尝试读操作

​ SIGTTOU——后台进程尝试写


信号的处理

  • 忽略此信号。
  • 捕捉信号,指定信号处理函数进行处理。详见信号的安装
  • 执行系统默认动作,大多数都是终止进程。

信号的捕获

信号的捕获是指,在接收到某种信号后,去执行指定的函数。

注意:

  • SIGKILL和SIGSTOP信号不能被捕获,即这两种信号的相应动作不能被改变。

信号的安装

signal

 sighandler_t signal(int signum, sighandler_t handler);
  • 其中:signal函数的返回类型与第二个参数都是函数指针类型。
  • 第二个参数中:有以下两个特殊值。
  • SIG_IGN——忽略信号
  • SIG_DFL—— 恢复默认行为

示例:

signal(SIGINT, myhandle);//myhandle为自定义信号处理函数

sigaction

与signal相比,sigaction更加健壮。

结构体sigaction:

 struct sigaction {
              //信号的响应函数
               void     (*sa_handler)(int);
              //略...
               void     (*sa_sigaction)(int, siginfo_t *, void *);
              //搁置信号集
               sigset_t   sa_mask;
              //当sa_flags中包含 SA_RESETHAND时,接收到该信号并调用指定的信号处理函数执行之后,把该信号的响应行为重置为默认行为SIG_DFL 
              //即自定义的信号处理函数执行一次后,回到默认处理方式。
               int        sa_flags;
              //略...
               void     (*sa_restorer)(void);
 };

补充:

  • 当sa_mask包含某个信号A时,在信号处理函数执行期间,如果发生了该信号A,则阻塞该信号A,即暂时不响应该信号,直到信号处理函数执行结束,再响应该信号A。

函数sigaction:

int sigaction(int signum, 
              const struct sigaction *act,
              struct sigaction *oldact//保存旧设置
        );

示例1:使用示例

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

void myhandle(int sig) {
    printf("Catch a signal : %d\n", sig);
}

int main(void) {
    struct sigaction act;
    act.sa_handler = myhandle;
    sigemptyset(&act.sa_mask);//清空搁置集
    act.sa_flags = 0;

    sigaction(SIGINT, &act, 0);
    while (1) {
        sleep(1);
        printf("sleep 1 second.\n");
    }
    return 0;
} 

示例2: 输入A主进程向子进程发送SIGUSR1信号,输出大写字符;输入a主进程向子进程发送SIGUSR2信号,输出小写字符。

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

int workflag = 0;

void work_up_handle(int sig) {
    workflag = 1;
}

void work_down_handle(int sig) {
    workflag = 0;
}

/*
  看起来貌似一个sigaction结构体对象可以设置多个信号与信号处理函数对。
  即,根据接收到的信号来到调用对应的信号处理函数。
*/

int main(void) {
    pid_t pd;
    char c;

    pd = fork();
    if (pd == -1) {
        printf("fork error!\n");
        exit(1);
    } else if (pd == 0) {//子进程
        char *msg;
        struct sigaction act; 
        act.sa_flags = 0;

        act.sa_handler = work_up_handle;//结构体设置信号处理函数
        sigemptyset(&act.sa_mask);      
        sigaction(SIGUSR1, &act, 0);

        act.sa_handler = work_down_handle;//结构体设置信号处理函数
        sigaction(SIGUSR2, &act, 0);

        while (1) {
            if (!workflag) {
                msg = "child process work!";
            } else {
                msg = "CHILD PROCESS WORK!";
            }
            printf("%s\n", msg);
            sleep(1);
        }   
    } else {//父进程
        while(1) {
            c = getchar();
            if (c == 'A') {
                kill(pd, SIGUSR1);//给子进程pd发送信号 SIGUSR1
            } else if (c == 'a') {
                kill(pd, SIGUSR2);//给子进程pd发送信号 SIGUSR2
            }
        }
    }   
    return 0;
}

示例3:使用子进程定时给父进程发送SIGALRM信号。

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

int wakeflag = 0;

void wake_handle(int sig) {
    wakeflag = 1;
}

int main(void) {
    pid_t pd;
    char c;

    pd = fork();
    if (pd == -1) {
        printf("fork error!\n");
        exit(1);
    } else if (pd == 0) {
        for(;;){
            sleep(5);
            //getppid()获取父进程id
            kill(getppid(), SIGALRM);//给父进程发送SIGALRM信号
        }
    } else {
        struct sigaction act; 
        act.sa_handler = wake_handle;
        act.sa_flags = 0;
        sigemptyset(&act.sa_mask);//清空
        sigaction(SIGALRM,  &act, 0);//设置信号响应

        for(;;){
            pause(); //把该进程挂起,直到收到任意一个信号

            if (wakeflag) {
                printf("Alarm clock work!!!\n");
            }
        }
    }

    return 0;
}

image-20220820231717052


使用alarm函数

  • 作用:在指定时间之内给该进程本身发送一个SIGALRM信号。指的是在调用这行代码之后,经过你定时的几秒钟后,发送SIGALRM信号。

  • 函数原型

 unsigned int alarm(unsigned int seconds);
  • 注意:

  • 时间的单位是“秒”

  • 实际闹钟时间比指定的时间要大一点。

  • 如果参数为0,则取消已设置的闹钟。

  • 如果闹钟时间还没有到,再次调用alarm,则闹钟将重新定时

  • 每个进程最多只能使用一个闹钟。

  • 返回值:

  • 失败:返回-1。

  • 成功:返回上次闹钟的剩余时间(秒)。

  • 示例:

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

int wakeflag = 0;

void wake_handle(int sig) 
{
    wakeflag = 1;
}

int main(void) {
    int ret;

    struct sigaction act;
    act.sa_flags = 0;
    act.sa_handler = wake_handle;//设置响应函数
    sigemptyset(&act.sa_mask);
    sigaction(SIGALRM, &act, 0);//设置信号-响应

    printf("time =%ld\n", time((time_t*)0));

    ret = alarm(5);
    if (ret == -1) {
        printf("alarm error!\n");
        exit(1);
    }

    for(;;){
        //挂起当前进程,直到收到任意一个信号
        pause();

        if (wakeflag) {
            printf("wake up, time =%ld\n", time((time_t*)0));
        }
    }

    return 0;
}

使用raise函数

  • 作用:给本进程自身发送某一指定信号。调用后立即发送。

  • 函数原型:int raise (int sig)

  • 示例:略。


发送多个信号

前提对应信号已绑定对应的信号处理函数。详见信号的处理。

某进程在执行某个信号对应的操作函数期间(即,该对应信号的安装函数),如果此时,该进程又多次收到同一个信号(同一种信号值的信号)。

  • 注意:
  • 如果该信号是不可靠信号(值<32),则只能再响应一次。
  • 如果该信号是可靠信号(>32),则能再响应多次(不会遗漏)。但是都必须等待该次响应函数执行完毕后,才能响应下一次。

某个进程正在执行某个信号对应的操作函数期间(该信号的安装函数),如果此时,该进程又收到一个信号(不同信号值的信号),则:

如果,该信号被包含在当前信号的signaction的sa_mask(信号搁置集)中,则不会立即处理该信号。直到当前的信号处理函数执行完毕后,才进行执行。

反之,如果该信号不在信号搁置集中,则中断当前信号处理函数,如果处于睡眠,比如sleep, 也会立即被唤醒,来执行新的这个信号处理函数,新的这个信号处理函数执行完毕后,再在返回至原来的信号处理函数继续执行。

示例:

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

void myhandle(int sig) 
{
    printf("Catch a signal : %d\n", sig);
    int i;
    for (i=0; i<10; i++) {
        sleep(1);
}
    printf("Catch end.%d\n", sig);
}

int main(void) 
{
    struct sigaction act, act2;

    act.sa_handler = myhandle; 
    sigemptyset(&act.sa_mask);
    sigaddset(&act.sa_mask, SIGUSR1);//将信号SIGUSR1加入到信号搁置集中
    act.sa_flags = 0;
    sigaction(SIGINT, &act, 0);//处理SIGINT停止信号
    //收到SIGINT信号,如果此时再收到SIGUSR1,则会执行完后在进行SIGUSR1信号处理。

    act2.sa_handler = myhandle;
    sigemptyset(&act2.sa_mask);
    act2.sa_flags = 0;
    sigaction(SIGUSR1, &act, 0);//处理SIGUSR1信号

    while (1) {
    }
    return 0;
}

以上详解:

  • 先收到SIGINT信号,如果此时再收到SIGUSR1,则会执行完SIGINT的信号处理函数后再进行SIGUSR1信号处理,因为SIGUSR1被加入到了响应SIGINT信号的信号搁置集中。结果如下图所示:

image-20220821131026619

  • 先收到SIGUSR1信号,如果此时再收到SIGINT,则会先执行SIGINT的信号处理函数后再回来继续进行SIGUSR1信号处理函数的执行。结果如下图所示:

image-20220821131412605


信号集sigset_t

什么是信号集?

例如:结构体sigaction中的参数——sigset_t sa_mask,这个sa_mask为信号搁置集。

  • 用sigset_t类型表示,实质是一个无符号长整形。

  • 用来表示包含多个信号的集合。


信号集的基本操作

  • sigemptyset——把信号集清空。
  • sigfillset——把所有已定义的信号填充到指定信号集。
  • sigdelset——从指定的信号集中删除指定的信号。
  • sigaddset——从指定的信号集中添加指定的信号。
  • sigismember——判断指定的信号是否在指定的信号集中。
    • 如果是, 返回 1。
    • 如果不是, 返回 0。
    • 信号无效, 返回-1。

进程的"信号屏蔽字"

什么是信号屏蔽字?

  • 进程的"信号屏蔽字"是一个信号集,
  • 向目标进程发送某信号时,如果这个信号在目标进程的信号屏蔽字中,则目标进程将不会捕获到该信号,即不会执行该信号的信号处理函数。
  • 当该进程的信号屏蔽字不再包含该信号时,则会捕获这个早已收到的信号(执行对应的信号处理函数)。

如何修改进程的信号屏蔽字?

  • 使用sigprocmask

  • 函数原型:

 int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
  • 参数列表:

    • how:
    • SIG_BLOCK——把参数set中的信号添加到信号屏蔽字中。在原来已有的基础上增加。
    • SIG_UNBLOCK——把参数set中的信号从信号屏蔽字中删除。在原来已经有的基础上删除。
    • SIG_SETMASK——把参数set中的信号设置为信号屏蔽字。原来的不生效了,新添加进的生效。
    • oldset:
    • 返回原来的信号屏蔽字,在set之前设置的,即原有的(之前设置的)信号屏蔽字。
  • 示例:

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

void myhandle(int sig) {
    printf("Catch a signal : %d\n", sig);
    printf("Catch end.%d\n", sig);
}

int main(void) {
    struct sigaction act, act2;

    act.sa_handler = myhandle;
    sigemptyset(&act.sa_mask);
    act.sa_flags = 0;
    sigaction(SIGINT, &act, 0);

    sigset_t proc_sig_msk;//当前的信号屏蔽字
    sigset_t old_mask;//旧的信号屏蔽字
    sigemptyset(&proc_sig_msk);//清空信号集
    sigaddset(&proc_sig_msk, SIGINT);//添加信号到信号屏蔽集中

    sigprocmask(SIG_BLOCK, &proc_sig_msk, &old_mask);//修改进程的信号屏蔽字,以增加的方式。
    sleep(5);
    printf("had delete SIGINT from process sig mask\n");
    sigprocmask(SIG_UNBLOCK, &proc_sig_msk, &old_mask);//删除proc_sig_msk中的信号

    while (1) { 
    }

    return 0;
}

获取未处理的信号

  • 当收到信号屏蔽字中的信号时,这些信号不会被该进程响应。

  • 可通过sigpending函数获取这些没有被处理的信号。

  • 函数原型:

int sigpending(sigset_t *set);
  • 返回值:
    • 成功:返回0。
    • 失败:返回-1。

阻塞式等待信号

pause

  • pause——阻塞进程,直到收到任意信号后解除阻塞。
    • 使用:pause()。
    • 注意:这里指的任意信号不能是在该进程的信号屏蔽集中的。并且,还要注意的是,如果该信号没有被屏蔽,并且没有被捕获(安装——安装即将进程中对应的信号绑定信号处理函数。),部分信号收到后会导致进程终止,详情见上面的有哪些信号

sigsuspend

  • sigsuspend——用指定的参数替换当前进程的屏蔽字,收到此信号屏蔽字内的信号无效,即阻塞。收到此信号屏蔽字之外的信号,解除阻塞,进行对应的响应。注意具体响应内容同上面的pause中的注意。

  • 示例:

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

void myhandle(int sig) {
    printf("Catch a signal : %d\n", sig);
    printf("Catch end.%d\n", sig);
}

int main(void) {
    struct sigaction act, act2;

    act.sa_handler = myhandle;
    sigemptyset(&act.sa_mask);
    act.sa_flags = 0;
    sigaction(SIGUSR1, &act, 0);//给当前进程安装SIGUSR1信号

    sigset_t proc_sig_msk;//当前的信号屏蔽字
    sigset_t old_mask;//旧的信号屏蔽字
    sigemptyset(&proc_sig_msk);//清空信号集
    sigaddset(&proc_sig_msk, SIGUSR2);//添加信号SIGUSR2到信号屏蔽集中
    sigprocmask(SIG_BLOCK, &proc_sig_msk, &old_mask);//修改进程的信号屏蔽字,以增加的方式。

    sigset_t tmp_sig_msk;//临时的信号屏蔽字——用于sigsuspend函数
    sigaddset(&tmp_sig_msk,SIGUSR1);
    while (1) { 
        printf("waitting...\n");
        sigsuspend(&tmp_sig_msk);//此时,当前进程无法响应SIGUSR1中
        printf("running...\n");
    }

    return 0;
}