• LINUX信号


    信号的机制

    文章目录


    A给B发送信号,B收到信号之前执行自己的代码,收到信号后,不管执行到程序的什么位置,都要暂停运行,
    去处理信号,处理完毕再继续执行。与硬件中断类似一一异步模式。但信号是软件层面上实现的中断,早期常被称
    为“软中断”。
    **信号的特质:**由于信号是通过软件方法实现,其实现手段导致信号有很强的延时性。但对于用户来说,这个延
    迟时间非常短,不易察觉。

    每个进程收到的所有信号,都是由内核负责发送的,内核处理。

    与信号相关的事件和状态

    产生信号:
    1.按键产生,如:Ctr+c、Ctr+z、Ct+
    2.系统调用产生,如:kill、raise、abort
    3.软件条件产生,如:定时器alarm
    4.硬件异常产生,如:非法访问内存(段错误)、除0(浮点数例外)、内存对齐出错(总线错误)
    5.命令产生,如:kill命令

    递达:递送并且到达进程。

    未决:产生和递达之间的状态。主要由于阻塞(屏蔽)导致该状态。

    未决信号集, 阻塞信号集

      阻塞信号集是当前进程要阻塞的信号的集合,未决信号集是当前进程中还处于未决状态的信号的集合,这两个集合存储在内核的PCB中。
    
    • 1

    一句话,未决信号集是发出信号后 ,未被处理的信号集合。

    阻塞信号集是 发出信号前 ,要阻塞的信号集合。

    以SIGINT为例说明信号未决信号集和阻塞信号集的关系:

            当进程收到一个ASIGINT信号(信号编号为2),首先这个信号会保存在未决信号集合中,此时对应的2号编号的这个位置上置为1,表示处于未决状态;在这个信号需要被处理之前首先要在阻塞信号集中的编号为2的位置上去检查该值是否为1:
            如果为1,表示SIGNIT信号被当前进程阻塞了,这个信号暂时不被处理,所以未决信号集  上该位置上的值保持为1,表示该信号处于未决状态;
            如果为0,表示SIGINT信号没有被当前进程阻塞,这个信号需要被处理,内核会对SIGINT信号进行处理(执行默认动作,忽略或者执行用户自定义的信号处理函数),并将未决信号集中编号为2的位置上将1变为0,表示该信号已经处理了,这个时间非常短暂,用户感知不到。
            当SIGINT信号从阻塞信号集中解除阻塞之后,该信号就会被处理。
    
    • 1
    • 2
    • 3
    • 4

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rPcq8SPR-1667951850681)(C:\Users\User\AppData\Roaming\Typora\typora-user-images\image-20220727161018978.png)]

    信号的处理方式:

    1.执行默认动作(五种)

    默认动作:

    Term:终止进程 Ign: 忽略信号 (默认即时对该种信号忽略操作)

    Core:终止进程,生成 Core 文件。(查验进程死亡原因, 用于 gdb 调试)

    Stop:停止(暂停)进程 Cont:继续运行进程**

    2.忽略

    (丢弃){是处理完毕后丢弃,不是未处理 }

    3.捕捉

    (调用户处理函数)

    Linux内核的进程控制块PcB是一个结构体,task_struct,除了包含进程id,状态,工作目录,用户id,组id,
    文件描述符表,还包含了信号相关的信息,主要指阻塞信号集和未决信号集。
    阻塞信号集(信号屏蔽字):将某些信号加入集合,对他们设置屏蔽,当屏蔽×信号后,再收到该信号,该信号
    的处理将推后(解除屏蔽后)

    信号4要素

    与变量三要素类似的,每个信号也有其必备4要素,分别是:
    1.编号2.名称3.事件4.默认处理动作
    可通过man7 signal查看帮助文档获取。

    名称 动作 事件

    Signal Standard Action Comment ────────────────────────────────────────────────────────────────────────
    SIGABRT P1990 Core Abort signal from abort(3)
    SIGALRM P1990 Term Timer signal from alarm(2)
    SIGBUS P2001 Core Bus error (bad memory access)
    SIGCHLD P1990 Ign Child stopped or terminated
    SIGCLD - Ign A synonym for SIGCHLD
    SIGCONT P1990 Cont Continue if stopped
    SIGEMT - Term Emulator trap
    SIGFPE P1990 Core Floating-point exception


    注意不同平台下 信号的编号是不同的

    Signal x86/ARM Alpha/ MIPS PARISC Notes
    most others SPARC
    ─────────────────────────────────────────────────────────────────
    SIGHUP 1 1 1 1
    SIGINT 2 2 2 2
    SIGQUIT 3 3 3 3
    SIGILL 4 4 4 4
    SIGTRAP 5 5 5 5
    SIGABRT 6 6 6 6
    SIGIOT 6 6 6 6
    SIGBUS 7 10 10 10
    SIGEMT - 7 7 -
    SIGFPE 8 8 8 8
    SIGKILL 9 9 9 9
    SIGUSR1 10 30 16 16


    查看所有信号

    kill  -l
    
    • 1
     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	
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    不能捕捉和阻塞的信号

    注意从 man 7 signal 帮助文档中可看到 : The signals SIGKILL and SIGSTOP cannot be caught, blocked, or ignored.

    这里特别强调了 9) SIGKILL 和 19) SIGSTOP 信号,不允许忽略和捕捉,只能执行默认动作。甚至不能将其设置为 阻塞。

    (为了安全的保证,比如防止危险进程不能被杀死和停止)

    另外需清楚,只有每个信号所对应的事件发生了,该信号才会被递送(但不一定递达),不应乱发信号!!

    Linux 常规信号一览表

    1. SIGHUP: 当用户退出 shell 时,由该 shell 启动的所有进程将收到这个信号,默认动作为终止进程

    2. SIGINT:当用户按下了组合键时,用户终端向正在运行中的由该终端启动的程序发出此信号。默认动 作为终止进程。

    3. SIGQUIT:当用户按下组合键时产生该信号,用户终端向正在运行中的由该终端启动的程序发出些信 号。默认动作为终止进程。

    4. SIGILL:CPU 检测到某进程执行了非法指令。默认动作为终止进程并产生 core 文件

    5. SIGTRAP:该信号由断点指令或其他 trap 指令产生。默认动作为终止里程 并产生 core 文件。

    6. SIGABRT: 调用 abort 函数时产生该信号。默认动作为终止进程并产生 core 文件。

    7. SIGBUS:非法访问内存地址,包括内存对齐出错,默认动作为终止进程并产生 core 文件。

    8. SIGFPE:在发生致命的运算错误时发出。不仅包括浮点运算错误,还包括溢出及除数为 0 等所有的算法错误。 默认动作为终止进程并产生 core 文件。

    9. SIGKILL:无条件终止进程。本信号不能被忽略,处理和阻塞。默认动作为终止进程。它向系统管理员提供了 可以杀死任何进程的方法。

    10. SIGUSE1:用户定义 的信号。即程序员可以在程序中定义并使用该信号。默认动作为终止进程。

    11. SIGSEGV:指示进程进行了无效内存访问。默认动作为终止进程并产生 core 文件。

    12. SIGUSR2:另外一个用户自定义信号,程序员可以在程序中定义并使用该信号。默认动作为终止进程。

    13. SIGPIPE:Broken pipe 向一个没有读端的管道写数据。默认动作为终止进程。 北京市昌平区建材城西路金燕龙办公楼一层 电话:400-618-9090

    14. SIGALRM: 定时器超时,超时的时间 由系统调用 alarm 设置。默认动作为终止进程。

    15. SIGTERM:程序结束信号,与 SIGKILL 不同的是,该信号可以被阻塞和终止。通常用来要示程序正常退出。 执行 shell 命令 Kill 时,缺省产生这个信号。默认动作为终止进程。

    16. SIGSTKFLT:Linux 早期版本出现的信号,现仍保留向后兼容。默认动作为终止进程。

    17. SIGCHLD:子进程状态发生变化时,父进程会收到这个信号。默认动作为忽略这个信号。

    18. SIGCONT:如果进程已停止,则使其继续运行。默认动作为继续/忽略。

    19. SIGSTOP:停止进程的执行。信号不能被忽略,处理和阻塞。默认动作为暂停进程。

    20. SIGTSTP:停止终端交互进程的运行。按下组合键时发出这个信号。默认动作为暂停进程。

    21. SIGTTIN:后台进程读终端控制台。默认动作为暂停进程。

    22. SIGTTOU: 该信号类似于 SIGTTIN,在后台进程要向终端输出数据时发生。默认动作为暂停进程。

    23. SIGURG:套接字上有紧急数据时,向当前正在运行的进程发出些信号,报告有紧急数据到达。如网络带外 数据到达,默认动作为忽略该信号。

    24. SIGXCPU:进程执行时间超过了分配给该进程的 CPU 时间 ,系统产生该信号并发送给该进程。默认动作为 终止进程。

    25. SIGXFSZ:超过文件的最大长度设置。默认动作为终止进程。

    26. SIGVTALRM:虚拟时钟超时时产生该信号。类似于 SIGALRM,但是该信号只计算该进程占用 CPU 的使用时 间。默认动作为终止进程。

    27. SGIPROF:类似于 SIGVTALRM,它不公包括该进程占用 CPU 时间还包括执行系统调用时间。默认动作为终止 进程。

    28. SIGWINCH:窗口变化大小时发出。默认动作为忽略该信号。

    29. SIGIO:此信号向进程指示发出了一个异步 IO 事件。默认动作为忽略。

    30. SIGPWR:关机。默认动作为终止进程。

    31. SIGSYS:无效的系统调用。默认动作为终止进程并产生 core 文件。

    32. SIGRTMIN ~ (64) SIGRTMAX:LINUX 的实时信号,它们没有固定的含义(可以由用户自定义)。所有的实时 信号的默认动作都为终止进程。

    信号的产生

    终端按键产生信号

    Ctrl+c→2)SIGINT(终止/中断)“INT”–Interrupt
    Ctrl+z→20)SIGTSTP(暂停/停止)“T”-Terminal终端。
    Ctrl+\→3)SIGQUIT(退出)

    硬件异常产生信号

    除0操作
    →8)S1GFPE(浮点数例外)“F”—foat浮点数。
    非法访问内存→11)SIGSEGV(段错误)
    总线错误→7)SIGBUS

    k训函数/命令产生信号

    采用命令行

    kill -SIGKILL pid
    
    • 1

    k训函数:给指定进程发送指定信号(不一定杀死)

    int kill(pid t pid,int sig);成功::失败:-l (ID非法,信号非法,普通用户杀init进程等权级问题),设置errno

    sg:不推荐直接使用数字,应使用宏名,因为不同操作系统信号编号可能不同,但名称一致。
    pid>0:发送信号给指定的进程。
    pid=0:发送信号给与调用kill函数进程属于同一进程组的所有进程。
    pid<0:取 |pid| 发给对应进程组。
    pid=-1:发送给进程有权限发送的系统中所有进程。

    进程组:

    每个进程都属于一个进程组,进程组是一个或多个进程集合,他们相互关联,共同完成一个实体任务,
    每个进程组都有一个进程组长,默认进程组ID与进程组长ID相同。

    权限保护:

    super用户(root)可以发送信号给任意用户,普通用户是不能向系统用户发送信号的。ki-9(root用
    户的pd是不可以的。同样,普通用户也不能向其他普通用户发送信号,终止其进程。只能向自己创建的进程发
    送信号。普通用户基本规则是:发送者实际或有效用户D=接收者实际或有效用户D

    例子:
    cat|cat|cat|cat|cat
    
    • 1
    ps ajx
       PPID     PID    PGID     SID TTY        TPGID STAT   UID   TIME COMMAND
    
       1847    2676    2676    1847 pts/1       2676 S+    1000   0:00 cat
       1847    2677    2676    1847 pts/1       2676 S+    1000   0:00 cat
       1847    2678    2676    1847 pts/1       2676 S+    1000   0:00 cat
       1847    2679    2676    1847 pts/1       2676 S+    1000   0:00 cat
       1847    2680    2676    1847 pts/1       2676 S+    1000   0:00 cat
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    使用  kill  -9   -2676  会杀死 进程组内所有进程
    
    • 1
    使用   kill -9    2676  也会 杀死所有进程  不过仅限于此种情况。    因为我们杀死的是管道的 写端,在cat|cat|cat|cat 里杀掉一个写端,就杀掉所有写端
    
    • 1

    raise 函数 和 abort 函数

    raise函数:给当前进程发送指定信号(自己给自己发)raise(signo)=kil(getpid(),signo:
    int raise(int sig);成功:0,失败非0值
    abort函数:给自己发送异常终止信号6)SIGABRT信号,终止并产生core文件
    void abort(void);该函数无返回

    alarm 函数

           #include 
    
           unsigned int alarm(unsigned int seconds);
    
    • 1
    • 2
    • 3

    设置定时器(闹钟)。在指定seconds后,内核会给当前进程发送14)SIGALRM信号。进程收到该信号,默认动
    作终止。
    每个进程都有且只有唯一个定时器。
    unsigned int alarm(unsigned int seconds);返回0或剩余的秒数,无失败.
    常用:取消定时器alarm(0) 直接触发信号

    定时,与进程状态无关(自然定时法)!就绪、运行、挂起(阻塞、暂停)、终止、僵尸无论进程处于何种状态,
    alarm都计时。

    练习:编写程序,测试你使用的计算机1秒钟能数多少个数。
    【alarm.c】
    使用time命令查看程序执行的时间。
    程序运行的瓶颈在于1O,优化程序,首选优化10。

    #include
    int main()
    {
    
    int i;
    alarm(1);
    for(i=0;;i++)
    {
    printf("%d\n",i);
    }
    
    
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    time ./alarm
    
    • 1
    real	0m1.001s
    user	0m0.070s
    sys	0m0.465s
    
    
    • 1
    • 2
    • 3
    • 4

    实际执行时间=系统时间+用户时间+等待时间

    这里 sys + user 不等于 real ,是因为大量时间 用在硬件IO 上,可以把内容输出到 文件 ,减少IO 时间。

    time   ./alarm  >> t
    
    • 1
    xxx@xxx-virtual-machine:~/SIG$ time ./alarm >> t
    Alarm clock
    
    real	0m1.001s
    user	0m0.265s
    sys	0m0.735s
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    此时 real = sys +user


    setitimer函数

      int getitimer(int which, struct itimerval *curr_value);
           int setitimer(int which, const struct itimerval *new_value,
                         struct itimerval *old_value);
    
    
    • 1
    • 2
    • 3
    • 4
    第一个参数 which

    ITIMER_REAL:以系统真实的时间来计算,它送出SIGALRM信号。
    ITIMER_VIRTUAL:以该进程在用户态下花费的时间来计算,它送出SIGVTALRM信号。
    ITIMER_PROF:以该进程在用户态下和内核态下所费的时间来计算,它送出SIGPROF信号。
    紧接着的new_value和old_value均为itimerval结构体,先看一下itimerval结构体定义:

    第二个参数结构体定义
     struct itimerval {
                   struct timeval it_interval; /* Interval for periodic timer */
                   struct timeval it_value;    /* Time until next expiration */
               };
    
               struct timeval {
                   time_t      tv_sec;         /* seconds */
                   suseconds_t tv_usec;        /* microseconds */
               };
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    itimeval又是由两个timeval结构体组成,timeval包含tv_sec和tv_usec两部分,其中tv_se为秒,tv_usec为微秒(即1/1000000秒)

    settimer工作机制是,先对it_value倒计时,当it_value为零时触发信号,然后重置为it_interval,继续对it_value倒计时,一直这样循环下去。

    基于此机制,setitimer既可以用来延时执行,也可定时执行。

    注意 将it_value 设置 为0是不会触发信号的,所以要能触发信号,it_value得大于0;如果it_interval为零,只会延时,不会定时(也就是说只会触发一次信号)。

    返回值:

    EFAULT:参数value或ovalue是无效的指针。

    EINVAL:参数which不是ITIMER_REAL、ITIMER_VIRT或ITIMER_PROF中的一个。

    示例一 演示 定时发送信号并捕获
    #include 
    #include 
    #include 
     
    void signalHandler(int signo)
    {
        switch (signo){
            case SIGALRM:
                printf("Caught the SIGALRM signal!\n");
                break;
       }
    }
     
    int main(int argc, char *argv[])
    {
        signal(SIGALRM, signalHandler);
     
        struct itimerval new_value, old_value;
        new_value.it_value.tv_sec = 0;
        new_value.it_value.tv_usec = 1;
        
        new_value.it_interval.tv_sec = 0;
        new_value.it_interval.tv_usec = 200000;
        
        setitimer(ITIMER_REAL, &new_value, &old_value);
        
        for(;;);
         
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    其中的new_value参数用来对计时器进行设置(作为传入参数),it_interval为计时间隔,it_value为延时时长,下面例子中表示的是在setitimer方法调用成功后,延时1微秒便触发一次SIGALRM信号,以后每隔200毫秒触发一次SIGALRM信号。

    第四个参数

    old_value参数,通常用不上,设置为NULL,它是用来存储上一次setitimer调用时设置的new_value值,也就是剩余时间,作为传出参数

    示例二:获得产生时钟信号的剩余时间
    #include 
    #include 
    #include 
    #include 
    #include 
    
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    
    
    #define ERR_EXIT(m) \
        do \
        { \
            perror(m); \
            exit(EXIT_FAILURE); \
        } while(0)
    
    
    int main(int argc, char *argv[])
    {
        struct timeval tv_interval = {1, 0};
        struct timeval tv_value = {1, 0};
        struct itimerval it;
        it.it_interval = tv_interval;
        it.it_value = tv_value;
        setitimer(ITIMER_REAL, &it, NULL);
    
        int i;
        for (i=0; i<10000; i++);
    
    //第一种方式获得剩余时间
        struct itimerval oit;
        setitimer(ITIMER_REAL, &it, &oit);//利用oit获得剩余时间产生时钟信号
        printf("%d %d %d %d\n", (int)oit.it_interval.tv_sec, (int)oit.it_interval.tv_usec, (int)oit.it_value.tv_sec, (int)oit.it_value.tv_usec);
    //第二种方式获得剩余时间
        //getitimer(ITIMER_REAL, &it);
        //printf("%d %d %d %d\n", (int)it.it_interval.tv_sec, (int)it.it_interval.tv_usec, (int)it.it_value.tv_sec, (int)it.it_value.tv_usec);
    
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44

    示例三:每隔一秒发出一个SIGALRM,每隔0.5秒发出一个SIGVTALRM信号

    #include 
    #include 
    #include 
    #include 
    
    void sigroutine(int signo)
    {
            switch (signo) {
            case SIGALRM:
            printf("Catch a signal -- SIGALRM\n ");
            break;
            case SIGVTALRM:
            printf("Catch a signal -- SIGVTALRM\n ");
            break;
            }
            return;
    }
    
    int main()
    {
    
           struct itimerval value,value2;
            printf("process id is %d\n ",getpid());
    
            signal(SIGALRM, sigroutine);
    
            signal(SIGVTALRM, sigroutine);
    
            value.it_value.tv_sec = 1;
    
            value.it_value.tv_usec = 0;
    
            value.it_interval.tv_sec = 1;
    
            value.it_interval.tv_usec = 0;
    
            setitimer(ITIMER_REAL, &value,NULL);
    
    
            value2.it_value.tv_sec = 0;
    
            value2.it_value.tv_usec = 500000;
    
            value2.it_interval.tv_sec = 0;
    
            value2.it_interval.tv_usec = 500000;
    
            setitimer(ITIMER_VIRTUAL, &value2,NULL);
    
            for (;;) ;
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52

    信号集操作函数

    sigset_t set;         //typedef unsigned long 
    
    • 1

    定义一个信号集合

    并用下列函数对自定义信号集合操作

    自定义信号集操作函数
    int sigemptyset(sigset_t *set);

    将某个信号集清0
    成功:0;失败:-1

    int sigfillset(sigset_t *set);

    将某个信号集置1
    成功:0;失败:1

    int sigaddset(sigset_t*set,int signum);

    将某个信号加入信号集
    成功:0:失败:-1

    int sigdelset(sigset_t *set,int signum);

    将某个信号清出信号集
    成功:0;失败:-1

    int sigismember(const sigset_t*set,int signum;

    判断某个信号是否在信号集中
    返回值:在集合:1;不在:0:
    出错:-1

    sigset_t类型的本质是位图。但不应该直接使用位操作,而应该使用上述函数,保证跨系统操作有效。

    sigprocmask函数

    用于将自定义信号集合 影响到 阻塞信号集合.并读取阻塞信号集

    用来屏蔽信号、解除屏蔽也使用该函数。其本质,读取或修改进程的信号屏蔽字(PCB中)
    严格注意,屏蔽信号:只是将信号处理延后执行(延至解除屏蔽);而忽略表示将信号丢处理。

    int sigprocmask(int how,const sigset_t*set,sigset_t*oldse;成功:0;失败:-l,设置errno
    
    • 1

    参数:
    set:传入参数,是一个位图,set中哪位置1,就表示当前进程屏蔽哪个信号。
    oldset:传出参数,保存旧的信号屏蔽集。
    how参数取值:
    假设当前的信号屏蔽字为mask 说明了how可选用的值。注意,不能阻塞SIGKILL和SIGSTOP信号
    1.
    SIG_BLOCK:当how设置为此值,set表示需要屏蔽的信号。相当于mask=mask|set
    2.
    SIG UNBLOCK::当how设置为此,set表示需要解除屏蔽的信号。相当于mask=mask&~set
    3.
    SIG SETMASK:当how设置为此,set表示用于替代原始屏蔽及的新屏蔽集。相当于mask=set
    若,(替换阻塞信号集)

    当把set 参数置为NULL ,how 将无意义, 此时 oldset 返回的就是当前阻塞信号集合

    sigprocmask(0,NULL,&oldset);
    
    • 1
    sigpending 函数

    读取当前进程的未决信号集
    int sigpending(sigset_t*set);set传出参数。返回值:成功:0;失败:-1,设置ermo
    练习:编写程序。把所有常规信号的未决状态打印至屏幕。
    【sigpending,c】

    读取未决信号集合
    #include
    void printped(sigset_t *ped)
    {
    int i;
    for(i=1;i<32;i++)
    {
    
    if(sigismember(ped,i)==1)
    {putchar('1');
    
    }
    else{
    putchar('0');
    }
    }
    printf("\n");
    }
    int main()
    {
    sigset_t myset,oldset,ped;
    sigemptyset(&myset);
    sigaddset(&myset,SIGQUIT);
    sigaddset(&myset,SIGINT);
    sigaddset(&myset,SIGTSTP);
    
    sigaddset(&myset,SIGKILL);
    sigaddset(&myset,SIGSTOP);
    int ret= sigprocmask(SIG_BLOCK,&myset,&oldset);
    if(ret==-1)
    {
    perror("sigporcmask");
    exit(1);
    }
    while(1)
    {
    
    sigpending(&ped);
    printped(&ped);
    
    }
    return 0;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    读取阻塞信号集合
    #include
    void printped(sigset_t *ped)
    {
    int i;
    for(i=1;i<32;i++)
    {
    
    if(sigismember(ped,i)==1)
    {putchar('1');
    
    }
    else{
    putchar('0');
    }
    }
    printf("\n");
    }
    int main()
    {
    sigset_t myset,oldset,ped;
    sigemptyset(&myset);
    sigaddset(&myset,SIGQUIT);
    sigaddset(&myset,SIGINT);
    sigaddset(&myset,SIGTSTP);
    
    sigaddset(&myset,SIGKILL);
    sigaddset(&myset,SIGSTOP);
    int ret= sigprocmask(0,&myset,&oldset);//让阻塞信号集合影响阻塞信号集合
     ret= sigprocmask(0,NULL,&oldset);//读取阻塞信号集合
    if(ret==-1)
    {
    perror("sigporcmask");
    exit(1);
    }
    printped(&oldset);
    return 0;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    如果调用sigprocmask解除了对当前若干个未决信号的阻塞,则在sigprocmask返回前,⾄至少将其中一个信号递达。

    也就是 当屏蔽信号集的信号被解除之后,之前屏蔽的信号将被处理

    #include
    
    
    int main()
    {
        int status;
        pid_t pid=fork();
        if(pid==-1)
        {
            perror("fork ");
            exit(1); 
        }
        if(pid==0)
        {
            printf(" Iam child pid=%d,ppid=%d\n",getpid(),getppid()) ;
            sigset_t set;
            int i = 0;
            sigemptyset(&set);
            sigfillset(&set) ;   //将阻塞信号集 全设置为1
            int ret=    sigprocmask( SIG_SETMASK, &set, NULL); 
            if(ret==-1)
            {
                perror("sigprocmsk");
                exit(1);
            }
            while(1)
            {
                i++;
                printf("run ~~~\n");
    
                sleep(1); //维持15s
                if(i ==15)         //在信号屏蔽字解除x信号
                    sigprocmask(SIG_UNBLOCK, &set, NULL);      //15s 之后才解除屏蔽,期间(8s)父进程 向子进程发送kill信号,但都会被阻塞 
            }
        }
        if(pid>0)
        {
            sleep(1) ;
            printf("---I am father pid=%d  myson pid=%d \n",getpid(),pid) ;
            int i=8;
    
            srand( (unsigned)time( 0 ) );   //随机数
            while(i--)
            {
                sleep(1);
                int irand=rand()%32+1;   // 产生 1 - 32号信号
                if(irand==9||irand==20 )
                {
    irand++;   //防止碰到 9 号和20号信号  不可被 阻塞 ,提前将子进程杀死
                }
                printf("parent kill SIGNAL id:%d\n ",irand);
                kill(pid,irand);   //向子进程发送 信号
            }
            int retw=waitpid(pid,&status,0);
            if(retw==-1)
            {
    
                perror("waitpid");
                exit(1);
            }
            if(WIFSIGNALED(status))
            {
                int num=WTERMSIG(status); //用waitpid 的 status  接收  杀死子进程的那个信号
                printf("child is killed by %d\n",num);
            }
    
            printf("parent  end---------------------------\n");
        }
    
    
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72

    子进程在程序运行前 就屏蔽所有信号。 在程序进行0–8s 内 ,父进程 向子进程不断发送 信号 , 15 s后 子进程解除所有屏蔽。

    此时之前被阻塞的信号 将有一个递达,将子进程杀死。

    xxx@xxx-virtual-machine:~/csdn$ ./test3
     Iam child pid=62438,ppid=62437
    run ~~~
    ---I am father pid=62437  myson pid=62438 
    run ~~~
    run ~~~
    run ~~~
    run ~~~
    run ~~~
    run ~~~
    run ~~~
    run ~~~
    run ~~~
    run ~~~
    run ~~~
    run ~~~
    run ~~~
    run ~~~
    parent kill SIGNAL id:22	 parent kill SIGNAL id:6	 parent kill SIGNAL id:18   parent kill SIGNAL id:28	 parent kill SIGNAL id:21	 parent kill SIGNAL id:18   parent kill SIGNAL id:27	 parent kill SIGNAL id:26	 child is killed by 6
    parent  end---------------------------
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    信号的捕捉

    signal 函数

           #include 
    
           typedef void (*sighandler_t)(int); //信号处理函数
    
           sighandler_t signal(int signum, sighandler_t handler);
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    #include 
    typedef void (*sighandler_t) (int);
    void catch_handle(int signo) //必须要有整形参数
    {
    printf("---catch   signal   SIGINT\n");
    
    }
    int main()
    {
    
    sighandler_t handler;
         handler=signal(SIGINT,catch_handle);
    if(handler == SIG_ERR)
    {
    perror("signal error::");
    exit(1);
    }
    while(1);
    return 0;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    ****注意:****该函数由ANSI定义,由于历史原因在不同版本的Unix和不同版本的Linux中可能有不同的行为。因此应该尽量避免使用它,取而代之使用sigaction函数。

    sigaction 函数

    int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact); 
    
    作用:对某个信号进行注册(同signal),即对某个信号之前对应的处理方式(函数)进行修改。
    
    返回值:成功0;失败-1,设置errno。
    
    参数:
    
    act:传入参数,新的处理方式。
    
    oldact:传出参数,旧的处理方式。
    最后一个参数如果不关心之前的处理方式,可以为NULL
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    struct sigaction结构体:
    struct sigaction {
    
        void     (*sa_handler)(int);
    
        void     (*sa_sigaction)(int, siginfo_t *, void *);
    
        sigset_t   sa_mask;
    
        int       sa_flags;
    
      //  void     (*sa_restorer)(void);
    
    };   //最后一个成员不用(舍弃了);第二成员不常用
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    sigaction 结构体参数 设置

    sa_restorer:该元素是过时的,不应该使用,POSIX.1标准将不指定该元素。(弃用)

    sa_handler:指定信号捕捉后的处理函数名(即注册函数)。也可赋值为SIG_IGN表忽略 或 SIG_DFL表执行默认动作;

    sa_mask: 调用信号处理函数时,所要屏蔽的信号集合(信号屏蔽字)。注意:仅在处理函数被调用期 间屏蔽生效,是临时性设置;sa_mask也是一个字(64位),只是在执行相应的用户处理函数期间生效。即在执行用户处理函数期间, sa_mask屏蔽的信号也不能递达,处于未决状态。如果sa_mask未屏蔽,则响应信号,中断嵌套。相当于此期间,sa_mask代替了mask。

    sa_flags:通常设置为0,表示用默认属性。默认属性即为:sa_mask中将自己屏蔽,即该信号的注册函数执行期间,再次向进程发送该信号,该信号不能递达,处于未决状态。

    SA_SIGINFO 参数设置

    在开头我们看到 struct sigacton结构有又一个 void (*sa_sigaction)(int,siginfo_t *,void *); 字段
    该字段是一个 替代的信号处理函数。
    当我们没有使用 SA_SIGINFO 标志时,调用的是 sa_handler指定的信号处理函数。
    当指定了该标志后,该标志对信号处理函数提供了附加的信息,一个指向siginfo结构的消息和一个指向进程上下文标识符
    的指针这时我们就能调用sa_sigaction指定的信号处理函数
    对于siginfo结构体 XSI规定至少包含下列字段;

    struct siginfo{
     int  si_signo;//信号编号
     int  si_errno;//错误值
     int  si_code;//信号对应的代码值
     pid_t si_pid;//发送信号的进程id
     /*
     后面还有一些这里就不列出了
     */
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
     4 void sig_int(int signo,siginfo_t *info,void *context){
      5     printf("\nget signal:%s\n",strsignal(signo));
      6     printf("signal number is %d\n",info->si_signo);
      7     printf("pid=%d\n",info->si_pid);
      8 }
      9 int main(void){
      10 
      11     struct sigaction new_action;
      12     sigemptyset(&new_action.sa_mask);
      13     new_action.sa_sigaction=sig_int;
      14     new_action.sa_flags=SA_SIGINFO;
      15     if(sigaction(SIGINT,&new_action,NULL)==-1){
      16         printf("set signal process mode\n");
      17         exit(1);
      18     }
      19 
      20     kill(getpid(),SIGINT);
      21     printf("Done\n");
      22     exit(0);
      23 }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    我们使用了 SA_SIGINFO标志 然后使用了 sa_sigaction字段表示的处理函数。
    设置信号处理函数后,进程向自己发送一个中断信号,然后结束。 在信号处理函数可以使用siginfo结构中提供的信息打印信号编号和发送信号进程id

    运行输出如下:

    [feng@ubuntu:~/learn_linux_c_second/chapter_10$](mailto:feng@ubuntu:~/learn_linux_c_second/chapter_10$2> ./a.out &
     [2] 5010
    
    [feng@ubuntu:~/learn_linux_c_second/chapter_10$](mailto:feng@ubuntu:~/learn_linux_c_second/chapter_10$2> get signal:Interrupt
     signal number is 2
     pid=5010
     Done
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    如果 不指定SA_SIGINFO标志我们仍可以使用sa_sigaction字段指定函数,但是应为此时不再提供附加信息,那么我们所有对siginfo的访问都是无效的将造成段错误
    如果将 14 行 改为new_action.sa_flags=0;
    输出如下:

    [feng@ubuntu:~/learn_linux_c_second/chapter_10$](mailto:feng@ubuntu:~/learn_linux_c_second/chapter_10$2> ./a.out &
     [2] 5041
    [feng@ubuntu:~/learn_linux_c_second/chapter_10$](mailto:feng@ubuntu:~/learn_linux_c_second/chapter_10$2> 
     get signal:Interrupt
     ^C
     [2]- Segmentation fault   (core dumped) ./a.out
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    我们看到的确发生了段错误

    sa_flag 参数 设置
    SA_INTERRUPT和**SA_RESTART

    SA_INTERRUPT:由此信号中断的系统调用不会自动重启动(针对sigaction的XSI默认处理方式不会自动重启)
    SA_RESTART: 由此信号中断的系统调用会自动重启。

       我们看下面这段程序:
     4 void sig_int(int signo){
      5         printf("\nint sig_int\n");
      6 }
      7 
      8 int main(void){
      9         struct sigaction new_action;
     10         new_action.sa_handler=sig_int;
     11         if(sigemptyset(&new_action.sa_mask)==-1){
     12                 printf("set new action mask error\n");
     13                 exit(1);
     14         }
     15         new_action.sa_flags=0;
     16 
     17         if(sigaction(SIGINT,&new_action,NULL)==-1){
     18                 printf("set SIGINT process error\n");
     19                 exit(1);
     20         }
     21         char c;
     22         read(0,&c,1);
     23         printf("read :%c\n",c);
     24         exit(0);
     25 }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    第十五行中我们没有使用任何标志。设置捕捉 SIGINT 信号,然后读标准输入

    输出如下:

       [feng@ubuntu:~/learn_linux_c_second/chapter_10$](mailto:feng@ubuntu:~/learn_linux_c_second/chapter_10$2> ./a.out
       ^C
       int sig_int
       read :?
    
    • 1
    • 2
    • 3
    • 4

    我们在输入任何字符之前就 按下中断键(ctrl+c),进程捕捉到SIGINT信号,然后打印信息后就终止了。read的输出内容是c变量位置处本来
    存在的数据并不是我们输入的。
    从输出可以看出,read调用被中断后,没有再重启。这说明我的系统默认就是 对被中断的系统调用不会自动重启。

    所以我们将 第十五行 改为 new_action.sa_flags=SA_INTERUPT;
    可以预测输出情况和上面的是一样的

    [feng@ubuntu:~/learn_linux_c_second/chapter_10$](mailto:feng@ubuntu:~/learn_linux_c_second/chapter_10$2> ./a.out
      ^C
      int sig_int
      read :?
    
    • 1
    • 2
    • 3
    • 4

    从输出我们看到 的确是一样的。

    从输出我们看到 的确是一样的。

    现在 我们将 第十五行改为 new_action.sa_flags=SA_RESTART; 再看看输出情况

    [feng@ubuntu:~/learn_linux_c_second/chapter_10$](mailto:feng@ubuntu:~/learn_linux_c_second/chapter_10$2> ./a.out
     ^C
     int sig_int
     ^C
     int sig_int
     ^C
     int sig_int
     ^\Quit (core dumped)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    从输出中我们看到 当多次按下 中断键后 进程捕捉到信号并打印消息,但是进程并未结束,而是继续等待输入。按下退出键时进程才退出
    也就是说 使用了 SA_RESTART标志后 read调用被中断后 会自动重启 继续等待中断输入。

    从输出中我们看到 当多次按下 中断键后 进程捕捉到信号并打印消息,但是进程并未结束,而是继续等待输入。按下退出键时进程才退出
    也就是说 使用了 SA_RESTART标志后 read调用被中断后 会自动重启 继续等待中断输入。

    SA_NOCLDSTOP:

    一般当进程终止或停止时都会产生SIGCHLD信号,但若对SIGCHLD信号设置了该标志,当子进程停止时不产生此信号。当子进程终止时才产生此信号

    我们看下面这段代码:

      4 void sig_chld(int signo){
      5     printf("get signal:%s\n",strsignal(signo));
      6 }
      7 
      8 int main(void){
      9     pid_t pid;
      10 
      11     struct sigaction new_action,old_action;
      12     new_action.sa_handler=sig_chld;
      13     if(sigemptyset(&new_action.sa_mask)==-1){
      14         printf("empty new_action.sa_mask error\n");
      15         exit(1);
      16     }
      17     new_action.sa_flags=0;
      18     sigaction(SIGCHLD,&new_action,&old_action);
      19 
      20     if((pid=fork())==-1){
      21         perror("fork error");
      22         exit(1);
      23     }else if(pid==0){
      24         if(kill(getpid(),SIGSTOP)==-1){
      25             printf("send SIGSTOP to child error\n");
      26             exit(1);
      27 
      28         }
      29         printf("child done\n");
      30     }else{
      31         sleep(1);
      32         kill(pid,SIGCONT);
      33         sleep(2);
      34         printf("parent done\n");
      35 
      36     }
      37     exit(0);
      38 }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35

    我们捕捉 SIGCHLD 信号 但 第十七行中 我们没有使用任何标志。我们期望让子进程先暂停(所以父进程先睡眠一秒),这时父进程应该收到一个SIGCHLD然后父进程发送SIGCONT
    信号给子进程让他继续运行,然后父进程休眠(为了在子进程之后终止),子进程运行并终止,此时父进程应该受到一个SIGCHLD信号中断睡眠,然后
    结束。
    我们看下输出情况:

    [feng@ubuntu:~/learn_linux_c_second/chapter_10$](mailto:feng@ubuntu:~/learn_linux_c_second/chapter_10$2> ./a.out
     get signal:Child exited
     child done
     get signal:Child exited
     parent done
    
    • 1
    • 2
    • 3
    • 4
    • 5

    我们捕捉 SIGCHLD 信号 但 第十七行中 我们没有使用任何标志。我们期望让子进程先暂停(所以父进程先睡眠一秒),这时父进程应该收到一个SIGCHLD然后父进程发送SIGCONT
    信号给子进程让他继续运行,然后父进程休眠(为了在子进程之后终止),子进程运行并终止,此时父进程应该受到一个SIGCHLD信号中断睡眠,然后
    结束。
    我们看下输出情况:
    [feng@ubuntu:/learn_linux_c_second/chapter_10$](mailto:feng@ubuntu:/learn_linux_c_second/chapter_10$2> ./a.out
    get signal:Child exited
    child done
    get signal:Child exited
    parent done

    正如 我们预测的。父进程先收到一次子进程暂停是而产生的SIGCHLD,然后当子进程结束时,父进程又接收到一次。
    注意这段程序是有问题的,它存在一个竞争条件。如果fork是实现是父进程先执行而系统非常繁忙,可能父进程睡眠一秒后子进程还未运行。那么SIGCONT信号将
    丢失,然后当子进程停止时,父进程收到信号中断睡眠(第二次的睡眠)然后结束进程。那么 子进程在结束前一直就是出于停止状态。我们也不会看到子进程打印的"child done"。
    ,如果你的系统是父进程先运行,你可以去掉父进程中的 sleep(1)实验一下。输出中是不会出现子进程打印的"child done")

    现在我们修改下 第十七行 中的标志选项 new_action.sa_flags=SA_NOCLDSTOP;

     输出如下:
    [feng@ubuntu:~/learn_linux_c_second/chapter_10$](mailto:feng@ubuntu:~/learn_linux_c_second/chapter_10$2> ./a.out
     child done
     get signal:Child exited
     parent Done
    
    • 1
    • 2
    • 3
    • 4
    • 5

    正如我们期望的,当 子进程暂停时。父进程并未受到 SIGCHLD信号。只在子进程结束时才收到一次。

    SA_NOCLDWAIT

    SA_NOCLDWAIT:若信号是 SIGCHLD时,当使用此标志时,1 当调用进程的子进程终止时不创建僵尸进程。2若调用进程在后面调用wait。则调用进程阻塞,直到****其所有子进程都终止

    对于第一个影响(当调用进程的子进程终止时不创建僵尸进程) 我们先给一个测试例子:

      4 int main(void){
      5     pid_t pid;
      6 
      7     struct sigaction action;
      8     sigemptyset(&action.sa_mask);
      9     action.sa_flags= 0;
      10 
      11     if(sigaction(SIGCHLD,&action,NULL)==-1){
      12         printf("sigaction error\n");
      13         exit(1);
      14     }
      15 
      16     if((pid=fork())==-1){
      17         printf("fork error\n");
      18         exit(1);
      19     }else if(pid==0){
      20         exit(0);
      21     }
      22     else {
      23         sleep(10);
      24     }
      25     exit(0);
      26 }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    第九行中 我们 没有使用任何标志。当子进程结束时,父进程并没有获取获取他的终止状态而是继续睡眠。 那么我们预测,子进程会成为僵尸进程

     输出如下:
    [feng@ubuntu:~/learn_linux_c_second/chapter_10$](mailto:feng@ubuntu:~/learn_linux_c_second/chapter_10$2> ./a.out &
     [2] 4369
    [feng@ubuntu:~/learn_linux_c_second/chapter_10$](mailto:feng@ubuntu:~/learn_linux_c_second/chapter_10$2> ps 4369 4370
      PID TTY   STAT  TIME COMMAND
      4369 pts/2  S   0:00 ./a.out
      4370 pts/2  Z   0:00 [a.out]
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    第九行中 我们 没有使用任何标志。当子进程结束时,父进程并没有获取获取他的终止状态而是继续睡眠。 那么我们预测,子进程会成为僵尸进程
    输出如下:
    [feng@ubuntu:/learn_linux_c_second/chapter_10$](mailto:feng@ubuntu:/learn_linux_c_second/chapter_10KaTeX parse error: Expected 'EOF', got '&' at position 12: 2> ./a.out &̲ [2] 4369 [fen…](mailto:feng@ubuntu:~/learn_linux_c_second/chapter_10$2> ps 4369 4370
    PID TTY STAT TIME COMMAND
    4369 pts/2 S 0:00 ./a.out
    4370 pts/2 Z 0:00 [a.out]

    我们在后台运行程序后,查看 父子进程的状态。正如预测的一样,父进程处于睡眠状态而子进程成为僵尸进程

    如果我们将第九行 改为: action.sa_flags= SA_NOCLDWAIT;

     输出如下:
    [feng@ubuntu:~/learn_linux_c_second/chapter_10$](mailto:feng@ubuntu:~/learn_linux_c_second/chapter_10$2> ./a.out &
     [2] 4410
    [feng@ubuntu:~/learn_linux_c_second/chapter_10$](mailto:feng@ubuntu:~/learn_linux_c_second/chapter_10$2> ps 4410 4411
      PID TTY   STAT  TIME COMMAND
      4410 pts/2  S   0:00 ./a.out
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    从输出中我们看到,没有子进程的 状态消息。说明子进程已经正常终止了,并未产生僵尸进程

    现在正对第二个影响(若调用进程在后面调用wait。则调用进程阻塞,直到其所有子进程都终止),我们在给出测试例子

      4 int main(void){
      5     pid_t pid;
      6 
      7     struct sigaction action;
      8     sigemptyset(&action.sa_mask);
      9     action.sa_flags= 0;
      10     if(sigaction(SIGCHLD,&action,NULL)==-1){
      11         printf("sigaction error\n");
      12         exit(1);
      13     }
      14 
      15     if((pid=fork())==-1){
      16         printf("fork error\n");
      17         exit(1);
      18     }else if(pid==0){
      19         sleep(2);
      20     }
      21     else {
      22         pid=fork();
      23         if(pid==0){
      24             sleep(5);
      25         }
      26         else{
      27             wait(NULL);
      28             printf("father done\n");
      29         }
      30     }
      31     exit(0);
      32 }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    同样第九行,我们没有使用任何标志。我们先创建第个子进程他只睡眠2秒。然后父进程创建第二个子进程。他休息5秒。之后父进程调用wait阻塞等待任意子进程结束
    。应为 wait等待任意子进程结束时就会返回,那么我们预测。父进程阻塞2秒左右就会结束。

    同样第九行,我们没有使用任何标志。我们先创建第个子进程他只睡眠2秒。然后父进程创建第二个子进程。他休息5秒。之后父进程调用wait阻塞等待任意子进程结束
    。应为 wait等待任意子进程结束时就会返回,那么我们预测。父进程阻塞2秒左右就会结束。

    输出如下:

    [feng@ubuntu:~/learn_linux_c_second/chapter_10$](mailto:feng@ubuntu:~/learn_linux_c_second/chapter_10$2> ./a.out
     father done
    
    • 1
    • 2

    运行时,会发现只阻塞了两秒左右。

    运行时,会发现只阻塞了两秒左右。

    现在我们将 第九行改为: action.sa_flags= SA_NOCLDWAIT;
    输出如下:

    [feng@ubuntu:~/learn_linux_c_second/chapter_10$](mailto:feng@ubuntu:~/learn_linux_c_second/chapter_10$2> ./a.out
     father done
    
    • 1
    • 2

    运行程序 我们明显能感觉到阻塞了 五秒左右才有输出。也就是说 父进程一直等待到第二个子进程结束时wait才返回。

    运行程序 我们明显能感觉到阻塞了 五秒左右才有输出。也就是说 父进程一直等待到第二个子进程结束时wait才返回。

    SA_NODEFER:
    SA_RESETHAND:
    这两个标志对应早期的不可靠信号机制,除非明确要求使用早期的不可靠信号,否则不应该使用。这里也不多做介绍

    (3)信号捕捉机制

    进程正常运行时,默认PCB中有一个信号屏蔽字,假定为mask,它决定了进程自动屏蔽哪些信号。当注册了某个信号捕捉函数,捕捉到该信号以后,要调用该函数。而该函数有可能执行很长时间,在这期间所屏蔽的信号不由mask来指定。而是用sa_mask来指定。调用完信号处理函数,再恢复为mask。

    sa_flags为0时,XXX信号捕捉函数执行期间,多个XXX信号产生自动被屏蔽。

    阻塞的常规信号(1-31)不支持排队,产生多次只记录一次。(后32个实时信号支持排队)

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lEzh2k8i-1667951850682)(C:\Users\User\AppData\Roaming\Typora\typora-user-images\image-20220731163133314.png)]

    首先,处于用户态(user mode)的某个进程在执行到某个指令时突然接收某个信号(软中断,终端按键产生;硬件异常产生;命令产生;系统调用产生或者软件条件产生),会暂停执行下一条指令而陷入内核进入内核态。

    内核在处理这一异常后,在准备会用户态之前先处理可以递达该进程的信号。

    如果该信号的处理方式为捕捉,则内核对该信号进行捕捉,同时调用相应的用户处理函数,回到用户态执行相应的用户处理函数(注意不是回到主控制流程)。

    在用户处理函数执行完返回时,再次执行系统调用sigretum再次进入内核。因为函数执行完需要返回到该函数的调用点,而该函数是内核调用的,因此需要再次返回到内核。

    最后,从内核再次返回到用户模式,从上次中断处继续执行下一条指令

    #include 
    #include 
    #include 
    #include 
     
    void docatch(int signo)  //用户处理函数
    {
        printf("the %dth signal is catched\n", signo);
        sleep(10); 
        printf("-------finish------\n");
    }
    int main(void)
    {
        int ret;
        struct sigaction act;
     
        act.sa_handler = docatch;
        sigemptyset(&act.sa_mask);
        sigaddset(&act.sa_mask, SIGQUIT);  //sa_mask屏蔽字中,3号信号置1
        act.sa_flags = 0;  //默认属性   信号捕捉函数执行期间,自动屏蔽本信号
     
        ret = sigaction(SIGINT, &act, NULL);   //注册2号信号
        if (ret == -1) {
            perror("sigaction error");
            exit(1);
        }
     
        while (1);
     
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    [root@localhost 01_signal_test]# ./test_sigac
    
    ^Cthe 2th signal is catched   // 发2号信号 Ctrl +C
    
    -------finish------
    
    ^Cthe 2th signal is catched  // 发2号信号 Ctrl +C
    
    ^C^C^C^C^C^C^C^C^C^C^C^C^C-------finish------   // 执行期间,发多个2号信号
    
    the 2th signal is catched
    
    -------finish------            //但是只是执行了一次
    
    ^Cthe 2th signal is catched
    
    ^\^\^\^\^\^\^\^\^\^\^\^\-------finish------   // 执行期间,发多个3号信号
    
    Quit (core dumped)   //2号信号处理完后,处理2号,则退出进程,结束。
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    pause 函数

    调用该函数可以造成进程主动挂起,等待信号唤醒。调用该系统调用的进程将处于阻塞状态(

    主动放弃处理及直到有信号递达将其唤醒。)
    int pause(void); 返回值:-1并设置errno为EINTR

    返回值:
    1   如果信号的默认处理动作是终止进程,则进程终止,pause 函数没有机会返回。
    2   如果信号的默认处理动作是忽略,进程继续处于挂起状态,pause 函数不返回。
    3   如果信号的处理动作是捕捉,则【调用完信号处理函数之后,pause 返回-1】
    4   pause 收到的信号不能被屏蔽,如果被屏蔽,那么pause就不能被唤醒。
    
    • 1
    • 2
    • 3
    • 4
    • 5

    当使用pause 函数主动挂起一个进程, 此时若有 init 信号 ,虽然pause 函数 会被唤醒,但是进程将会被 init 信号终止,因此,就必须对信号进行捕获。

    时序竞态

    用 pause 和 alarm 函数 模拟 sleep 函数

    #include
    void catch_sigalarm(int signo)      
    {
        ;   //只捕获,不让其执行其默认动作(终止)
    }
    
    unsigned int mysleep(unsigned int seconds)
    {
        struct sigaction act ,oldact; 
        act.sa_handler=catch_sigalarm;
        sigemptyset(&act.sa_mask);
        act.sa_flags=0;
        act.sa_flags=0;
        int ret=   sigaction(SIGALRM,&act,&oldact);    
        if(ret==-1)
        {
            perror("sigaction") ;
            exit(1);
        }
        alarm(seconds);
        ret=pause();
        if(ret==-1&&errno==EINTR)
        {
            printf("pause success");
        }
        ret=    alarm(0);//防止出现异常提前收到信号,而出现还在计时的情况
        sigaction(SIGALRM,&oldact,NULL);
        return ret;
    }
    int main()
    {
    
        while(1)
        {
            mysleep(3);
            printf("--------------------------\n");
    
        }
        return 0;
    
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    出现问题

    时序竞态前导例
    在讲时序竞态具体现象之前,我们先来看一个生活中常见的场景:

    想午睡10分钟,于是定了个10分钟的闹钟,希望10分钟后闹钟将自己叫醒。

    正常情况:定好闹钟,午睡,10分钟后闹钟叫醒自己;

    异常情况:定好闹钟,躺下睡觉2分钟,被同学叫醒去打球,打了20分钟后回来继续睡觉。但在打球期间,闹钟早就响过了,将不会再唤醒自己。

    这个例子与之后要讲的时序竞态有很大的相似之处。

    出现的问题

    1  SIGALRM默认动作是终止进程,因此我们要将其捕捉,对SIGALRM注册信号处理函数;
    
    2 调用alarm(3)函数定时3秒钟;
    
    3 alarm(3)调用结束,定时器开始计时。就在这时,进程失去CPU,进入就绪态等待CPU(相当于被同学叫醒去打球)。失去CPU的方式有可能是内核调度了优先级更高的进程取代了当前进程,使得当前进程无法获得CPU;
    
    4  我们知道,alarm函数如果采用自然定时法的话,定时器将一直计时,与进程状态无关。于是,1秒后,闹钟定时时间到,内核向当前进程发送SIGALRM信号。高优先级进程尚未执行完毕,当前进程仍然无法获得CPU,继续处于就绪态,信号无法处理(处于未决状态);
    
    5 优先级高的进程执行完毕,当前进程获得CPU资源,内核调度回当前进程执行。SIGALRM信号递达,并被进程处理;
    
    6 信号处理完毕后,返回当前主控流程,并调用pause()函数,挂起等待alarm函数发送的SIGALRM信号将自己唤醒;
    
    7 但实际SIGALRM信号已经处理完毕,pause()函数永远不会等到。
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    通过以上时序分析,我们可以看出,造成时序竞态的原因就是SIGALRM信号在进程失去CPU的时候就已经发送过来。为了防止这个现象出现,我们可以先将该信号阻塞,将其“抓住”,再在解除阻塞的时候立刻调用pause函数挂起等待。这样即使在调用alarm就失去CPU,也可以在进程重新获得CPU时将抓到的SIGALRM信号重新“放出来”,并将之后的pause函数唤醒。

    但在解除阻塞与pause等待挂起信号之间,还是有可能失去CPU,除非将这两个步骤做成一个“原子操作”。Linux系统提供的sigsuspend函数就具备这个功能。所以,在时序要求比较严格的场合下都应该使用sigsuspend函数,而非pause函数。


    改进方法

    sigsuspend 函数

    int sigsuspend(const sigset_t *mask);

    函数作用:

    挂起等待信号;

    函数参数:

    mask,传入参数,sigsuspend函数调用期间,进程信号屏蔽字由参数mask指定。

    具体用法:可将某个信号(如SIGALRM)从临时信号屏蔽字mask中删除,也就是在调用sigsuspend函数时对该信号解除屏蔽,然后挂起等待信号。但我们此时已经改变了进程的信号屏蔽字,所以调用完sigsuspend函数之后,应将进程的信号屏蔽字恢复原样。

    #include2#include3#include4
     void sig_alrm(int signo)
     {
         /* nothing to do */
     }
     
    10unsigned int mysleep(unsigned int nsecs)
    {
        struct sigaction newact, oldact;
        sigset_t newmask, oldmask, suspmask;
        unsigned int unslept;
    
        //1.为SIGALRM设置捕捉函数,一个空函数
        newact.sa_handler = sig_alrm;
        sigemptyset(&newact.sa_mask);
        newact.sa_flags = 0;
        sigaction(SIGALRM, &newact, &oldact);
    
        //2.设置阻塞信号集,阻塞SIGALRM信号
        sigemptyset(&newmask);
        sigaddset(&newmask, SIGALRM);
       sigprocmask(SIG_BLOCK, &newmask, &oldmask);   //信号屏蔽字 mask
    
        //3.定时n秒,到时后可以产生SIGALRM信号
        alarm(nsecs);
    
        /*4.构造一个调用sigsuspend临时有效的阻塞信号集,
         *  在临时阻塞信号集里解除SIGALRM的阻塞*/
        suspmask = oldmask;
        sigdelset(&suspmask, SIGALRM);
    
        /*5.sigsuspend调用期间,采用临时阻塞信号集suspmask替换原有阻塞信号集
         *  这个信号集中不包含SIGALRM信号,同时挂起等待,
         *  当sigsuspend被信号唤醒返回时,恢复原有的阻塞信号集*/
        sigsuspend(&suspmask); 
    
        unslept = alarm(0);
        //6.恢复SIGALRM原有的处理动作,呼应前面注释1
        sigaction(SIGALRM, &oldact, NULL);
    
        //7.解除对SIGALRM的阻塞,呼应前面注释2
        sigprocmask(SIG_SETMASK, &oldmask, NULL);
    
        return(unslept);
    }
    
    int main(void)
    {
        while(1){
            mysleep(2);
            printf("Two seconds passed\n");
        }
    
        return 0;
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55

    基于时序竞态的全局变量的异步IO

    全局变量的异步IO问题属于时序竞态问题中比较常见的一种。由于kernel的调度关系,涉及全局变量控制的程序逻辑跑出设计者预计之外的结果。如(主控程序和信号捕捉函数的用户处理函数)对同一个变量进行修改,由于kernel调度的缘故,执行顺序与预期的不一致,导致程序没达到设计目的。

    (避免父子进程同时对全局变量进行写操作)

    接下去同样借助一个例子来说明。程序设计目的是,主程序和子程序交替数数。

    #ifndef LINUX_UTILS_H
    #define LINUX_UTILS_H
    
    #include 
    #include 
    #include 
    
    /*!
     * 检查系统调用返回值
     * @param x 返回值
     * @param msg 错误提示语句
     * @param y 错误状态,默认为-1
     */
    bool check_error(int x, const std::string &msg = "error", int y = -1);
    /*!
     * 清零mask,并将il中的信号加入到mask中
     * @param mask
     * @param il
     */
    void add2mask(sigset_t *mask, std::initializer_list<int> il);
    /*!
     * 将il中的信号从mask中删除
     * @param mask
     * @param il
     */
    void del2mask(sigset_t *mask, std::initializer_list<int> il);
    
    /*!
     * 向阻塞信号集里面添加信号
     * @param oldset
     * @param il
     */
    void add2procmask(std::initializer_list<int> il);
    
    /*!
     *  从阻塞信号集里面删除信号
     * @param il
     */
    void del2procmask(std::initializer_list<int> il);
    
    #endif //LINUX_UTILS_H
    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43

    2.1 父子进程交替数数

    #include 
    #include 
    #include 
    #include 
     
    int n = 0, flag = 0;  ;
    //pid_t pid;
     
    void sys_err(char *str)
    {
        perror(str);
        exit(1);
    }
     
    void child_handle_signal(int num)
    {
        printf("I am child  %d\t%d\n", getpid(), n);
        n += 2;
        flag = 1;
        //sleep(1);
        //kill(getppid( ) , SIGUSR2);
    }
     
    void parent_handle_signal(int num)
    {
        printf("I am parent %d\t%d\n", getpid(), n);
        n += 2;
        flag = 1;
        //sleep(1);
        //kill(pid , SIGUSR1);
    }
     
    int main(void)
    {
        struct sigaction act;
     
        if ((pid = fork()) < 0)
            sys_err("fork");
     
        else if (pid > 0) {
            n = 1;
            sleep(1);
            act.sa_handler = do_sig_parent;
            sigemptyset(&act.sa_mask);
            act.sa_flags = 0;
            sigaction(SIGUSR2, &act, NULL);            
            parent_handle_signal(0);
     
            while(1) {
                    if(flag == 1)
                    {
                        kill(pid , SIGUSR1);
                                        //--->由于kernel 调度失去CPU时间
                        flag = 0;
                    }
                ;
            }
        } else if (pid == 0){
            n = 2;
            act.sa_handler = do_sig_child;
            sigemptyset(&act.sa_mask);
            act.sa_flags = 0;
            sigaction(SIGUSR1, &act, NULL);
     
            while(1) {
                     if(flag == 1)
                    {
                        kill(getppid() , SIGUSR2);
                                    //--->由于kernel 调度失去CPU时间
                        flag = 0;
                    }
                ;
            }
          }
     
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77

    正如代码//—>由于kernel 调度失去CPU时间中指出的一样,由于调度失去CPU时间,没有按照设计修改全局变量flag。如父进程在//—>由于kernel 调度失去CPU时间 失去了CPU时间,而子进程获取CPU时间,执行完信号处理函数中的数数行为,又完成了主进程中的发送信号函数。那么在父进程再次获得CPU时,先执行了信号处理函数中的数数行为,在把上次中断的//—>由于kernel 调度失去CPU时间之后的语句执行完,程序就走样了。这样就造成时序竞态问题。

    解决问题的方法:是在程序编辑过程中,就主动避免全局变量异步IO的问题。另一个方法就是加锁操作。

    捕获 SIGCHID 信号

    sigchld 函数的产生

    ==================子进程的状态发生改变就会产生。

    子进程终止时
    子进程接收到 SIGSTOP信号停止时
    子进程处在停止态,接受到 SIGCONT 后唤醒时

    Linux创建多个子进程并通过捕获SIGCHLD信号进行非阻塞回收

    我们通过fork函数创建多个子进程,并通过exec函数族在子进程中进行其他的工作,但是为了避免僵尸进程,我们要对子进程进行回收。常用的回收方式是wait或者waitpid进行阻塞回收,因为如果非阻塞回收很难把握时机,而阻塞回收将导致父进程无法进行其他的工作。通过子进程状态改变后会发送一个SIGCHLD信号这一机制,我们可以在父进程中将这一信号进行捕获然后进行非阻塞的回收子进程并保证能够回收所有的,也不需要通过sleep函数去强制保证异步。

    通过捕获SIGCHLD信号进行回收子进程最害怕的就是父进程还没有设置完捕获函数,子进程全部都死翘翘了,然后父进程就等不到SIGCHLD信号,无法开始回收进程。为了避免这种情况,一般的解决方法是首先对子进程进行一个sleep等待父进程设置捕获函数,我觉得这种做法十分低效,我想到的解决方式是在fork函数前就对SIGCHLD信号进行屏蔽,等父进程设置好捕获函数后再解除屏蔽,这样就不会错过SIGCHLD信号啦。

    代码如下:
    Utils.h:封装了一些简单的操作,简化代码,实现放在文末

    int &wait_child_num() {
        static int num = 0;
        return num;
    }
    //当有多个进程同时死亡(状态发送改变,发出sigchid信号)
    void wait_child(int signum) { //当有多个进程同时发出sigchid 信号,只会被捕获一次信号(普通信号不支持排队)。
        pid_t pid;
        int wstatus;
        while ((pid = waitpid(0, &wstatus, WNOHANG)) > 0) {   // 在这些同时发出sigchid 信号的进程中一次只会有一个被waitpid 处理,其他的进程将挂起,使用while循环,就能让多个进程被处理(也就是进一次信号捕捉函数,处理多个进程)  。如果使用if 语句,处理完一个信号就退出信号捕捉函数了,
            ++wait_child_num();
            if (WIFEXITED(wstatus)) {
                cout << "process[" << pid << "] exited with " << WEXITSTATUS(wstatus) << endl;
            } else {
                cout << "process[" << pid << "] was terminated by signal " << WTERMSIG(wstatus) << endl;
            }
        }
    }
    
    int test_wait() {
        int idx;
        pid_t pid;
        constexpr int N = 5;
        /*!
         * 在fork前应该将SIGALRM信号加入阻塞信号集,否则父进程还没有来得及设置信号捕捉函数回收子进程,他们全都死亡了,回收了个寂寞
         */
        add2procmask({SIGCHLD});
        
        for (idx = 0; idx < N; ++idx) {
            pid = fork();
            check_error(pid, "fork error");
            if (pid == 0)
                break;
        }
        if (idx == N) {
            //父进程
            //注册SIGALRM信号捕捉函数
            struct sigaction act, oldact;
            act.sa_flags = 0;
            add2mask(&act.sa_mask, {SIGINT, SIGQUIT, SIGTSTP});
            act.sa_handler = wait_child;
            check_error(sigaction(SIGCHLD, &act, &oldact), "sigaction error"); //捕获异常退出信号
            //解除对SIGALRM的屏蔽
            del2procmask({SIGCHLD}); //保证SIFALRM能被捕获到
            cout << "begin to wait for children" << endl;
            while (wait_child_num() < N);
            check_error(sigaction(SIGCHLD, &oldact, nullptr), "sigaction error");//捕获sigchid信号
        } else {
            my_sleep(idx, 0);//子进程睡眠 idx s
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51

    通过wait_child_num返回一个局部静态变量num引用获取回收了的子进程的个数,虽然在捕获函数中使用静态变量将导致捕获函数不再是一个可重入函数,但是因为在我的代码中只有捕获函数会对num进行写操作,因此不会发生全局变量异步IO,而且在捕获信号期间会对SIGCHLD信号屏蔽(通过设置sigaction结构体的sa_flags为0),也不用担心会发生重入。

    之所以将其变成一个局部静态变量而不是直接使用一个静态变量是 Effective C++ 条款18:让接口容易被正确使用的建议,尽可能使用局部静态变量,因为这样一方面可以避免名字污染,另一方面可以避免初始化次序问题,当在多个文件中的时候确保使用到该变量时能够被初始化。

    Utils.cpp:工具函数的实现,非常简单

    #include "utils.h"
    
    using std::string;
    
    bool check_error(int x, const string &msg, int y) {
        if (x == y) {
            perror(msg.c_str());
            exit(1);
        }
        return true;
    }
    
    void add2mask(sigset_t *mask, std::initializer_list<int> il) {
        check_error(sigemptyset(mask), "sigemptyset error");
        for (auto signum : il) {
            check_error(sigaddset(mask, signum), "sigaddset error");
        }
    }
    
    void del2mask(sigset_t *mask, std::initializer_list<int> il) {
        for (auto signum : il) {
            check_error(sigdelset(mask, signum), "sigdelset error");
        }
    }
    
    void add2procmask(std::initializer_list<int> il) {
        sigset_t mask;
        add2mask(&mask, il);
        check_error(sigprocmask(SIG_BLOCK, &mask, nullptr), "sigprocmask error");
    }
    
    void del2procmask(std::initializer_list<int> il) {
        sigset_t mask;
        add2mask(&mask, il);
        check_error(sigprocmask(SIG_UNBLOCK, &mask, nullptr), "sigprocmask error");
    }
    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38

    终端

    在 UN1X 系统中,用户通过终端登录系统后得到一个Shell进程,这个终端成为 Shell 进程的控制终端(Controlling
    进程的控制终端也是这个终端。默认情况下(没有重定向),每个进程的标准输入、标准输出和标准错误输出都指
    向控制终端,进程从标准输入读也就是读用户的键盘输入,进程往标准输出或标准错误输出写也就是输出到显示器
    上。信号中还讲过,在控制终端输入一些特殊的控制键可以给前台进程发信号,例如Ctrl-C表示 SIGINT,Ctrl-\表示 SIGQUIT。
    Alt + Ctrl + F1、F2、F3、F4、F5、F6字符终端
    pts(pseudo terminal slave) 指伪终端。

    Alt + F7
    图形终端

    SSH、Telnet…
    网络终端


    终端启动步骤

    每个讲程都可以通过一个的设备文件/dev/tty访问它的控制终端。事实上每个终端设备都对应一个不同的设备文件,/dev/tty提供了一个通用的接口,一个进程要访问它的控制终端既可以通过/dev/tty
    出可以通过该终端设备所对应的设备文件来访问。ttvnam由文件描述符杏出对应的文件名,该文件描述
    符必须指向一个终端设备而不能是任意文件。
    简单来说,一个Linux 系统启动,大致经历如下的步骤:

    init --> fork --> exetty -→>用户输入帐号 -->> login -→ 输入密码 ->exec--> bash
    
    • 1

    硬件驱动程序货页读与实际的硬件设备,比如硬盘读入字符和把字付 前出到显,线路规程像一个过据器,
    对于某些特殊字符并不是让它直接通过,而是做特殊处理,比如在键盘上按下 Ctrl-z,对应的字符并不会被用户程
    序的read 读到,而是被线路规程截获,解释成SIGTSTP信号发给前台进程,通常会使该进程停止。线路规程应该过滤哪些字符和做哪些特殊处理是可以配置的。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5qCTjXY3-1667951850683)(C:\Users\User\AppData\Roaming\Typora\typora-user-images\image-20220815092747767.png)]

    line disciline: 线路规程,用来过滤键盘输入的内容。

    ttyname 函数

    由文件描述符查出对应的文件名

    char *ttyname(int fd); 成功:终端名;失败:NULL,设置errno
    
    • 1

    下面我们借助ttyname 函数,通过实验看一下各种不同的终端所对应的设备文件名。

    下面我们借助ttyname 函数,通过实验看一下各种不同的终端所对应的设备文件名。

    
    #include 
    #include 
    int main(void)
    {
    I
    printf("fd O: %s\n", ttymame(0));
    printf("fd 1: %s\n", ttyname(1));
    printf("fd 2: %s\n", ttymame(2));
    return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    进程组

    前提

    进程都有父进程,父进程也有父进程,这就形成了一个以init进程为根的家族树。除此以外,进程还有其他层次关系:进程、进程组和会话。
    进程组和会话在进程之间形成了两级的层次:进程组是一组相关进程的集合,会话是一组相关进程组的集合。
    这样说来,一个进程会有如下ID:
    ·PID:进程的唯一标识。对于多线程的进程而言,所有线程调用getpid函数会返回相同的值。
    ·PGID:进程组ID。每个进程都会有进程组ID,表示该进程所属的进程组。默认情况下新创建的进程会继承父进程的进程组ID。
    ·SID:会话ID。每个进程也都有会话ID。默认情况下,新创建的进程会继承父进程的会话ID。
    可以调用如下指令来查看所有进程的层次关系

    ps -ejH
    ps axjf 
    
    • 1
    • 2

    前面提到过,新进程默认继承父进程的进程组ID和会话ID,如果都是默认情况的话,那么追根溯源可知,所有的进程应该有共同的进程组ID和会话ID。但是调用ps axjf可以看到,实际情况并非如此,系统中存在很多不同的会话,每个会话下也有不同的进程组。
    为何会如此呢?
    就像家族企业一样,如果从创业之初,所有家族成员都墨守成规,循规蹈矩,默认情况下,就只会有一个公司、一个部门。但是也有些“叛逆”的子弟,愿意为家族公司开疆拓土,愿意成立新的部门。这些新的部门就是新创建的进程组。如果有子弟“离经叛道”,甚至不愿意呆在家族公司里,他别开天地,另创了一个公司,那这个新公司就是新创建的会话组。由此可见,系统必须要有改变和设置进程组ID和会话ID的函数接口,否则,系统中只会存在一个会话、一个进程组。
    进程组和会话是为了支持shell作业控制而引入的概念。
    当有新的用户登录Linux时,登录进程会为这个用户创建一个会话。用户的登录shell就是会话的首进程。会话的首进程ID会作为整个会话的ID。会话是一个或多个进程组的集合,囊括了登录用户的所有活动。
    在登录shell时,用户可能会使用管道,让多个进程互相配合完成一项工作,这一组进程属于同一个进程组。

    当用户通过SSH客户端工具(putty、xshell等)连入Linux时,与上述登录的情景是类似的。

    必要了解
    当父进程,创建子进程的时候,默认子进程与父进程属于同一进程组。进程组ID==第一个进程ID(组长进程)。
    所以,组长进程标识:其进程组ID==其进程ID
    
    • 1
    • 2
    可以使用kill -SIGKILL-进程组ID(负的)来将整个进程组内的进程全部杀死。
    
    • 1
    组长进程可以创建一个进程组,创建该进程组中的进程,然后终止。只要进程组中有一个进程存在,进程组就存在,与组长进程是否终止无关。
    
    进程组生存期:进程组创建到最后一个进程离开(终止或转移到另一个进程组)。
    
    • 1
    • 2
    • 3
    **一个进程可以为自己或子进程设置进程组 ID**
    
    • 1

    组长进程可以创建一个进程组,创建该进程组中的进程,然后终止。只要进程组中有一个进程存在,进程组就
    存在,与组长进程是否终止无关。
    进程组生存期:进程组创建到最后一个进程离开(终止或转移到另一个进程组)。
    一个进程可以为自己或子进程设置进程组 ID

    可以使用kill -SIGKILL-进程组ID(负的)来将整个进程组内的进程全部杀死。
    【kill_multprocess.c】
    组长进程可以创建一个进程组,创建该进程组中的进程,然后终止。只要进程组中有一个进程存在,进程组就
    存在,与组长进程是否终止无关。
    进程组生存期:进程组创建到最后一个进程离开(终止或转移到另一个进程组)。
    一个进程可以为自己或子进程设置进程组 ID (就是后面的 setpgid(0,0) 和 set(pid,pid))

    进程组操作函数
    getpgrp函数

    获取当前进程的进程组ID
    pid_t getpgrp(void); 总是返回调用者的进程组ID

    getpgid函数

    获取脂定进程的进程组 ID
    pid_t getpgid(pid_t pid); 成功:0;失败:-1,设置errno
    如果pid =0,那么该函数作用和getpgrp 一样。
    练习:查看进程对应的进程组 ID

    setpgid函数

    改变进程默认所属的进程组。通常可用来加入一个现有的进程组或创建一个新进程组。
    int setpgid(pid_t pid, pid_t pgid); 成功:0;失败:-1,设置errno
    将参1对应的进程,加入参2 对应的进程组中。
    注意:
    1.如改变子进程为新的组,应fork 后,exec 前。
    2.权级问题。非root进程只能改变自己创建的子进程,或有权限操作的进程

    这个函数的含义是,找到进程ID为pid的进程,将其进程组ID修改为pgid,如果pid的值为0,则表示要修改调用进程的进程组ID。该接口一般用来创建一个新的进程组。 
    
    
    • 1
    • 2

    下面三个接口含义一致,都是创立新的进程组,并且指定的进程会成为进程组的首进程。

    setpgid(0,0)
    setpgid(getpid(),0)
    setpgid(getpid(),getpid()) 
    
    • 1
    • 2
    • 3
    setpgid函数有很多限制(import):

    ·pid参数必须指定为调用setpgid函数的进程或其子进程,不能随意修改不相关进程的进程组ID,如果违反这条规则,则返回-1,并置errno为ESRCH。
    ·pid参数可以指定调用进程的子进程,但是子进程如果已经执行了exec函数,则不能修改子进程的进程组ID。如果违反这条规则,则返回-1,并置errno为EACCESS。
    ·在进程组间移动,调用进程,pid指定的进程及目标进程组必须在同一个会话之内。这个比较好理解,不加入公司(会话),就无法加入公司下属的部门(进程组),否则就是部门要造反的节奏。如果违反这条规则,则返回-1,并置errno为EPERM。
    ·pid指定的进程,不能是会话首进程。如果违反这条规则,则返回-1,并置errno为EPERM。

    练习:修改子进程的进程组 ID
    #include
    int main()
    {
        pid_t pid;
        if ((pid = fork()) < 0) {
            perror("fork");
            exit(1);
        } else if (pid == 0) {
            printf("child PID == %d\n",getpid());
            printf("child Group ID == %d\n",getpgid(0));// 返回组id
            //printf("child Group ID == %d\n",getpgrp()://返回组id
            sleep;
            printf("----Group ID of child is changed to %d\n",getpgid(0));
            exit(0);
        } else if (pid > 0) {
            sleep(1); 
            setpgid(pid,pid);
            //让子进程自立门户,成为进程组组长,以它的pid为进程组id
            sleep(13);
            printf("\n") ;
            printf("parent PID == %d\n", getpid());
            printf("parent's parent process PID ==%d\n", getppid());
            printf("parent Group ID == %d\n", getpgid(0));
    
            sleep(5);
            setpgid(getpid(),getppid()); //改变组id为父进程的父进程
            printf("\n-----Group ID of parent is changed to %d\n",getpgid(0));
    printf("===============================k");
    setpgid(0,0);
    
            printf("\n-----Group ID of parent is changed to %d\n",getpgid(0));
        }
        return 0;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    引入了进程组的概念,可以更方便地管理这一组进程了。比如这项工作放弃了,不必向每个进程一一发送信号,可以直接将信号发送给进程组,进程组内的所有进程都会收到该信号。 
    前文曾提到过,子进程一旦执行exec,父进程就无法调用setpgid函数来设置子进程的进程组ID了,这条规则会影响shell的作业控制。出于保险的考虑,一般父进程在调用fork创建子进程后,会调用setpgid函数设置子进程的进程组ID,同时子进程也要调用setpgid函数来设置自身的进程组ID。这两次调用有一次是多余的,但是这样做能够保证无论是父进程先执行,还是子进程先执行,子进程一定已经进入了指定的进程组中。由于fork之后,父子进程的执行顺序是不确定的,因此如果不这样做,就会造成在一定的时间窗口内,无法确定子进程是否进入了相应的进程组。
    
    • 1
    • 2
    用户在shell中可以同时执行多个命令。对于耗时很久的命令(如编译大型工程),用户不必傻傻等待命令运行完毕才执行下一个命令。用户在执行命令时,可以在命令的结尾添加“&”符号,表示将命令放入后台执行。这样该命令对应的进程组即为后台进程组。在任意时刻,可能同时存在多个后台进程组,但是不管什么时候都只能有一个前台进程组。只有在前台进程组中进程才能在控制终端读取输入。当用户在终端输入信号生成终端字符(如ctrl+c、ctrl+z、ctr+\等)时,对应的信号只会发送给前台进程组。 
    
    shell中可以存在多个进程组,无论是前台进程组还是后台进程组,它们或多或少存在一定的联系,为了更好地控制这些进程组(或者称为作业),系统引入了会话的概念。会话的意义在于将很多的工作囊括在一个终端,选取其中一个作为前台来直接接收终端的输入及信号,其他的工作则放在后台执行。
    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5

    如果这个函数的调用进程不是进程组组长,那么调用该函数会发生以下事情:
    1)创建一个新会话,会话ID等于进程ID,调用进程成为会话的首进程。
    2)创建一个进程组,进程组ID等于进程ID,调用进程成为进程组的组长。
    3)该进程没有控制终端,如果调用setsid前,该进程有控制终端,这种联系就会断掉。
    调用setsid函数的进程不能是进程组的组长,否则调用会失败,返回-1,并置errno为EPERM。

    这个限制是比较合理的。如果允许进程组组长迁移到新的会话,而进程组的其他成员仍然在老的会话中,那么,就会出现同一个进程组的进程分属不同的会话之中的情况,这就破坏了进程组和会话的严格的层次关系了。

    Get-ItemProperty HKLM:\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall* | Select-Object DisplayName, DisplayVersion, Publisher, InstallDate | Format-Table -AutoSize > D:\InstalledApplications-PS.txt
    
    • 1

    intf(“child Group ID == %d\n”,getpgrp()://返回组id
    sleep;
    printf(“----Group ID of child is changed to %d\n”,getpgid(0));
    exit(0);
    } else if (pid > 0) {
    sleep(1);
    setpgid(pid,pid);
    //让子进程自立门户,成为进程组组长,以它的pid为进程组id
    sleep(13);
    printf(“\n”) ;
    printf(“parent PID == %d\n”, getpid());
    printf(“parent’s parent process PID ==%d\n”, getppid());
    printf(“parent Group ID == %d\n”, getpgid(0));

        sleep(5);
        setpgid(getpid(),getppid()); //改变组id为父进程的父进程
        printf("\n-----Group ID of parent is changed to %d\n",getpgid(0));
    
    • 1
    • 2
    • 3

    printf(“===============================k”);
    setpgid(0,0);

        printf("\n-----Group ID of parent is changed to %d\n",getpgid(0));
    }
    return 0;
    
    • 1
    • 2
    • 3

    }

    
    
    • 1

    引入了进程组的概念,可以更方便地管理这一组进程了。比如这项工作放弃了,不必向每个进程一一发送信号,可以直接将信号发送给进程组,进程组内的所有进程都会收到该信号。
    前文曾提到过,子进程一旦执行exec,父进程就无法调用setpgid函数来设置子进程的进程组ID了,这条规则会影响shell的作业控制。出于保险的考虑,一般父进程在调用fork创建子进程后,会调用setpgid函数设置子进程的进程组ID,同时子进程也要调用setpgid函数来设置自身的进程组ID。这两次调用有一次是多余的,但是这样做能够保证无论是父进程先执行,还是子进程先执行,子进程一定已经进入了指定的进程组中。由于fork之后,父子进程的执行顺序是不确定的,因此如果不这样做,就会造成在一定的时间窗口内,无法确定子进程是否进入了相应的进程组。

    
    
    
    
    • 1
    • 2
    • 3

    用户在shell中可以同时执行多个命令。对于耗时很久的命令(如编译大型工程),用户不必傻傻等待命令运行完毕才执行下一个命令。用户在执行命令时,可以在命令的结尾添加“&”符号,表示将命令放入后台执行。这样该命令对应的进程组即为后台进程组。在任意时刻,可能同时存在多个后台进程组,但是不管什么时候都只能有一个前台进程组。只有在前台进程组中进程才能在控制终端读取输入。当用户在终端输入信号生成终端字符(如ctrl+c、ctrl+z、ctr+\等)时,对应的信号只会发送给前台进程组。

    shell中可以存在多个进程组,无论是前台进程组还是后台进程组,它们或多或少存在一定的联系,为了更好地控制这些进程组(或者称为作业),系统引入了会话的概念。会话的意义在于将很多的工作囊括在一个终端,选取其中一个作为前台来直接接收终端的输入及信号,其他的工作则放在后台执行。

    
    
    
    
    
    
    
    如果这个函数的调用进程不是进程组组长,那么调用该函数会发生以下事情: 
    1)创建一个新会话,会话ID等于进程ID,调用进程成为会话的首进程。 
    2)创建一个进程组,进程组ID等于进程ID,调用进程成为进程组的组长。 
    3)该进程没有控制终端,如果调用setsid前,该进程有控制终端,这种联系就会断掉。 
    调用setsid函数的进程不能是进程组的组长,否则调用会失败,返回-1,并置errno为EPERM。 
    
    这个限制是比较合理的。如果允许进程组组长迁移到新的会话,而进程组的其他成员仍然在老的会话中,那么,就会出现同一个进程组的进程分属不同的会话之中的情况,这就破坏了进程组和会话的严格的层次关系了。
    
    
    
    
    
    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    Get-ItemProperty HKLM:\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall* | Select-Object DisplayName, DisplayVersion, Publisher, InstallDate | Format-Table -AutoSize > D:\InstalledApplications-PS.txt

    
    
    
    
    
    • 1
    • 2
    • 3
    • 4
  • 相关阅读:
    【AI绘画】Stable Diffusion 3开源
    javaweb 之 HTML快速入门 常用标签 转义字符
    ssm基于Springboot的大学生竞赛辅导网站系统java-maven项目
    teb局部路径规划参数调节--差速小车
    JavaWeb-JSP
    Autosar MCAL-ADC详解(一)-基于Tc27x的cfg软件
    react获取Datepicker组件日期
    代码随想录——单词接龙(图论)
    Java设计模式教程
    文件系统(八):Linux JFFS2文件系统工作原理、优势与局限
  • 原文地址:https://blog.csdn.net/qq_55125921/article/details/127762319