进程间通信----信号

来源:互联网 发布:深圳市速度云网络 编辑:程序博客网 时间:2024/04/19 15:19

一、信号的介绍


信号是在软件层次上对中断机制的一种模拟,是一种异步通信方式。

信号可以直接进行用户空间进程和内核进程之间的交互,内核进程也可以利用它来通知用户空间进程发生了那些系统事件。

如果该进程当前并未处于执行态,则该信号就由内核保存起来,直到该进程恢复执行再传递个它;如果一个信号被进程设置为阻塞,则该信号的传递被延迟,直到其阻塞取消时才被传递给进程。

unix系统信号:

产生信号:

• 当用户按某些终端键时,产生信号。在终端上按DELETE键通常产生中断信号(SIGINT)。这是停止一个已失去控制程序的方法。
• 硬件异常产生信号:除数为0、无效的存储访问等等。这些条件通常由硬件检测到,并将其通知内核。然后内核为该条件发生时正在运行的进程产生适当的信号。例如,对执行一个无效存储访问的进程产生一个SIGSEGV。
• 进程用kill( 2 )函数可将信号发送给另一个进程或进程组。自然,有些限制:接收信号进程和发送信号进程的所有者必须相同,或发送信号进程的所有者必须是超级用户。
• 用户可用kill( 1 )命令将信号发送给其他进程。此程序是k i l l函数的界面。常用此命令终止一个失控的后台进程。
• 当检测到某种软件条件已经发生,并将其通知有关进程时也产生信号。这里并不是指硬件产生条件(如被0除),而是软件条件。例如SIGURG (在网络连接上传来非规定波特率的数据)、SIGPIPE (在管道的读进程已终止后一个进程写此管道),以及SIGALRM (进程所设置的闹钟时间已经超时)。

信号是异步事件的经典实例。产生信号的事件对进程而言是随机出现的。进程不能只是测试一个变量(例如errno)来判别是否发生了一个信号,而是必须告诉内核“在此信号发生时,请执行下列操作”。

可以要求系统在某个信号出现时按照下列三种方式中的一种进行操作。

(1) 忽略此信号。大多数信号都可使用这种方式进行处理,但有两种信号却决不能被忽略。它们是:SIGKILL和SIGSTOP。这两种信号不能被忽略的原因是:它们向超级用户提供一种使进程终止或停止的可靠方法。另外,如果忽略某些由硬件异常产生的信号(例如非法存储访问或除以0),则进程的行为是未定义的。

(2) 捕捉信号。为了做到这一点要通知内核在某种信号发生时,调用一个用户函数。在用户函数中,可执行用户希望对这种事件进行的处理。例如,若编写一个命令解释器,当用户用键盘产生中断信号时,很可能希望返回到程序的主循环,终止系统正在为该用户执行的命令。如果捕捉到SIGCHLD信号,则表示子进程已经终止,所以此信号的捕捉函数可以调用waitpid以取得该子进程的进程I D以及它的终止状态。又例如,如果进程创建了临时文件,那么可能要为SIGTERM信号编写一个信号捕捉函数以清除临时文件(kill命令传送的系统默认信号是终止信号)。

(3) 执行系统默认动作。表10-1给出了对每一种信号的系统默认动作。注意,对大多数信号的系统默认动作是终止该进程。

二、signal


#include  <signal.h>void  (*signal(int signo, void (*func)(int ))) (int);//返回值:若成功,返回以前的信号处理配置;若出错,返回SIG_ERR
signo参数是表10-1中的信号名。func的值是:(a)常数SIGIGN,或( b )常数SIGDFL,或( c )当接到此信号后要调用的函数的地址。如果指定SIGIGN,则向内核表示忽略此信号。(记住有两个信号SIGKILL和SIGSTOP不能忽略。)如果指定SIGDFL,则表示接到此信号后的动作是系统默认动作(见表10-1中的最后1列)。当指定函数地址时,我们称此为捕捉此信号。我们称此函数为信号处理程序(signal handler)或信号捕捉函数(signal-catching function)。
signal函数的原型说明此函数要求两个参数,返回一个函数指针,而该指针所指向的函数无返回值( void )。第一个参数signo是一个整型数,第二个参数是函数指针,它所指向的函数需要一个整型参数,无返回值。用一般语言来描述也就是要向信号处理程序传送一个整型参数,而它却无返回值。当调用signal设置信号处理程序时,第二个参数是指向该函数(也就是信号处理程序)的指针。signal的返回值则是指向以前的信号处理程序的指针。
#include <unistd.h>#include <stdio.h>#include <signal.h>static void sig_usr(int);int main(int argc, char *argv[]){if(signal(SIGUSR1, sig_usr) == SIG_ERR)//安装信号处理,指示该信号到来时执行的操作printf("can't catch SIGUSR1");//同时判断信号是否时SIGUSR1if(signal(SIGUSR2, sig_usr) == SIG_ERR)//安装信号处理,判断信号是否为SIGUSR2printf("can't catch SIGUSR2");for( ; ;)//等待信号到来pause();}static void sig_usr(int signo)//信号处理函数{if(signo == SIGUSR1)printf("received SIGUSR1\n");else if(signo == SIGUSR2)printf("received SIGUSR2\n");elseprintf("received signal %d\n", signo);}


#include  <signal.h>int  kill(pid_t  pid,  int signo);int  raise(int  signo);//两个函数返回值:若成功,返回0;若出错,返回-1

kill函数将信号发送给进程或进程组。raise函数则允许进程向自身发送信号。

kill的pid参数有四种不同的情况:
• pid > 0 将信号发送给进程ID为pid的进程。
• pid == 0 将信号发送给其进程组ID等于发送进程的进程组ID,而且发送进程有许可权向其发送信号的所有进程。这里用的术语“所有进程”不包括实现定义的系统进程集。对于大多数U N I X系统,系统进程集包括:交换进程(pid 0),init (pid 1)以及页精灵进程(pid 2)。
• pid < 0 将信号发送给其进程组ID等于pid绝对值,而且发送进程有许可权向其发送信号的所有进程。如上所述一样,“所有进程”并不包括系统进程集中的进程。
• pid == -1       POSIX.1未定义此种情况。

进程将信号发送给其他进程需要许可权。超级用户可将信号发送给另一个进程。对于非超级用户,其基本规则是发送者的实际或有效用户ID必须等于接收者的实际或有效用户ID。在对许可权进行测试时也有一个特例:如果被发送的信号是SIGCONT,则进程可将它发送给属于同一对话期的任一其他进程。

POSIX.1将信号编号0定义为空信号。如果signo参数是0,则kill仍执行正常的错误检查,但不发送信号。这常被用来确定一个特定进程是否仍旧存在。

#include <unistd.h>unsigned  int  alarm(unsigned int  seconds) ;//返回:0或以前设置的闹钟时间的余留秒数

使用alarm函数可以设置一个时间值(闹钟时间),在将来的某个时刻该时间值会被超过。当所设置的时间值被超过后,产生SIGALRM信号。如果不忽略或不捕捉此信号,则其默认动作是终止该进程。

seconds的值是产生信号SIGALRM需要经过的时钟描述。当这一时刻到达时,信号由内核产生,由于进程调度的延迟,所以进程得到控制从而能够处理该信号还需要一个时间间隔。

每个进程只能有一个闹钟时间。如果在调用alarm时,以前已为该进程设置过闹钟时间,而且它还没有超时,则该闹钟时间的余留值作为本次alarm函数调用的值返回。以前登记的闹钟时间则被新值代换。
如果有以前登记的尚未超过的闹钟时间,而且seconds值是0,则取消以前的闹钟时间,其余留值仍作为函数的返回值。

虽然SIGALRM的默认动作是终止进程,但是大多数使用闹钟的进程捕捉此信号。如果此时进程要终止,则在终止之前它可以执行所需的清除操作。

pause函数使调用进程挂起直至捕捉到一个信号。
#include <unistd.h>int pause(void);返回:-1,errno设置为EINTR

只有执行了一个信号处理程序并从其返回时, pause才返回。在这种情况下, pause返回-1,errno设置为EINTR。


三、信号集与屏蔽信号


//come from /usr/include/bits/sigset.htypedef _sigset_t  sigset_t#define  _SIGSET_NWORDS  (1024/( 8 * sizeof(unsigned long int)))typedef struct{        unsigned long int  __val[_SIGSET_NWORDS];.} _sigset_t//此结构体占据32*32=1024bit,每bit对应一个信号,val[0]的0~31位对应常用书中的信号
Linux提供了大量信号集操作函数,包括添加、删除以及测试等操作。
#include <signal.h>int sigemptyset(sigset_st et)* ;int sigfillset(sigset_st et)* ;int sigaddset(sigset_ts et,* int signo) ;int sigdelset(sigset_ts et,* int signo) ;//四个函数返回:若成功则为0,若出错则为-1int sigismember(const sigset_t *set , int signo) ;//返回:若真则为1,若假则为0
(1)清空信号集。函数sigemptyset()用来清空信号集,即没有任何信号在此集合中。
(2)完全填空信号集。函数sigfillset()用来完全填空信号集,即阻塞所有信号,但SIGKILL和SIGSTOP信号时无法阻塞的。
(3)添加信号到信号集中。函数死gaddset()用来将某一信号添加信号集。
(4)从信号集中删除某个信号。函数sigdelset()用来将某一个信号从信号集中删除。
(5)sigismember函数用来检测信号是否在信号集中。

#include <stdio.h>#include <signal.h>#include <stdlib.h>int output(sigset_t set);int main(int argc, char *argv[]){sigset_t set;printf("after empty the set: \n");sigemptyset(&set);//置空时查看值output(set);printf("after add signo = 2: \n");sigaddset(&set, 2);//将sig=2的信号添加到集合查看output(set);printf("after add signo = 10: \n");sigaddset(&set, 10);       //将sig=10的信号添加到集合查看output(set);sigfillset(&set);printf("after fill all: \n");output(set);return 0;}int output(sigset_t set){int i = 0;for (i = 0; i < 1; i++)//此时只查看第一个变量{printf("0x%8x\n", set.__val[i]);if ((i+1)%8 == 0)printf("\n");}}


在进程执行过程中,可以调用sigpending()函数返回当前进程阻塞的集合,或者调用sigprocmask()设置或获取当前进程阻塞的信号集合。

#include <signal.h>int  sigpending(sigset_t  *set);返回值:若成功,返回0;若出错,返回-1

sigpending函数返回一信号,对于调用进程而言,其中的各信号时阻塞不能递送的,因而也一定是当前未决的。该信号集通过set参数返回。

一个进程的信号屏蔽字规定了当前阻塞而不能递送给该进程的信号集。调用函数sigprocmask可以检测或更改(或两者)进程的信号屏蔽字。
# include <signal.h>int sigprocmask(int how, const sigset_t   *restrict set, sigset_t *restrict oset) ;//返回:若成功则为0,若出错则为-1

首先, oset是非空指针,进程的当前信号屏蔽字通过oset返回。其次,若set是一个非空指针,则参数how指示如何修改当前信号屏蔽字。
如果set是个空指针,则不改变该进程的信号屏蔽字, how的值也无意义。
如果在调用sigprocmask后有任何未决的、不再阻塞的信号,则在sigprocmask返回前,至少将其中之一递送给该进程。


#include <stdio.h>#include <signal.h>#include <stdlib.h>static void sig_quit(int);int main(int argc, char *argv[]){sigset_t newmask, oldmask, pendmask;if (signal(SIGQUIT, sig_quit) == SIG_ERR)//安装信号处理函数{perror("signal");exit(EXIT_FAILURE);}printf("install sig_quit\n");sigemptyset(&newmask);//清除所有信号集体sigaddset(&newmask, SIGQUIT);//添加SIGQUIT到信号集体中if (sigprocmask(SIG_BLOCK, &newmask, &oldmask) < 0){//设置进程阻塞newmask, 原来值读到oldmaskperror("signalmask");exit(EXIT_FAILURE);}printf("Block SIGQUIT, wait 15 second\n");sleep(15);//等待15sif (sigpending(&pendmask) < 0)//保存阻塞信号{perror("sigpending");exit(EXIT_FAILURE);}if (sigismember(&pendmask, SIGQUIT))//检测SIGQUIT是否在信号集中printf("\nSIGQUIT pending \n");if (sigprocmask(SIG_SETMASK, &oldmask, NULL) < 0){//替换进程掩码,即清除阻塞SIGQUITperror("sigprocmask");exit(EXIT_FAILURE);}printf("SIGQUIT unblocked\n");sleep(15);return 0;}static void sig_quit(int signo)//信号处理函数{printf("caught SIGQUIT, the process will quit\n");if (signal(SIGQUIT, SIG_DFL) == SIG_ERR)//再次安装{perror("signal");exit(EXIT_FAILURE);}}


四、安装信号


sigaction函数的功能是检查或修改(或两者)与指定信号相关联的处理动作。此函数取代了UNIX早期版本使用的signal函数。
#include <signal.h>int sigaction(int signo, const struct sigactioan *restrict act, struct sigaction  *restrict oact) ;//返回:若成功则为0,若出错则为- 1

参数signo是要检测或修改具体动作的信号的编号数。若act指针非空,则要修改其动作。如果oact指针非空,则系统返回该信号的原先动作。
struct sigaction {void (*sa_handler)();                             /* addr of signal handler,or SIG_IGN, or SIG_DFL */sigset_t sa_mask;                                 /* additional signals to block */int sa_flags;                                     /* signal options, Table 10-5 */void  (*sa_sigaction)(int, siginfo_t *, void *)};
sa_mask是一个信号集,用于表示在执行信号捕捉函数时,添加到进程阻塞信号集中的信号集(即阻塞的信号集)。但不会将SIGKILL和SIGSTOP信号添加到进程阻塞信号集中,此限制将由系统强制执行,并且不会导致需要指出的错误。
当更改信号动作时,如果sahandler指向一个信号捕捉函数(不是常数SIGIGN或SIGDFL ),则samask字段说明了一个信号集,在调用信号捕捉函数之前,该信号集要加到进程的信号屏蔽字中。仅当从信号捕捉函数返回时再将进程的信号屏蔽字恢复为原先值。这样,在调用信号处理程序时就能阻塞某些信号。在信号处理程序被调用时,系统建立的新信号屏蔽字会自动包括正被递送的信号。因此保证了在处理一个给定的信号时,如果这种信号再次发生,那么它会被阻塞到对前一个信号的处理结束为止。若同一种信号多次发生,通常并不将它们排队,所以如果在某种信号被阻塞时,它发生了五次,那么对这种信号解除阻塞后,其信号处理函数通常只会被调用一次。sa_flags可用于更改指定信号的行为。
#include <stdio.h>#include <signal.h>#include <stdlib.h>void myHandler(int sig);int main(int argc, char *argv[]){struct sigaction act, oact;act.sa_handler = myHandler;sigemptyset(&act.sa_mask);act.sa_flags = 0;sigaction(SIGUSR1, &act, &oact);printf("1\n");while(1){printf("Hello world.\n");pause();}}void myHandler(int sig){printf("I got signal: %d.\n", sig);}



sa_handler或sa_sigaction标识要与指定信号关联的操作,两者一般取其一即可。
如果结构体struct sigaction成员sa_flags设置为SA_SIGINFO,函数sigaction()设置信号的处理函数则通过成员sa_sigaction设置。它的功能强大,在中断处理函数中,除了获取基本的信号值外,还可以获取其他值。
void  (*sa_sigaction)(int, siginfo_t *, void *)

第一个参数对应的信号,第二个参数使用struct siginf来描述信号中断的部分信息,第三个参数可以赋给指向ucontext_t类型的一个对象的指针,以引用在传递信号时被中断的接收进程或线程的上下文。

siginfo_t的结构形式:
typedef struct {  int  si_signo;      //signal number  int  si_errno;      //if nonzero, errno value from <errno.h>  int  si_code;       //additional info (deponds on signal)  pid_t  si_pid;      //sending process ID  uid_t  si_uid;      //sending process real user ID  void  *si_addr;     //address that caused the fault  int  si_status;     //exit value or signal number  union  sigval si_value;  //application-specific value//possibly other fields also } siginfo_t;
sigval 联合包含下列字段:
int  sival_int;void  *sival_ptr;
应用程序在传递信号时,在si_value.sival_int中传递一个整型数或者在si_valude.sival_ptr中传递一个指针值。

#include <stdio.h>#include <signal.h>#include <stdlib.h>#include <unistd.h>void func(int signo, siginfo_t *info, void *p){printf("signo=%d\n", signo);printf("sender pid=%d\n", info->si_pid);//打印发送者的pid}int main(int argc, char *argv[]){struct sigaction act, oact;sigemptyset(&act.sa_mask);act.sa_sigaction = func;//处理函数act.sa_flags = SA_SIGINFO;//需要修改的sa_flagssigaction(SIGUSR1, &act, &oact);//安装信号while (1){printf("pid is %d Hello world\n", getpid());pause();//等待一个信号}}
两个终端的显示:





五、信号应用示例:


(1)父亲进程执行文件拷贝操作(为了验证此程序,请选择大小在M级以上文件)。如果接收到SIGUSR1信号,将打印出当前拷贝进度,因此,父亲进程需要安装SIGUSR1信号;
(2)子进程每隔一个固定时间(其时间有ularm函数产生SIGALRM信号来决定)向父进程发送SIGUSR1信号。因此,子进程需要安装SIGALRM信号。

#include <stdio.h>#include <fcntl.h>#include <stdlib.h>#include <signal.h>#include <string.h>//the copy fiel size must > M//为了便于检测此程序,在拷贝时,请选择大小在M级的文件int count;//current copy number, 当前拷贝大小int file_size;//the file size 文件大小,因在中断无法传递普通参数,故用全局变量void sig_alarm(int arg);//处理alarm信号void sig_usr(int sig);//处理普通信号SIGUSR1int main(int argc, char *argv[]){pid_t pid;int i;int fd_src, fd_des;char buf[128];       //拷贝操作临时空间if(argc != 3){printf("check the format: comm src_file des_file\n");return -1;}if((fd_src = open(argv[1], O_RDONLY)) == -1)   //只读方式打开源文件{perror("open file src");exit(EXIT_FAILURE);}file_size = lseek(fd_src, 0, SEEK_END);//获取源文件大小lseek(fd_src, 0, SEEK_SET);//重新设置读写位置为文件头if((fd_des = open(argv[2], O_RDWR|O_CREAT, 0644)) == -1){//以读写方式打开目标文件,如果不存在,则创建perror("open fd_fdes");exit(EXIT_FAILURE);}if((pid = fork()) == -1)//创建子进程{perror("fork");exit(EXIT_FAILURE);}else if(pid > 0)//在父亲进程中,拷贝文件处于子进程信号请求{//并在信号处理时打印拷贝进程signal(SIGUSR1, sig_usr);//安装信号SIGUSR1do{memset(buf, '\0', 128);if((i = read(fd_src, buf, 1)) == -1){//拷贝数据,为验证结果,可以调整每次拷贝大小perror("read");exit(EXIT_FAILURE);}else if(i == 0)//如果拷贝完毕,则向子进程发送SIGINT信号{//使子进程中止kill(pid, SIGINT);break;}else{if(write(fd_des, buf, i) == -1) //否则执行拷贝操作{perror("write");exit(EXIT_FAILURE);}count += i;//更新已经拷贝大小}//usleep(1)} while(i != 0);wait(pid, NULL, 0);//等待子进程退出exit(EXIT_FAILURE);}else if (pid == 0)//在子进程中,每隔一段时间(ualarm决定){//请求父亲进程打印拷贝进度usleep(1);//wait for parent to install signalsignal(SIGALRM, sig_alarm);//安装SIGALRM信号ualarm(1, 1);//产生SIGALRM信号if alarm, in sig_alarm function to install againwhile (1)//一直执行{;}exit(EXIT_FAILURE);}}void sig_alarm(int arg){//alarm(1);//if alarm(), may add this linekill(getppid(), SIGUSR1);//在子进程的SIGALRM信号处理中}//向父亲进程发送SIGUSR1信号void sig_usr(int sig){//父亲进程对SIGUSR1信号处理函数float i;i = (float) count/(float)file_size;//求出拷贝过程printf("curent over : %0.0f\n", i*100);}
0 0