Linux下多任务间通信和同步-信号

时间:2021-01-14 08:07:54

Linux下多任务间通信和同步-信号

嵌入式开发交流群280352802,欢迎加入!

1.概述

信号是在软件层次上对中断机制的一种模拟,是一种异步通信方式.信号可以直接进行用户空间进程和内核进程之间的交互,内核进程也可以利用它来通知用户空间进程发生了哪些系统事件。它可以在任何时候发给某一进程,而无需知道该进程的状态.如果该进程当前并未处于执行态,则该信号就由内核保存起来,直到该进程恢复执行再传递给它;如果一个信号被进程设置为阻塞,则该信号的传递被延迟,直到其阻塞被取消时才被传递给进程.信号是进程间通信机制中惟一的异步通信机制,可以看作是异步通知,通知接收信号的进程有哪些事情发生了.信号机制经过POSIX实时扩展后,功能更加强大,除了基本通知功能外,还可以传递附加信息.

从信号发送到信号处理函数执行完毕的全过程称为信号的生命周期.对一个完整的信号生命周期来说,可以分为三个重要的处理阶段,这三个阶段由四个重要事件来刻画:信号诞生,信号在进程中注册完毕,信号在进程中的注销完毕,信号处理函数执行完毕,如下图如所示.相邻两个事件的时间间隔构成信号生命周期的一个阶段. Linux下多任务间通信和同步-信号
四个重要事件的含义如下:
  1. 信号的产生指的是触发信号的事件发生(如检测到硬件异常,定时器超时以及调用信号发送函数kill()或者sigqueue()等).
  2. 信号在进程中注册指的是使进程知道需要处理某个信号,但是还没来得及处理,或者该信号被进程阻塞,则先将该信号保存到某个链表中(如未决信号链).
  3. 信号在进程中注销是进程等待处理某个信号,且该信号没有被进程阻塞,则在运行响应的信号处理函数前,进程会把信号从未决信号链中卸除.
  4. 进程注销信号后,立即执行响应的信号处理函数,执行完毕后,信号的本次发送对进程的影响彻底结束.

进程可以通过3种方式来响应一个信号:

  • 忽略信号.即对信号不做任何处理,其中,有两个信号不能忽略:SIGKILL及SIGSTOP.
  • 捕捉信号.定义信号处理函数,当信号发生时,执行相应的处理函数.
  • 执行缺省操作.Linux对每种信号都规定了默认操作.
    • Linux下多任务间通信和同步-信号

2.信号的产生

信号事件的发生有两个来源:硬件来源(比如我们按下了键盘或者其他硬件故障);软件来源,最常用发送信号的系统函数有kill(),raise(),alarm(),setitimer()和sigqueue()等,软件来源还包括一些非法运算等操作.

3.信号发送和捕捉

3.1.信号发送:kill()和raise()

kill()函数同读者熟知的kill系统命令一样,可以发送信号给进程或进程组,它不仅可以中止进程(实际上发出SIGKILL信号),也可以向进程发送其他信号.
kill()函数语法:

Linux下多任务间通信和同步-信号
当kill函数返回-1时,同时设置errno变量:
  • 如果,失败的原因是给定的信号无效,errno将设置为EINVAL;
  • 如果是权限不足,errno将设置为SPERM;
  • 如果是指定的进程不存在,errno将被设置为ESRCH.
raise函数则允许一个进程向自身发送信号.因此,调用raise函数实际上相当于调用kill(getpid(),signo).
下面,我们通过一个加简单的例子演示kill函数的用法.
#include <signal.h>#include <stdio.h>
/*响应SIGINT信号*/
void sig_usr(int signo)
{
if(signo==SIGINT)
printf("received SIGINT\n");
kill(getpid(),SIGKILL); /*可以使用raise(SIGKILL);*/
}
/*主程序中捕捉SIGINT信号*/
int main()
{
if(signal(SIGINT,sig_usr)==SIG_ERR)
perror("error");
while(1);
return 0;
}
Linux下多任务间通信和同步-信号运行运行后,当我们按下CTRL+C后,程序调用信号处理函数输出"receive SIFINT",然后调用kill函数中止进程.

3.2.信号捕捉:alarm(),pause()

alarm()也称为闹钟函数,是专门为SIGALRM信号而设的,它可以在进程中设置一个定时器,当定时器指定的时间到时,它就向进程发送SIGALARM信号.由于每个进程只能有一个闹钟,进程调用alarm函数后,任何以前的alarm函数调用都无效.如果参数second为0,那么进程将不再包含任何闹钟.
Linux下多任务间通信和同步-信号
pause()函数是用于将调用进程挂起直至捕捉到信号为止.这个函数很常用,通常可以用于判断信号是否已到.
Linux下多任务间通信和同步-信号
下面我们看一个简单的例子.
#include <signal.h>#include <stdio.h>#include <unistd.h>#include <stdlib.h>#define MAXLINE 200static void sig_alrm(int signo){    /*I/O操作超时提示*/    printf("Time out!\n");    exit(0);}int main(void){intn;charline[MAXLINE];/*注册信号SIGALRM*/if (signal(SIGALRM, sig_alrm) == SIG_ERR)perror("signal(SIGALRM) error");/*设置超时时间为10秒*/alarm(3);if ( (n = read(STDIN_FILENO, line, MAXLINE)) < 0)perror("read error");/*取消闹钟*/alarm(0);write(STDOUT_FILENO, line, n);return 0;}
Linux下多任务间通信和同步-信号该程序从标准输入读入一行,然后将其写到标准输出上,如果不能再10s内完成读取操作,就执行sig_alrm函数,打印一行信心侯退出程序.

3.3.信号处理:signal,sigaction

如果进程要处理某一信号,那么要在进程中注册该信号.注册信号主要用来确定信号值以及进程针对该信号值得动作间的映射关系即进程将要处理哪个信号和该信号传递给进程时,将要执行何种操作.主要有两个函数:signal和sigantion.
使用signal()处理信号时,只需要指出要处理的信号和处理函数即可.它主要是用于前32种非实时信号的处理,不支持信号传递信息,但是由于使用简单,易于理解,因此也受到很多程序员的欢迎.signal()函数的语法:
Linux下多任务间通信和同步-信号
下面是一个简单的例子,用于了解signal函数的用法.需要主要的是:由signal函数注册的信号处理函数执行完毕后,对信号的响应将还原为系统默认的处理方式,因此需要重新注册信号.
#include <signal.h>#include <stdio.h>#include <unistd.h>/*对信号SIGINT的响应函数*/void sig_int(int sig){    printf("Get signal: %d\n", sig);    /*重新注册信号*/    (void) signal(SIGINT, sig_int);}int main(){    (void) signal(SIGINT, sig_int);    while(1)     {        printf("Hello World!\n");        sleep(1);    }    return 0;}
Linux下多任务间通信和同步-信号运行程序后,每隔1s在终端上打印一行"Hello World",当按下Ctrl+C时,不会导致程序的中止,而会打印出"Get signal:2",然后继续循环打印"Hello World".如果要退出程序,可以按下Ctrl+\,该组合键的作用是产生SIGQUIT信号.
从signal函数的调用中可以看出,不改变信号的处理的处理方式就不能确定信号的当前处理方式,在很多处理信号的程序中都有类似下面的代码段:
void sig_int(int);if (signal(SIGINT, SIG_IGN) != SIG_INT)signal(SIG_INT, sig_int);

当程序不知道进程的SIGINT信号是否有信号处理函数时,需要先测试是否为被忽略的处理方式,仅当信号当前未被忽略时,进程才会处理这些信号.而采用sigaction函数就可以确定一个信号的处理方式,而无需改变它. sigaction() 函数相对于signal()更加健壮,推荐使用这个. Linux下多任务间通信和同步-信号
sigaction()函数中第2个和第3个参数用到的sigaction结构
struct sigaction { void (*sa_handler)(int signo); sigset_t sa_mask; int sa_flags; void (*sa_restore)(void); }
  • sa_handler是一个函数指针,指定信号处理函数,这里除可以是用户自定义的处理函数外,还可以为SIG_DFL(采用缺省的处理方式)或SIG_IGN(忽略信号).它的处理函数只有一个参数,即信号值.
  • sa_mask是一个信号集,它可以指定在信号处理程序执行过程中哪些信号应当被屏蔽,在调用信号捕获函数之前,该信号集要加入到信号的信号屏蔽字中.
  • sa_flags中包含了许多标志位,是对信号进行处理的各个选择项.
注意:信号屏蔽字:每个进程都有一个信号屏蔽字,它规定了当前要阻塞传递到干进程的信号集,对于每种可能的信号,该屏蔽字中都有一位与之对应.对于某信号,若其对应的位已经置位,则它当前是阻塞的.
sa_flags字段常见取值如下:
  • SA_NODEFER:一般情况下,当信号处理函数运行时,内核将阻塞该给定信号.但是如果设置了SA_NODEFER标记,那么在该信号处理函数运行时,内核将不会阻塞该信号.SA_NODEFER是这个标记的正式的POSIX名字(还有一个名字SA_NOMASK,为了软件的可移植性,一般不用这个名字).
  • SA_RESETHAND:当调用信号处理函数时,将信号的处理函数重置为缺省值.SA_RESETHAND是这个标记的正式的POSIX名字(还有一个名字SA_ONESHOT,为了软件的可移植性,一般不用这个名字).
  • SA_NOCLDSTOP:当子进程中止时不产生SIGCHLD信号.
  • SA_RESTART:由此信号中断的系统调用会自动启动.
与signal函数略有不同,由sigaction函数设置的信号处理函数所捕获的信号在默认情况下不会被重置,除非在sa_flags中设置了SA_RESETHAND.下面是一个简单的例子.
#include <signal.h>#include <stdio.h>#include <unistd.h>/*对信号SIGINT的响应函数*/void sig_int(int sig){    printf("Get signal: %d\n", sig);}int main(){    /*初始化sigaction 结构*/    struct sigaction act;    act.sa_handler = sig_int;    sigemptyset(&act.sa_mask);    act.sa_flags = 0;    sigaction(SIGINT, &act, 0);    while(1) {        printf("Hello World!\n");        sleep(1);    }}
该函数的执行结果和上一个例子的结果完全一致,不同之处在于信号处理函数只需注册一次即可,不用像signal函数那样重复注册.
另外,为了配合sigaction函数处理带有参数的信号,需要有可以发送带有参数的信号的函数.这个函数就是sigqueue().函数原型如下:
#include<signal.h>int sigqueue(pid_t pid,int sig,const union sigval value);
pid是目标进程的进程号
sig是信号代号
value参数是一个联合体,表示信号附带的数据,附带数据可以是一个整数也可以是一个指针,有如下形式:

union sigval{int sival_int;void *sival_ptr;//指向要传递的信号参数};value


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

4.信号集

信号集用来描述信号的集合,Linux所支持的所有信号可以全部或部分的出现在信号集中,主要与信号阻塞相关函数配合使用.使用信号集函数组处理信号时涉及一系列的函数,这些函数按照调用的先后次序可分为以下几大功能模块:创建信号集合,注册信号处理函数以及检测信号.
其中,创建信号集合主要用于处理用户感兴趣的一些信号,其函数包括以下几个:
  • sigemptyset():将信号集合初始化为空.
  • sigfillset():将信号集合初始化为包含所有已定义的信号的集合.
  • sigaddset():将指定信号加入到信号集合中去.
  • sigdelset():将指定信号从信号集合中删去.
  • sigismember():查询指定信号是否在信号集合之中.
它们在成功时都会返回0,失败时都会返回-1,并设置errno(只定义了一个错误,那就是在给定信号不合法时的EINVAL).
每个进程都会有一个信号屏蔽字,用来描述哪些信号递送到进程时将被阻塞的信号集,该信号集中的所有信号递送到进程后将被阻塞.下面是和信号阻塞相关的几个函数:
#include <signal.h>int  sigprocmask(int  how,  const  sigset_t *set, sigset_t *oldset));int sigpending(sigset_t *set));int sigsuspend(const sigset_t *mask));
  • sigprocmask()函数能够根据参数how来实现对信号集的操作,操作主要有三种:
    • SIG_BLOCK:在进程当前阻塞信号集中添加set指向信号集中的信号;
    • SIG_UNBLOCK:如果进程阻塞信号集中包含set指向信号集中的信号,则解除对该信号的阻塞;
    • SIG_SETMASK:更新进程阻塞信号集为set指向的信号集.
  • sigpending(sigset_t *set))获得当前已递送到进程,却被阻塞的所有信号,在set指向的信号集中返回结果.
  • sigsuspend(const sigset_t *mask))用于在接收到某个信号之前,临时用mask替换进程的信号掩码,并暂停进程执行,直到收到信号为止.sigsuspend返回后将恢复调用之前的信号掩码.信号处理函数完成后,进程将继续执行.该系统调用始终返回-1,并将errno设置为EINTR.
下面我们通过一个例子来了解上述函数的用法.
#include <signal.h>#include <stdio.h>#include <stdlib.h>static void sig_quit(int signo){printf("caught SIGQUIT\n");if (signal(SIGQUIT, SIG_DFL) == SIG_ERR)perror("can't reset SIGQUIT");}static void sig_int(int signo){printf("caught SIGINT\n");if (signal(SIGINT, SIG_DFL) == SIG_ERR)perror("can't reset SIGINT");}int main(void){sigset_tnewmask, oldmask, pendmask;if (signal(SIGQUIT, sig_quit) == SIG_ERR)perror("can't catch SIGQUIT");if (signal(SIGINT, sig_int) == SIG_ERR)perror("can't catch SIGINT");sigemptyset(&newmask);/* 添加信号SIG_BLOCK和SIGINT至信号集*/sigaddset(&newmask, SIGQUIT);sigaddset(&newmask, SIGINT);/* 设置为屏蔽这两个信号并保存当前的信号屏蔽字 */if (sigprocmask(SIG_BLOCK, &newmask, &oldmask) < 0)perror("SIG_BLOCK error");sleep(5);/*进程休眠期间将阻塞的信号放置信号阻塞队列中*/if (sigpending(&pendmask) < 0)perror("sigpending error");    /*处理信号阻塞队列*/if (sigismember(&pendmask, SIGQUIT))printf("SIGQUIT pending\n");if (sigismember(&pendmask, SIGINT))printf("SIGINT pending\n");/* 恢复最初的信号屏蔽字 */if (sigprocmask(SIG_SETMASK, &oldmask, NULL) < 0)perror("SIG_SETMASK error");fprintf(stderr,"SIGNAL unblocked\n");sleep(5);exit(0);}
Linux下多任务间通信和同步-信号
该程序运行的前5s屏蔽了SIGINT和SIGQUIT信号,这时无论是Ctrl+C还是Ctrl+\都会被阻塞.在后5s恢复了SIGQUIT和SIGINT信号的默认处理.如果信号阻塞队列不是空的,就会根据阻塞期间被阻塞的SIGQUIT和SIGINT信号,调用相应的信号处理函数.
总之,在处理信号时,一般遵循下图所示的操作流程.
Linux下多任务间通信和同步-信号