• 浅谈Linux中的信号处理机制(一)


         有好些日子没有写博客了,自己想想还是不要荒废了时间,写点儿东西记录自己的成长还是百利无一害的。今天是9月17号,暑假在某家游戏公司实习了一段时间,做的事情是在Windows上用c++写一些游戏英雄技能的逻辑实现。虽然时间不算长,但是也算学了一点东西,对团队项目开发流程也有了一个直观的感受,项目里c++11新特性也有用到不少,特别是lambda表达式,STL的一些容器和算法也终于有了可以实践的地方。由于自己比较喜欢Linux C,也就没有做留下的打算,现在回到了学校,好好复习一段时间,准备一下校招吧。如果有朋友有工作机会的话,不妨可以推荐一下我O(∩_∩)O,我的EMAIL:baixiangcpp@gamil.com。貌似有点扯远了,好久不写东西了。

    信号的基本概念

      谈起Linux编程signal是非常重要的一块内容。关于signal,Linux 的 manual有很详细的介绍。具体:man 7 signal .我就不浪费篇幅贴出来了。

      信号被认为是一种软件中断(区别于硬件中断)。信号机制提供了一种在单进程/线程 下处理异步事件的方法。具体过程是当进程运行到某处,接受到一个信号,保留“现场”,响应信号(注意这里的响应是一种宏观意义上的响应,对信号的忽略(SIG_IGN)也被以为是一种响应,后面会详细谈到信号响应的方式。),在返回到刚刚保存的地方继续运行。我制作了一张GIF或许可以清晰的体现这样的处理方式:

          产生信号的条件有很多,某些组合键(CTRL+C、CTRL+,CTRL+Z等),kill命令,kill系统调用以及由内核产生的某些信号(如内核检测到段错误、管道破裂等)。值得注意的是当我们发送信号时受到权限的限制,发送一个信号到另一个没有权限的进程是不合法的(关于权限的规则会在之后的博客总结)。信号的种类非常多,都以SIG+名字的形式命名的宏,通常都有实际意义和用法具体可查阅manual。有些常见的信号是需要熟记的如SIGINT,SIGCHLD,SIGIO等等。在编写程序的时候,我们最好用信号的宏的形式,这样可读性更好。那么如何“响应”信号呢?

    信号处理的接口之一 signal()

          对于大部分的信号,Linux系统都有默认的处理方式。而大部分默认的处理方式是终止程序并转储core文件。要处理信号,Linux系统处理信号的接口有两个sigaction(),signal(),较简单的是signal()函数,其形式如下:

     typedef void (*sighandler_t)(int);

    sighandler_t signal(int signum, sighandler_t handler);

          siganl()函数有两个参数其中有一个int的参数便是要处理的信号,诸如SIGINT的宏。另一个参数类型为sighandler_t的函数指针,handler指针对应的函数我们称之为:信号处理函数(signal-handler function)。可见signal()的第二个参数是一个信号处理函数,返回值也是一个信号处理函数,失败返回宏SIG_ERR(SIGKILL和SIGSTOP的默认行为分别是杀死和停止一个进程,任何试图改变这两个信号的处理方式的行为都将返回错误)。这样经典形式的函数在Linux上我们经常会经常碰到。signal()函数的作用就是建立一个signum信号的处理函数(establish a signal handler function)。通俗一点来说就是当signum信号到来时,进程会保存当前的进程栈,转去执行siganl()中指定的handler函数。之前提到过,信号的响应方式有多种,因此handler不仅可以是一个函数指针也可以是ISO C为我们定义的宏:SIG_IGN,SIG_DEL,和他们的名字一样SIG_IGN是忽略这个信号,SIG_DEL是保持这个信号的默认处理方式(默认处理方式也可以可以是SIG_IGN ,比较绕,但是合理)。前文提到的三个宏定义分别如下(/usr/include/bits/signum.h):

    #define SIG_ERR ((__sighandler_t) -1) 
    #define SIG_DFL ((__sighandler_t) 0)
    #define SIG_IGN ((__sighandler_t) 1)

          下面我写一个小的DEMO演示一下如何写一个信号处理函数:

    #include <signal.h>
    #include <stdio.h>
    
    void sigdemo(int sig)
    {
        printf("Receive a signal:%s
    ",strsignal(sig));
    }
    
    int main()
    {
        if(signal(SIGINT,sigdemo) == SIG_ERR)
      {
        perror("signal()");
        return ;
      }
        printf("Main started.
    ");
        pause();//wait a signal.
    }
    

      

    可以看到,我在main函数中并没有主动调用sigdemo函数,可是运行程序后,当我们在中断按下CTRL+C时(发送SIGINT信号,宏对应的值是2),出现了这样的结果:

    [baixiang ~$]a.out
     Main started.
     ^CReceive a signal:2

    可见sigdemo函数得到了执行,其参数sig便是接受到的信号的值。要将信号的值,转换为其意义string.h中提供了一个函数char* strsignal(int sig), 基本上看到该函数原型就知道这个函数怎么用了,在此我就不再浪费篇幅赘述了。

    发送信号

         上文我们通过使用CTRL+C组合键发送信号SIGINT给当前的进程。但是这种方法只能发送少部分信号且并不适用所有的进程比如后台进程和守护进程。守护进程不必说,连终端都没有。交互shell (interactive shell)在启动一个后台进程的时候,会自动把中断和退出信号设置为忽略,关于这点我在网上看到一篇不错的博客:http://hongjiang.info/shell-script-background-process-ignore-sigint/ 。这样的情况下就无法使用快捷键的方式了。这里我介绍几种其他的发送信号的方式。

         首先是shell命令kill其用法如下:

    kill [-s signal|-p] [-q sigval] [-a] [--] pid...

         -s  signal  signal可以是诸如SIGINT,SIGQUIT之类的宏,亦可以是1,2,3...这样的值,可以随意使用,你开心就好。

         -q queue  sigval是值,可以伴随信号传递,但是这里只可以是一个integer,在进程中可以使用sigaction()接收到这个值,与之对应的是另一个函数sigqueue()。这里先不详细介绍,下文会谈到。

         pid就是目标进程的进程id,可以是一个或者多个。但是发送信号时,要确保你所使用的用户是具有发送信号到目标进程的权限的。

         kill的选项远不止这些,但是通常这些已经够用了。如有兴趣请自行 “man 1 kill”查看。

         和shell命令kill有一个同名的系统调用kill(),其原型是这样的:

    int kill(pid_t pid, int sig);

         相信看了上边的shell指令,这个函数的用法就一目了然了吧。pid是目标进程的pid,sig是要发送的信号。和其他函数一样它也是成功返回0,失败-1。然而真的这么简单吗?事实上不是。pid这个参数在这里大有学问。它的取值不仅仅可以是进程id,它甚至可以是负的。如果你对linux下编程熟悉的话,这样的用法肯定接触过,获取消息队列时使用的msgrcv()函数,其中的msgtype参数也具有类似的用法。当然扯远了。

        pid>0 此时正式最普通的一种情况,pid是要目标进程的pid。

        pid=0  那么kill()会将信号发送给调用进程同组的所有进程,也包括他自己。

        pid=-1 那么信号将被发送至所有它具有权限发送信号的每一个进程(init进程和调用进程除外)。

        pid<-1 信号会发送sig信号到组id等于该pid绝对值的进程组中的每一个进程

    如果pid在以上四种情况之外,无法匹配到目标进程,那么就会返回-1,errno被设置为ESRCH。当没有权限发送时kill()也将失败返回-1,errno会被设置为EPERM。关于linux上权限是如何作用的细节,我争取再后面的博客总结一下。

         与kill()类似的还有一个函数killpg(),用法简单多了,也不浪费篇幅了,查看manual就能搞定。

         最后一个发送信号的函数是raise(),它只接受一个参数signal,然后把该信号传递给调用进程:

    int raise(int sig);//成功返回0,失败返回-1

        由于这个函数不需要引用进程ID,它是被纳入C99标准的函数。

        除了这几种产生信号的shell命令和函数之外还有一些情况下可以产生信号,比如alarm(),settimer()之类的一些与时间相关的函数,以及一些常见的软硬件错误都会产生信号。详细谈这些貌似就有点淡化主题了,扯远了。

    不可靠信号与可靠信号的语义

          信号的可靠与不可靠主要体现在两个方面:

    • 对于不可靠信号,进程每次处理信号后,都会将信号的处理方式设置为默认动作。而对于可靠信号,它的处理函数执行以后,对该信号的处理方式不会发生变化。
    • 信号可能会丢失。

         由于Linux信号机制基本上从早期的UNIX系统上的信号机制移植过来的,所以Linux仍旧支持这些早期的不可靠信号。但是Linux也对不可靠信号做了(上面两点区别的第一小点)改进,即不可靠信号处理方式,不会在处理函数执行后变成默认方式。所以,在Linux上对于不可靠信号与可靠信号的区别就在于是否支持排队。

         关于信号是否会丢失,我们看这样两段代码,首先是rcv.c:

    #include <signal.h>
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <unistd.h>
    
    void fun(int sig)
    {
        printf("Recvive a signal:%s
    ",strsignal(sig));
    }
    
    int main()
    {
        if(signal(SIGINT,fun) == SIG_ERR)
            perror("signal");
        printf("%d
    ",getpid());
        while(1)
           pause();
    }

         这段程序先安装SIGINT的信号处理函数fun,fun函数只是打印信号信息。之后打印出进程id同时死循环等待信号。

    另一段程序是send.c:

    #include <signal.h>
    #include <stdlib.h>
    #include <stdio.h>
    
    int main(int argc,char** argv)
    {
        pid_t pid = (pid_t)atol(argv[1]);
        printf("%d
    ",pid);
        int i = 0;
        for(;i<500;++i)
            kill(pid,SIGINT);
    }
    

      

    send程序从终端接收一个参数即目标进程的PID,然后向其发送500次SIGINT信号。下面我们分别把rcv.c,send.c编译成rcv和send。首先运行rcv,打印出了进程pid 20273然后,我们在开一个终端运行send 20273,观察到这样的结果:

    send明明发送了500个SIGINT信号,而rcv中只接受处理了13个SIGINT信号,这是怎么回事儿呢?明天再接着写下去,今天太晚了,战斗力不足有点犯困了。

  • 相关阅读:
    《计算机网络 自顶向下方法》整理(二)应用层
    《计算机网络 自顶向下方法》整理(一)计算机网络和因特网
    《深入理解C#》整理10-使用async/await进行异步编程
    STM32 HAL库之串口详细篇
    .Net微服务实战之负载均衡(下)
    面试官:来,年轻人!请手撸5种常见限流算法!
    工具用的好,下班回家早!iTerm2使用技巧指北!
    Java编程规范(命名规则)
    Go语言快速安装手册
    Educational Codeforces Round 6 620E. New Year Tree(DFS序+线段树)
  • 原文地址:https://www.cnblogs.com/ittinybird/p/4777047.html
Copyright © 2020-2023  润新知