• linux进阶-ipc信号(软中断信号)


    目录

    信号

    系统支持的信号

    非实时信号和实时信号

    信号的处理

    捕获信号API函数

    signal()函数(不推荐使用)

    头文件和函数原型

    signal.c文件

    Makefile文件

    执行过程

    sigaction()函数(推荐使用)

    头文件和函数原型

    sigaction.c文件

    Makefile文件

    执行过程

    发送信号API函数

    kill()函数

    头文件和函数原型

    raise()函数

    头文件和函数原型

    raiseANDkill.c文件

    Makefile文件

    执行过程

    alarm()函数

    头文件和函数原型

    alarm.c文件(实验一)

    Makefile文件(实验一)

    执行过程(实验一)

    alarm.c文件(实验二)

    Makefile文件(实验二)

    执行过程(实验二)


    信号

    信号(软中断信号),用于通知进程发生了异步事件(它是Linux系统响应某些条件而产生的一个事件,它是在软件层次上对中断机制的一种模拟,是一种异步通信的方式,在原理上,一个进程收到一个信号与处理器收到一个中断请求可以说是一样的)。

    信号是进程间通信机制的唯一异步通信机制一个进程不必通过任何操作来等待信号的到达。当进程接收到一个信号时,也会相应地采取一些行动

    生成:一个信号的产生。

    捕获:进程接收到一个信号。

    在linux系统中,信号可能是由于系统中某些错误而产生如内存段冲突、浮点处理器错误或非法指令等,由shell和终端处理器生成并且引起中断),也可以是某个进程主动生成的一个信号可以作为在进程间传递通知或修改行为的一种方式,它可以明确地由一个进程发送给另一个进程,当进程捕获到这个信号就会按照程序进行相应操作并且去处理它)。

    无论何种情况,他们的编程接口都是相同的,信号可以被生成、捕获、响应或忽略。

    进程间可以互相发送信号,内核也可以因为内部事件而给进程发送信号,通知进程发生了某个事件。

    系统支持的信号

    kill -l可查看系统支持的信号类型。

    1~31信号值的信号属性为非实时信号(不可靠信号),34~64信号值的信号属性为实时信号(可靠信号),总共62个信号类型。具体信号类型意思可自行百度。

    一般而言,信号的响应处理过程为:

    如果该信号被阻塞,那么将该信号挂起,不对其做任何处理,等到解除对其阻塞为止。

    如果该信号被捕获,那么将进一步判断捕获的类型(如果设置为响应函数,那么执行该响应函数;如果设置为忽略,那么直接丢弃该信号。)最后才执行信号的默认处理。

    非实时信号和实时信号

    非实时信号,主要是因为这类信号不支持排队,因此信号可能会丢失。比如发送多次相同的信号,进程只能收到一次,也只会处理一次,因此剩下的信号将被丢弃。

    实时信号,主要是因为这类信号支持排队,发送了多少个信号给进程,进程就会处理多少次。

    一般来说,一个进程收到一个信号后不会被立即处理,而是在恰当时机进行处理(一般是在中断返回时,或常见是内核态返回用户态时)。

    即使收到信号,进程也不一定会立即去处理它,因为系统不会为了处理一个信号而把当前正在运行的进程挂起,因为这样子系统的资源消耗过大。如果不是紧急信号,是不会立即处理的,所以系统一般都会选择在内核态切换回用户态时处理信号,比如有时候进程处于休眠状态,但是又收到一个信号,于是系统就得把信号存储在进程唯一的进程PCB中。

    信号的处理

    生成信号的事件:程序错误、外部事件、显式请求

            程序错误:零作除数、非法存储访问等,这种情况通常是由硬件而不是由Linux内核检测到的,但由内核向发生此错误的那个进程发送相应的信号。

            外部事件:当用户在终端按下某些键时产生终端生成的信号,当进程超越了CPU或文件大小的限制时,内核会生成一个信号通知进程。

            显式请求:使用kill()函数允许进程发送任何信号给其他进程或进程组。

    信号的生成既可以是同步的,也可以是异步的。

            同步信号大多数是程序执行过程中出现了某个错误而产生的,由进程显式请求生成的给自己的信号也是同步的。

            异步信号是接收过程可控制之外的事件所生成的信号,这类信号一般是进程无法控制的,只能被动接收,因为进程也不知道这个信号会何时发生,只能在发生时去处理它。一般外部事件总是异步地生成信号,异步信号可在进程运行中的任意时刻产生,进程无法预期信号到达的时刻,它所能做的只是告诉Linux内核假如有信号生成时应当采取什么行动(这相当于注册信号对应的处理)

    无论是同步还是异步信号,当信号发生时,我们可以告诉linux内核采取以下3钟动作中的任意一种

            忽略信号。大部分信号都可以被忽略,但有两个除外:SIGSTOP和SIGKILL,因为是为了给超级用户提供杀掉或停止任何进程的一种手段。此外,尽管其他信号都可以被忽略,但其中有一些却不宜忽略。例如,若忽略硬件例外(非法指令)信号,则会导致进程的行为不确定。

            捕获信号。这种处理是想要告诉Linux内核,当信号出现时调用专门提供的一个函数(信号处理函数,专门对产生信号的事件作出处理)。

            让信号默认动作起作用。系统为每种信号规定了一个默认动作,这种动作由Linux内核来完成,有以下几种可能的默认动作:

                    终止进程并且生成内存转储文件,即写出进程的地址空间和寄存器上下文到进程当前目录下名为core的文件中。

                    终止进程但不生成core文件。

                    忽略信号

                    暂停进程

                    若进程为暂停状态,恢复进程,否则将忽略信号

    捕获信号API函数

    当执行一个不断while循环的函数时,通过ctrl+c键,可以通过系统默认的处理方式去终止进程。很多时候使用信号只是向通知进程而不是终止进程,或者在终止前想进行某些收尾工作,因此需要去捕获这个信号,然后去处理它。

    signal()函数(不推荐使用)

    主要是捕获信号,可以改变进程中对信号的默认动作

    使用signal()函数时,需要提前设置一个回调函数,即进程接收到信号后将要跳转执行的响应函数,或者设置忽略某个信号,才能改变信号的默认动作,这个过程称为“信号的捕获”。

    可以重复进行对一个信号的捕获,不过signal()函数将会返回前一次设置的信号响应函数指针。

    头文件和函数原型
    1. #include
    2. typedef void (*sighandler_t)(int);
    3. sighandler_t signal(int signum, sighandler_t handler);
    4. /*
    5. signum:指定捕获的信号。如果指定的是一个无效的信号,或者尝试处理的信号是不可捕获或不可忽略的信号(如SIGKILL),errno将被设置为EINVAL
    6. handler:函数指针,传递接收到的信号值。需要用户自定义处理信号的方式,也可以用宏
    7. SIG_IGN:忽略该信号
    8. SIG_DFL:采用系统默认方式处理信号
    9. */

    准备捕获或忽略的信号由参数signum指出,接收到指定的信号后将要调用的函数由参数handler指出。

    signal()函数会返回一个sighandler_t类型的函数指针,这是因为调用signal()函数修改了信号的动作,需要返回之前的信号处理动作。

    signal()函数如果调用处理程序导致信号被阻塞,则从处理程序返回后,信号将被解除阻塞。无法捕获或忽略信号SIGKILL和SIGSTOP。

    signal.c文件
    1. #include
    2. #include
    3. #include
    4. #include
    5. #include
    6. #include
    7. void signal_handler(int sig)
    8. {
    9. printf("the signal number:%d\n", sig);
    10. if(sig == SIGINT){
    11. printf("get SIGINT!!!\n");
    12. // 回调中恢复该信号默认动作,回到进程继续往下执行
    13. signal(SIGINT, SIG_DFL);
    14. }
    15. }
    16. int main(void)
    17. {
    18. // 改变了CTRL+C的处理方式,不再是终止进程,而是进入回调做处理
    19. signal(SIGINT, signal_handler);
    20. while(1)
    21. {
    22. printf("waiting for the SIGINT signal, please enter : ctrl + c\n");
    23. sleep(1);
    24. }
    25. exit(0);
    26. }

    Makefile文件

    照旧

    执行过程

    sigaction()函数(推荐使用)

    头文件和函数原型
    1. #include
    2. int sigaction(int signum, const struct sigaction *act, struct sigaction *lodact);
    3. /*
    4. signum:指定捕获的信号。如果指定的是一个无效的信号,或者尝试处理的信号是不可捕获或不可忽略的信号(如SIGKILL),errno将被设置为EINVAL
    5. act:结构体。
    6. oldact:返回原有的信号处理参数,一般设置为NULL即可。
    7. */

    struct sigaction{
            void (*sa_handler)(int);
            void (*sa_sigaction)(int, siginfo_t *, void *);
            sigset_t sa_mask;
            int sa_flags;
            void (*sa_restorer)(void);
        };

    sa_handler:函数指针,捕获信号后的处理函数,传入的是信号的值(int),这个函数就是标准的信号处理函数。

    sa_sigaction:函数指针,扩展捕获信号后的处理函数,比sa_handler函数指针复杂。不用同时使用sa_handler和sa_sigaction,因为这两个处理函数有共同的联合体。

    sa_mask:信号掩码,指定了在执行信号处理函数期间阻塞的信号的掩码,被设置在该掩码中的信号,在进程响应信号期间被临时阻塞。除非使用SA_NODEFER标志,否则即使是当前正在处理的响应的信号再次到来时被阻塞。

    sa_flags:指定一系列用于修改信号处理过程动作的标志.

            SA_NOCLDSTOP:如果signum是SIGCHLD,则在子进程停止或恢复时,不会传入信号给调用sigaction()函数的进程。即当系统接收到SIGSTOP、SIGTSTO、SIGTTIN或SIGTTOU中的一种时或接收到SIGCONT(恢复)时,父进程不会收到通知。仅当为SIGCHLD建立信号处理程序时,此标志才有意义。

            SA_NOCLDWAIT:表示父进程在它的子进程终止时不会收到SIGCHLD信号,这时子进程终止则不会称为僵尸进程。

            SA_NODEFER:不用阻止从其自身的信号处理程序中接收信号,使进程对信号的屏蔽无效,即在信号处理函数执行期间仍能接收到这个信号,仅当建立信号处理程序时,此标志才有意义。

            SA_RESETHAND:信号处理后重新设置为默认的处理方式。

            SA_SIGINFO:指示使用sa_sigaction成员而不是使用sa_handler成员作为信号处理函数。

    当sa_flags指定SA_SIGINFO标志时,信号处理程序地址将通过sg_sigaction字段传递。该处理程序采用三个参数:

    void handler(int sig, siginfo_t *info, void *ucontext){}

    info指向siginfo_t的指针,它是一个包含有关信号的更多信息的结构。

    1. siginfo_t {
    2. int si_signo; /* 信号数值 */
    3. int si_errno; /* 错误值 */
    4. int si_code; /* 信号代码 */
    5. int si_trapno; /* 导致硬件生成信号的陷阱号,在大多数体系结构中未使用 */
    6. pid_t si_pid; /* 发送信号的进程 ID */
    7. uid_t si_uid; /* 发送信号的真实用户 ID */
    8. int si_status; /* 退出值或信号状态 */
    9. clock_t si_utime; /* 消耗的用户时间 */
    10. clock_t si_stime; /* 消耗的系统时间 */
    11. sigval_t si_value; /* 信号值 */
    12. int si_int; /* POSIX.1b 信号 */
    13. void *si_ptr;
    14. int si_overrun; /* 计时器溢出计数 */
    15. int si_timerid; /* 计时器 ID */
    16. void *si_addr; /* 导致故障的内存位置 */
    17. long si_band;
    18. int si_fd; /* 文件描述符 */
    19. short si_addr_lsb; /* 地址的最低有效位 (从 Linux 2.6.,→32 开始存在) */
    20. void *si_lower; /* 地址冲突时的下限 */
    21. void *si_upper; /* 地址冲突时的上限 (从 Linux 3.19开始存在) */
    22. int si_pkey; /* 导致的 PTE 上的保护密钥 */
    23. void *si_call_addr; /* 系统调用指令的地址 */
    24. int si_syscall; /* 尝试的系统调用次数 */
    25. unsigned int si_arch; /* 尝试的系统调用的体系结构 */
    26. }

    成员变量绝大部分几乎用不到,因此如果只是对信号的简单处理,可以直接使用sa_handler处理,无需配置siginfo_t这些信息。

    sigaction.c文件
    1. #include
    2. #include
    3. #include
    4. #include
    5. #include
    6. #include
    7. void signal_handler(int sig)
    8. {
    9. printf("the signal number:%d\n", sig);
    10. if(sig == SIGINT){
    11. printf("get SIGINT!!!\n");
    12. // 信号自动恢复为默认处理函数
    13. }
    14. }
    15. int main(void)
    16. {
    17. struct sigaction act;
    18. act.sa_handler = signal_handler;
    19. // 清空进程屏蔽的信号集,即在信号处理时不会屏蔽任何信号
    20. sigemptyset(&act.sa_mask);
    21. // 在处理完信号后恢复默认信号处理
    22. act.sa_flags = SA_RESETHAND;
    23. sigaction(SIGINT, &act, NULL);
    24. while(1)
    25. {
    26. printf("waiting for the SIGINT signal, please enter : ctrl + c\n");
    27. sleep(1);
    28. }
    29. exit(0);
    30. }

    Makefile文件

    照旧

    执行过程

    发送信号API函数

    kill()函数

    不仅可以终止进程(SIGKILL信号),也可以向进程发送其他信号。

    进程可以通过调用kill()函数向包括它本身在内的其他进程发送一个信号。如果程序没有发送该信号的权限,对kill函数的调用将失败,失败的常用原因是目标进程由另一个用户所拥有。因此想要发送一个信号,发送进程必须拥有相应的权限,这通常意味着两个进程必须拥有相同的用户ID(即你只能发送信号给属于自己的进程,但超级用户可以发送信号给任何进程)。

    kill()函数会在失败时返回-1并设置errno变量。失败的原因可能是:给定的信号无效(errno设置为INVAL)、发送进程权限不够(errno设置为EPERM)、目标进程不存在(errno设置为ESRCH)等情况。

    头文件和函数原型
    1. #include
    2. #include
    3. int kill(pid_t pid, int sig);
    4. /*
    5. pid:
    6. >1:将信号sig发送到进程ID值为pid指定的进程
    7. =0:信号被发送到所有和当前进程在同一进程组的进程
    8. =-1:将信号sig发送到系统中所有的进程,但进程1(init)除外
    9. <-1:将信号sig发送给进程组号为-pid(pid绝对值)的每一个进程
    10. sig:要发送的信号值
    11. 返回值:
    12. 0:发送成功
    13. -1:发送失败
    14. */

    raise()函数

    和kill()函数类似。区别在于,raise()函数只是进程向自身发送信号,而没有向其他进程发送信号。相当于kill(getpid(), sig)等同于raise(sig)。

    头文件和函数原型
    1. #include
    2. int raise(int sig);
    3. /*
    4. sig:发送的信号值
    5. 返回值:
    6. 0:发送成功
    7. -1:发送失败
    8. */

    发送失败的原因主要是信号无效,因为它只往自身发送信号,不存在权限问题,也不存在目标进程不存在的情况。

    raiseANDkill.c文件
    1. #include
    2. #include
    3. #include
    4. #include
    5. #include
    6. #include
    7. int main(void)
    8. {
    9. pid_t pid;
    10. int ret;
    11. if((pid = fork()) < 0){
    12. printf("fork error!\n");
    13. exit(1);
    14. }else if(pid == 0){
    15. printf("the child pid:%d\n waiting for any signal\n", getpid());
    16. // 子进程停在这里
    17. raise(SIGSTOP);
    18. exit(0);
    19. }else{
    20. // 等待一下,确保子进程先执行
    21. sleep(1);
    22. // 父进程暂时停止执行,等待子进程发出信号或结束
    23. // WNOHANG:若指定的子进程还没有结束,返回0,不等待(不阻塞);若结束,返回子进程PID
    24. // 代码意思是如果子进程还没有结束,则使用kill()函数杀死子进程
    25. if((waitpid(pid, NULL, WNOHANG)) == 0){
    26. if((ret == kill(pid, SIGKILL)) == 0){
    27. printf("this is the parent, kill %d", pid);
    28. }
    29. }
    30. }
    31. waitpid(pid, NULL, 0);
    32. exit(0);
    33. }

    Makefile文件

    照旧

    执行过程

    alarm()函数

    闹钟函数,可以在进程中设置一个定时器,当定时器指定的时间seconds到来时,它就像进程发送SIGALARM信号。

    头文件和函数原型
    1. #include
    2. unsigned int alarm(unsigned int seconds);

    如果在seconds秒内再次调用了alarm()函数设置了新的函数,则新的设置将覆盖前面的设置,即之前设置的秒数被新的闹钟时间取代。

    它的返回值是之前闹钟的剩余秒数,如果之前未设闹钟则返回0。

    如果新的seconds为0,则之前设置的闹钟会被取消,并将剩下的时间返回。

    alarm.c文件(实验一)
    1. #include
    2. #include
    3. #include
    4. #include
    5. #include
    6. int main(void)
    7. {
    8. // 设置5s的闹钟,让SIGALARM信号在5秒后传达给当前进程
    9. alarm(5);
    10. // 让进程休眠20秒
    11. sleep(20);
    12. // 当产生SIGALARM信号时,由于没有做捕获处理,系统会调用该信号的默认处理函数,即exit(0)并且自动打印Alarm clock或闹钟
    13. // 因此以下代码无法执行
    14. printf("end!!!\n");
    15. exit(0);
    16. }

    Makefile文件(实验一)

    照旧

    执行过程(实验一)

    alarm.c文件(实验二)
    1. #include
    2. #include
    3. #include
    4. #include
    5. #include
    6. int main(void)
    7. {
    8. unsigned int seconds;
    9. // 设置20s的闹钟,让SIGALARM信号在20秒后传达给当前进程,没有上一个闹钟,返回0
    10. seconds = alarm(20);
    11. printf("last alarm seconds remaining is %d!\n", seconds);
    12. // 让进程休眠5秒
    13. sleep(5);
    14. // 覆盖上一个的闹钟,重新设置为5s的闹钟,让SIGALARM信号在5秒后传达给当前进程,并返回上一个闹钟的剩余秒数
    15. seconds = alarm(5);
    16. printf("last alarm seconds remaining is %d!\n", seconds);
    17. // 让进程休眠20秒
    18. sleep(20);
    19. // 当产生SIGALARM信号时,由于没有做捕获处理,系统会调用该信号的默认处理函数,即exit(0)并且自动打印Alarm clock
    20. // 因此以下代码无法执行
    21. printf("end!!!\n");
    22. exit(0);
    23. }

    Makefile文件(实验二)

    照旧

    执行过程(实验二)

  • 相关阅读:
    node node-sass sass-loader版本兼容问题
    Spark启动流程
    day40-网络编程02
    C#/WPF/.NET 找到的程序集清单定义与程序集引用不匹配
    el7升级Apache模块编译
    基于matlab的异步(感应)电机直接转矩控制系统
    Ros 基本基本构架
    NC-UClient下载安装应用详解
    《文献阅读》- 遗传算法作为量子近似优化算法的经典优化器(未完成)
    Python中的编程经典案例【考题】判断日期是该年中的第几天
  • 原文地址:https://blog.csdn.net/weixin_47077788/article/details/133812783