Linux进程间通信详解(六) —— 信号种类及函数

主要介绍:

  • Linux中的信号种类
  • 信号操作的相关函数

Linux中的信号种类

信号是一种进程间通信的方法,应用于异步事件的处理。信号的实质是一种软中断。

使用kill -l可以查看Linux系统中的所有信号,如下:

deeplearning@deeplearning:~$ kill -l
 1) SIGHUP       2) SIGINT       3) SIGQUIT      4) SIGILL       5) SIGTRAP
 6) SIGABRT      7) SIGBUS       8) SIGFPE       9) SIGKILL     10) SIGUSR1
11) SIGSEGV     12) SIGUSR2     13) SIGPIPE     14) SIGALRM     15) SIGTERM
16) SIGSTKFLT   17) SIGCHLD     18) SIGCONT     19) SIGSTOP     20) SIGTSTP
21) SIGTTIN     22) SIGTTOU     23) SIGURG      24) SIGXCPU     25) SIGXFSZ
26) SIGVTALRM   27) SIGPROF     28) SIGWINCH    29) SIGIO       30) SIGPWR
31) SIGSYS      34) SIGRTMIN    35) SIGRTMIN+1  36) SIGRTMIN+2  37) SIGRTMIN+3
38) SIGRTMIN+4  39) SIGRTMIN+5  40) SIGRTMIN+6  41) SIGRTMIN+7  42) SIGRTMIN+8
43) SIGRTMIN+9  44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9  56) SIGRTMAX-8  57) SIGRTMAX-7
58) SIGRTMAX-6  59) SIGRTMAX-5  60) SIGRTMAX-4  61) SIGRTMAX-3  62) SIGRTMAX-2
63) SIGRTMAX-1  64) SIGRTMAX

对其中一些信号进行介绍:

  • SIGHUP:本信号在用户终端连接结束时(正常或非正常)发出。
  • SIGINT:程序终止(或中断,interrupt)信号,通常是Ctrl+cDelete键(INTR字符)时发出。
  • SIGQUIT:与SIGINT类似,但由Ctrl+\(QUIT字符)控制,进程收到该信号时会产生core文件,类似于一个程序错误信号。
  • SIGLL:执行了非法指令,通常是可执行文件本身错误。
  • SIGKILL:用来立即结束程序的运行,该信号不能被阻塞、处理或忽略。
  • SIGTERM:程序结束(terminate)信号,与SIGKILL不同的是该信号可以被阻塞或处理,shell命令的kill默认产生该信号。
  • SIGSTOP:停止(stopped)进程的执行,注意和terminate及interrupt的区别,该进程还未结束,只是暂停执行,该信号与SIGKILL一样不能被阻塞、处理或忽略。
  • SIGWINCH:窗口大小改变时发出的信号。

信号操作的相关函数

信号的处理

signal函数

要对一个信号进行处理(除了无法捕捉的SIGKILL和SIGSTOP),需要为其注册相应的处理函数,通过调用**signal()**函数可以进行注册。

    1. 捕捉SIGINT信号,catch_sigint.c:
    #include 
    #include 
    #include 
    #include 
    void SignHandler(int iSignNum)
    {
        printf("Capture signal number:%d\n",iSignNum);
        exit(1);
    }
    int main(void)
    {
        signal(SIGINT,SignHandler);
        while(1)
            sleep(1);
        return 0;
    }
    
    

    使用gcc编译,并执行,通过Ctrl+c查看效果:

    $ gcc -o catch_sigint catch_sigint.c
    $ ./catch_sigint
    
    ^CCapture signal number:2    (键入“Ctrl+c”)
    

    可以看到,“Ctrl+c”产生的SIGINT信号(代码为2)被程序捕捉到了。

    1. 忽略SIGINT信号,ignore_sigint.c:
    #include 
    #include 
    #include 
    
    int main(void)
    {
        signal(SIGINT,SIG_IGN);
        while(1)
            sleep(1);
        return 0;
    }
    

    该程序将“Ctrl+C"产生的SIGINT信号忽略掉了,不能结束进行,不过可以通过”Ctrl+"发送SIGQUIT信号。

    编译运行:

    $ ./ignore_sigint
    ^C^\Quit (core dumped)    (键入“Ctrl+c”,再键入“Ctrl+\”)
    

    可以看到,“Ctrl+c”无效,因为被程序忽略了,自能通过“Ctrl+\”结束程序。

    1. SIGINT信号的默认处理,default_sigint.c:
    #include 
    #include 
    #include 
    
    int main(void)
    {
        signal(SIGINT,SIG_DFL);
        while(1)
            sleep(1);
        return 0;
    }
    

    运行1:

    $ ./default_sigint
    ^C    (键入“Ctrl+c”)
    

    运行2:

    $ ./default_sigint
    ^\Quit (core dumped)   (键入“Ctrl+\”)
    

    可以看到两种程序结束方式。

    1. 定义多个信号处理函数,signals.c:
    #include 
    #include 
    #include 
    #include 
    
    void sigroutine(int dunno)
    {
        switch(dunno)
        {
            case 1:printf("Capture SIGHUP signal,the signal number is %d\n",dunno);break;
            case 2:printf("Capture SIGINT signal,the signal number is %d\n",dunno);break;
            case 3:printf("Capture SIGQUIT signal,the signal number is %d\n",dunno);break;
        }
        return;
    }
    
    int main(void)
    {
        printf("process ID is %d\n", getpid());
        if(signal(SIGHUP,sigroutine)==SIG_ERR)
            printf("Couldn't register signal handler for SIGHUP!\n");
        if(signal(SIGINT,sigroutine)==SIG_ERR)
            printf("Couldn't register signal handler for SIGINT!\n");
        if(signal(SIGQUIT,sigroutine)==SIG_ERR)
            printf("Couldn't register signal handler for SIGQUIT!\n");
        while(1)
            sleep(1);
        return 0;
    }
    

    运行:

    $ ./signals
    process ID is 5793
    ^CCapture SIGINT signal,the signal number is 2      (键入“Ctrl+c”)
    ^\Capture SIGQUIT signal,the signal number is 3     (键入“Ctrl+\”)
    ^Z                                                  (键入“Ctrl+z”)
    [1]+  Stopped                 ./signals
    

    键入“Ctrl+z”后,进程置于后台,继续使用bg命令:

    $ bg
    [1]+ ./signals &
    

    继续发送SIGUP信号:

    $ kill -HUP 5793
    Capture SIGHUP signal,the signal number is 1
    

    最后使用kill结束进程:

    kill -9 5793
    

sigaction函数

Linux还提供另外一种功能更加强大的信号处理机制:sigaction系统调用。sigaction函数的功能是检查或修改与指定信号相关联的处理动作,该函数可完全替代signal函数,并且还提供更加详细的信息,确切了解进程接收到信号时所发生的具体细节。

  • sigaction函数使用举例,sigaction.c:

    #include 
    #include 
    #include 
    #include 
    
    int g_iSeq=0;
    
    void SignHandlerNew(int iSignNo, siginfo_t *pInfo, void *pReserved)
    {
       int iSeq = g_iSeq++;
       printf("%d Enter SignHandlerNew, signo:%d.\n", iSeq, iSignNo);
       sleep(3);
       printf("%d Leave SignHandlerNew, signo:%d.\n", iSeq, iSignNo);
    }
    
    int main(void)
    {
        char szBuf[20];
        int iRet;
        struct sigaction act;
        act.sa_sigaction = SignHandlerNew;
        act.sa_flags = SA_SIGINFO;
        sigemptyset(&act.sa_mask);
    
        sigaction(SIGINT, &act, NULL);
        sigaction(SIGQUIT, &act, NULL);
    
        do{
            iRet=read(STDIN_FILENO, szBuf, sizeof(szBuf)-1);
            if(iRet<0)
            {
                perror("read fail.");
                break;
            }
            szBuf[iRet]=0;
            printf("Get: %s", szBuf);
        }while(strcmp(szBuf, "quit\n")!=0);
    
        return 0;
    }
    

    执行:

    $ ./sigaction
    hello!                   (键入“hello!”)
    Get: hello!
    linux!                   (键入“linux!”)
    Get: linux!
    ^C0 Enter SignHandlerNew, signo:2.        (键入“Ctrl+c”,产生SIGINT信号)
    ^\1 Enter SignHandlerNew, signo:3.        (3秒内再次,键入“Ctrl+\”,产生SIGQUIT信号)
    1 Leave SignHandlerNew, signo:3.          (SIGQUIT信号处理完毕)
    0 Leave SignHandlerNew, signo:2.          (SIGINT信号处理完毕)
    read fail.: Interrupted system call       (读出错,进程中断,程序非正常退出)
    

    可以看出,当终端还未产生SIGINT或SIGQUIT信号时,可以正确的进行输入,并打印出输入的数据,而当信号产生时,进程被中断了。

    再次运行程序,使用退出字符“quit”,测试如下:

    $ ./sigaction
    hello!                    (键入“hello!”)
    Get: hello!
    quit                      (键入“quit”,程序正常退出)
    Get: quit               
    

信号集

在实际应用中,一个用户进程常常需要对多个信号进行处理,在LInux中引入信号集(signal set)概念,用于表示由多个信号所组成集合的数据类型,其定义为sigset_t类型的变量。

信号的发送

发送信号的函数有:kill,raise,sigqueue,alarm,setitimer,abort。

kill函数

kill函数用于向某一进程或进程组发送信号。

  • 父进程使用kill函数向子进程传递一个SIGABRT信号,使子进程非正常结束,kill.c:

    #include
    #include
    #include
    #include
    #include
    
    int main(void)
    {
        pid_t pid;
        int status;
        if(!(pid=fork()))
        {
            printf("Hi I am child process!\n");
            sleep(10);
            printf("Hi I am child process, again!\n");
            return 1;
        }
        else
        {
            printf("send signal to child process (%d)\n", pid);
            sleep(1);
            if(kill(pid, SIGABRT)==1)
                printf("kill failed!\n");
            wait(&status);
            if(WIFSIGNALED(status))
                printf("child process receive signal %d\n", WTERMSIG(status));
        }
        return 0;
    }
    

    运行:

    $ ./kill
    send signal to child process (2689)
    Hi I am child process!
    child process receive signal 6
    

    从结果可以看出,当父进程将SIGABRT发送给子进程(ID 2689)后,子进程非正常结束,第2句输出语句没有执行。

raise函数

raise函数用于向进程本身发送信号。

  • 使用raise函数向自身进程发送一个SIGABRT信号,使自己非正常结束,raise.c:

    #include
    #include
    #include
    #include
    
    int main(void)
    {
        printf("Hello, I like Linux C Progrms!\n");
        if(raise(SIGABRT)==-1)
        {
            printf("raise failed!");
            exit(1);
        }
        printf("Hello, I like Linux C Progrms, again!\n");
    
        return 0;
    }
    

    运行:

    $ ./raise
    Hello, I like Linux C Progrms!
    Aborted (core dumped)
    

    可以看到程序非正常结束。

sigqueue函数

sigqueue是比较新的发送信号系统调用,主要针对实时信号提出的,支持信号带有参数,通常与sigaction函数配合使用。

  • 使用sigqueue函数向进程自身发送信号SIGUSR1信号,并附加一个字符串信息,sigqueue.c:

    #include
    #include
    #include
    #include
    
    void SigHandler(int signo, siginfo_t *info, void *context)
    {
        char *pMsg=(char*)info->si_value.sival_ptr;
        printf("Receive signalunmber:%d\n",signo);
        printf("Receive Meaasage:%s\n",pMsg);
    }
    
    int main(void)
    {
        struct sigaction sigAct;
        sigAct.sa_flags=SA_SIGINFO;
        sigAct.sa_sigaction=SigHandler;
        if(sigaction(SIGUSR1, &sigAct, NULL) == -1)
        {
            printf("sigaction filed!\n");
            exit(1);
        }
        sigval_t val;
        char pMsg[] = "I like Linux C programs!";
        val.sival_ptr = pMsg;
        if(sigqueue(getpid(), SIGUSR1, val) == -1)
        {
            printf("sigqueue failed!\n");
            exit(1);
        }
        sleep(3);
        return 0;
    }
    

    运行:

    $ ./sigqueue
    Receive signalunmber:10
    Receive Meaasage:I like Linux C programs!
    

    可以看出,进程成功接收到了自身发送的信号10(SIGUSR1)以及信号携带的字符串参数。

alarm函数

alarm函数专门为SIGALRM信号而设,使系统在一定时间之后发送信号。

使用alarm函数产生SIGALRM信号,alarm时间参数设置为5分钟,alarm.c:

#include
#include
#include

void handler()
{
    printf("Hello, I like linux C programs!\n");
}

int main(void)
{
    int i;
    signal(SIGALRM, handler);
    alarm(5);
    for(i=1;i<7;i++)
    {
        printf("sleep %d ...\n", i);
        sleep(1);
    }

    return 0;
}

执行:

$ ./alarm
sleep 1 ...
sleep 2 ...
sleep 3 ...
sleep 4 ...
sleep 5 ...
Hello, I like linux C programs!
sleep 6 ...

在for循环运行了5次,即大约5秒后,产生了SIGALRM信号,此时由signal注册信号的处理函数handler,输出字符串。信号处理完毕后又返回先前程序的中断点,继续执行for循环。

setitimer函数

setitimer函数与alarm函数一样,也可以用于使系统在某一时刻发出信号,但它可以更加精确地控制程序。

  • 使用setitimer函数产生SIGALRM信号,setitimer.c:

    #include
    #include
    #include
    #include
    #include
    #include
    
    static void ElsfTimer(int signo)
    {
        struct timeval tp;
        struct tm *tm;
        gettimeofday(&tp, NULL);
        tm = localtime(&tp.tv_sec);
        printf("sec = %ld\t", tp.tv_sec);
        printf("usec = %ld\n", tp.tv_usec);
        printf("%d-%d-%d%d:%d:%d\n", tm->tm_year+1900, tm->tm_mon+1, tm->tm_mday, tm->tm_hour, tm->tm_min, tm->tm_sec);
    }
    
    static void InitTime(int tv_sec, int tv_usec)
    {
        struct itimerval value;
        signal(SIGALRM, ElsfTimer);
        value.it_value.tv_sec = tv_sec;
        value.it_value.tv_usec = tv_usec;
        value.it_interval.tv_sec = tv_sec;
        value.it_interval.tv_usec = tv_usec;
        setitimer(ITIMER_REAL, &value, NULL);
    }
    
    int main(void)
    {
        InitTime(5, 0);
        while(1){}
    
        exit(0);
    }
    

    执行:

    ./setitimer
    sec = 1574475716        usec = 295047
    2019-11-2310:21:56
    sec = 1574475721        usec = 295042
    2019-11-2310:22:1
    sec = 1574475726        usec = 295041
    2019-11-2310:22:6
    sec = 1574475731        usec = 295041
    2019-11-2310:22:11
    sec = 1574475736        usec = 295041
    2019-11-2310:22:16
    sec = 1574475741        usec = 295041
    2019-11-2310:22:21
    ^\Quit (core dumped)             (键入“Ctrl+\”退出)
    

    可以看出,程序每隔5秒便会调用信号处理函数ElsfTimer,打印当前系统的时间和日期。

abort函数

向进程发送SIGABORT信号,默认情况下进程会异常退出,当然可以定义自己的信号处理函数。即使SIGABORT被进程设置为阻塞信号,调用abort后,SIGABORT仍能被进程接收。

信号的阻塞

在Linux的信号控制中,有时不希望进程在接收到信号时立刻中断进行的执行,也不希望该信号被完全忽略,而是延时一段时间再去调用相关的信号处理函数。

sigprocmask函数

sigprocmask函数可以用于检查或更改进程的信号掩码(signalmask)。信号掩码是由被阻塞的发送给当前进程的信号组成的信号集。

将sigaction.c程序进行修改,得到如下程序:

  • 阻塞屏蔽SIGINT信号,block_sigint.c函数:

    #include
    #include
    #include
    #include
    
    int g_iSeq = 0;
    
    void SignHandlerNew(int iSignNo, siginfo_t *pInfo, void *pReserved)
    {
        int iSeq = g_iSeq++;
        printf("%d Enter SignHandlerNew, signo:%d.\n", iSeq, iSignNo);
        sleep(3);
        printf("%d Leave SignHandlerNew, signo:%d.\n", iSeq, iSignNo);
    }
    
    int main(void)
    {
        char szBuf[20];
        int iRet;
        struct sigaction act;
        act.sa_sigaction = SignHandlerNew;
        act.sa_flags = SA_SIGINFO;
        sigset_t sigSet;
    
        sigemptyset(&sigSet);
        sigaddset(&sigSet, SIGINT);
        sigprocmask(SIG_BLOCK, &sigSet, NULL);
        sigemptyset(&act.sa_mask);
        sigaction(SIGINT, &act, NULL);
        sigaction(SIGQUIT, &act, NULL);
        do{
            iRet = read(STDIN_FILENO, szBuf, sizeof(szBuf)-1);
            if(iRet<0)
            {
               perror("read fail.");
               break;
            }
            szBuf[iRet]=0;
            printf("Get:%s",szBuf);
         }while(strcmp(szBuf, "quit\n")!=0);
    
        return 0;
    }
    

    运行:

    $ ./block_sigint
    hello!             (键入“hello!”)
    Get:hello!         
    linux!             (键入“linux!”)
    Get:linux!
    ^\0 Enter SignHandlerNew, signo:3.     (键入“Ctrl+\”,产生SIGQUIT信号)
    0 Leave SignHandlerNew, signo:3.       (SIGQUIT信号处理完毕)
    read fail.: Interrupted system call    (读出错,进程中断,程序非正常退出)
    

    与上面 的sigaction.c程序相比,此程序键入“Ctrl+c"不再有反应,屏蔽了SIGINT信号。

sigsuspend函数

sigsuspend函数用于使进程挂起,然后等待开放信号的唤醒。注意,此函数没有成功返回值,如果它返回到调用者,则总是返回-1。

计时器与信号

睡眠函数

Linux系统下有两个睡眠函数:sleep()usleep(),函数原型为:

#include 
unsigned int sleep(unsigned int seconds);
void usleep(unsigned long usec);

两个函数分别让进程睡眠seconds秒和usec微秒。

sleep函数的内部是使用信号机制进行处理,用到的函数有:

#include 
unsigned int alarm(unsigned int seconds);
int pause(void);

alarm函数告知自身进程,在seconds秒后自动产生一个SIGALRM信号。而pause函数用于将自身进程挂起,直到有信号发生时才从pause返回。

  • 使用pause函数将进程挂起,模拟随眠3秒钟,pause.c:

    #include
    #include
    #include
    
    void SignHandler(int iSignNo)
    {
        printf("signal;%d\n", iSignNo);
    }
    
    int main(void)
    {
        signal(SIGALRM, SignHandler);
        alarm(3);
        printf("Before pause().\n");
        pause();
        printf("After pause().\n");
        return 0;
    }
    

    执行:

    $ ./pause
    Before pause().
    signal;14
    After pause().
    

    在输出第一句"Before pause()."后,等待约3秒钟,采输出第二句。

时钟处理

Linux系统为每个进程维护3个计时器:

  • 真实计时器计算的是程序运行的实际时间
  • 虚拟计时器计算的是程序运行在用户态时所消耗的时间(实际时间减去系统调用和程序随眠时间)
  • 实用计时器计算的是程序处于用户态内核态所消耗的时间之和

参考:《精通Linux C编程》- 程国钢

你可能感兴趣的:(Linux知识库)