系统编程 学习笔记 03

信号

信号在我们的生活中随处可见,他们都有共性:

  • 简单。
  • 不能携带大量信息。
  • 满足某个特设条件才发送。

信号的机制

A 给 B 发送信号,B 收到信号之前执行自己的代码,**收到信号后,不管执行到程度的什么位置,都要暂停运行,去处理信号,处理完毕再继续执行。**与硬件中断类似 – 异步模式。但信号似软件层面上实现的中断,早期常被称为“软中断”。

**信号的特质:**由于信号是通过软件方式实现,其实现手段导致信号有很强的延时性。但对于用户来说,这个延时时间非常短,不易察觉。

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

与信号相关的事件和状态

产生信号:

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

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

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

信号的处理方式:

  • 执行默认动作
  • 忽略(丢弃)
  • 捕捉(调用户处理函数)

Linux 内核的进程控制块 PCB 是一个结构体,task_struct,除了包含进程 id,状态,工作目录,用户id,组id,文件描述符表,还包含了信号相关的信息,主要指阻塞信号集和未决信号集。

**阻塞信号集(信号屏蔽字):**将某些信号加入集合,对他们设置屏蔽,当屏蔽 x 信号后,再收到该信号,该信号的处理将推后(接触屏蔽后)

未决信号集:

  • 信号产生,未决信号集中描述该信号的位立即翻转位 1,表信号处于未决状态。当信号被处理对应位翻转回为 0。这一时刻往往非常短暂。
  • 信号产生后由于某些原因(主要是阻塞)不能抵达。这类信号的集合称之为未决信号集。在屏蔽解除前,信号一直处于未决状态。

信号的编号

可以使用 kill -l 命令查看当前系统可使用的信号有那些。

不存在编号为 0 的信号。其中 1-31 号信号称之为常规信号(也叫普通信号或标准信号),34-64 称之为实时信号,驱动编程与硬件相关。名字上区别不大。而前 32 个名字各不相同。

信号 4 要素

  1. 编号 2) 名称 3) 事件 4) 默认处理动作

可通过 man 7 signal 查看帮助文档获取。

默认动作:

  • Term: 终止进程

  • Ign: 忽略信号(默认即使对这种信号忽略操作)

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

  • Stop: 停止(暂停)进程

  • Cont: 继续运行进程

9)sigkill 和 19)sigstop 信号,不允许忽略和捕捉,只能执行默认动作。甚至不能将其设置为阻塞

Linux 常规信号一览表

    1. SIGHUP(hang up): 当用户退出 shell 时,由该 shell 启动的所有进程将收到这个信号,默认动作为终止进程。
    1. SIGINT(Interrupt): 当用户按下<Ctrl+C>组合键时,用户终端向正在运行中的由该终端启动的程序发出此信号。默认动作终止进程。
    1. SIGQUIT(quit): 当用户按下<ctrl + \ >组合键时产生该信号,用户终端向正在运行中的由该终端启动的程序发出此信号。默认动作终止进程。
    1. SIGILL(illegal): CPU 检测到某进程执行了非法指令。默认动作为终止进程并产生 core 文件。
    1. SIGTRAP: 该信号由断点指令或其他 trap 指令产生。默认动作为终止里程并产生 core 文件。
    1. SIGABRT(abort): 调用 abort 函数时产生该信号。默认动作为终止进程并产生 core 文件。
    1. SIGBUS: 非法访问内存地址,包括内存对齐出错,默认动作为终止进程并产生 core 文件。
    1. SIGFPE: 在发生致命的运算错误时发出。不仅包括浮点运行错误,还包括溢出及除数为 0 等所有的算法错误。默认动作为终止进程并产生 core 文件。
    1. SIGKILL: 无条件终止进程。本信号不能被忽略,处理和阻塞。默认动作为终止进程。它向系统管理员提供了可以杀死任何进程的方法。
    1. SIGUSE1: 用户定义的信号。即程序员可以在程序中定义并使用该信号。默认动作为终止进程。
    1. SIGSEGV: 指明进程进行了无效内存访问。默认动作为终止进程并产生 core 文件。
    1. SIGUSR2: 另外一个用户自定义信号,程序员可以在程序中定义并使用该信号。默认动作为终止进程。
    1. SIGPIPE: broken pipe 向一个没有读端的管道写数据。默认动作为终止进程。
    1. SIGALRM: 定时器超时,超时的时间由系统调用 alarm 设置。默认动作为终止进程。
    1. SIGTERM: 程序结束信号,与 SIGKILL 不同的是,该信号可以被阻塞和终止。通常用来要示程序正常退出。
    1. SIGSTKFLT: Linux 早期版本出现的信号,现仍保留向后兼容。默认动作为终止进程。
    1. SIGCHLD: 子进程结束时,父进程会收到这个信号。默认动作为忽略这个信号。
    1. SIGCONT: 如果进程已停止,则使其继续运行。默认动作为继续/忽略。
    1. SIGSTOP: 停止进程的执行。信号不能被忽略,处理和阻塞。默认动作为暂停进程。
    1. SIGTSTP: 停止终端交互进程的运行。按下<ctrl+z>组合键时发出这个信号。默认动作为暂停进程。
    1. SIGTTIN: 后台进程读终端控制台。默认动作为暂停进程。
    1. SIGTTOU: 该信号类似于 SIGTTIN, 在后台进程要向终端输出数据时发生。默认动作为暂停进程。
    1. SIGURG: 套接字上有紧急数据时,向当前正在运行的进程发出些信号,报告有紧急数据到达。如网络带外数据到达,默认动作为忽略该信号。
    1. SIGXCPU: 进程执行时间超过了分配给该进程的 CPU 时间,系统产生该信号并发送给该进程。默认动作为终止进程。
    1. SIGXFSZ: 超过文件的最大长度设置。默认动作为终止进程。
    1. SIGVTALRM: 虚拟时钟超时时产生该信号。类似于 SIGALRM,但是该信号只计算该进程占用 CPU 的使用时间。默认动作为终止进程。
    1. SGIPROF: 类似于 SIGVTALRM,它不但包括该进程占用 CPU 时间还包括执行系统调用时间。默认动作为终止进程。
    1. SIGWINCH: 窗口变化大小时发出。默认动作为忽略该信号。
    1. SIGIO: 此信号向进程指示发出了一个异步 IO 时间。默认动作为忽略
    1. SIGPWR: 关机。默认动作为终止进程。
    1. SIGSYS: 无效的系统调用。默认动作为终止进程并产生 core 文件。
    1. SIGRTMIN~(64) SIGRTMAX: LINUX 的实时信号,它没有固定的含义(可以由用户自定义)。所有的实时信号的默认动作都为终止进程。

信号产生:

终端按键产生信号:

  • Ctrl+c -> 2) SIGINT(终止/中断) “INT” – interrupt

  • Ctrl+z -> 20) SIGTSTP(暂停/停止) “T” – Terminal

    • fg – 恢复暂停的进程
  • Ctrl+\ -> 3) SIGQUIT(退出)

硬件异常产生信号

  • 除 0 操作 -> 8)SIGFPE (浮点数例外) “FPE” – Floating point exception

  • 非法访问内存 -> 11)SIGSEGV (段错误)

  • 总线错误 -> 7)SIGBUS

kill函数/命令产生信号

kill 命令产生信号:kill -SIGKILL pid

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

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

  • sig: 不推荐直接使用数字,应使用宏名。

  • pid>0: 发送信号给指定的进程。

  • pid==0: 发送信号给与调用 kill 函数进程属于同一进程组的所有进程。

  • pid==-1: 发送给进程有权限发送的系统中所有进程。

  • pid<-1: 取|pid|发给对应的进程组。

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

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

raise 和 abort 函数:

**raise 函数:**给当前进程发送指定信号(自己给自己发)raise(signo) == kill(getpid(), signo);

  • int raise(int sig);
    • 成功 0。
    • 失败非 0 值。

**abort 函数:**给自己发送异常终止信号 6)SIGABRT 信号,终止并产生 core 文件

  • **void abort(void); **该函数无返回

软件条件产生信号:

alarm 函数:

设定定时器(闹钟)。在指定 seconds 后,内核会给当前进程发送 14) SIGALRM 信号。进程收到该信号,默认动作终止。

每个进程都有且只有唯一个定时器。

  • unsigned int alarm(unsigned int seconds);
    • 返回 0 或 上一次剩余的秒数,无失败。

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

练习:编写程序,测试你使用的计算机 1 秒钟能数多少个数。

#include <cstdio>
#include <iostream>
#include <unistd.h>
using namespace std;
int main()
{
    alarm(1);
    for(int i = 1; ; i++) {
        if(i >= 400000000)
        cout << i << endl;
    }
}

使用 time 命令查看进程执行的时间。程序运行的瓶颈在于 IO.

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

setitimer 函数

设置定时器(闹钟)。可代替 alarm 函数。精度微妙 us,可以实现周期定时。

  • **int setitimer(int which, const struct itimerval *new_value, struct itimerval *old_value); **
    • 成功:0;
    • 失败:-1,设置 errno.

参数:which:指定定时方式

  • 自然定时:ITIMER_REAL -> 14) SIGLARM 计算自然时间

  • 虚拟空间计时(用户空间):ITIMER_VIRTUAL -> 26) SIGVTALRM 只计算进程占用 cpu 的时间

  • 运行时计时(用户+内核):ITIMER_PROF -> 27) SIGPROF 计算占用 cpu 及执行系统调用的时间。

练习:结果 man page 编写程序,测试 it_interval、it_value 这两个参数的作用。

#include <cstdio>
#include <cstdlib>
#include <sys/time.h>
#include <signal.h>
#include <iostream>
using namespace std;
void myf(int signo) { 
    cout << "hello world" << endl;
}
int main(void)
{
    signal(SIGALRM, myf);
    struct itimerval it, oldit;
    it.it_value.tv_sec = 5;
    it.it_value.tv_usec = 0;

    it.it_interval.tv_sec = 3;//周期间隔时间.
    it.it_interval.tv_usec = 0;

    if(setitimer(ITIMER_REAL, &it, &oldit) == -1) {
        perror("alarm error");
        exit(1);
    }
    while(1);
    return 0;
}
  • it_interval: 用来设定两次定时任务之间间隔的时间。

  • it_value: 定时的时长

  • 两个参数都设置为 0,即清 0 操作。

信号集操作函数

内核通过读取未决信号集来判断信号是否应被处理。信号屏蔽字 mask 可以影响未决信号集。而我们可以在应用程序中自定义 set 来改变 mask。已达到屏蔽指定信号的目的。

信号集设定

  • sigset_t set; //typedef unsigned long sigset_t; 位图

  • 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;

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

sigprocmask 函数

用来屏蔽信号、解除屏蔽也使用该函数。其本质,读取或修改进程的信号屏蔽字(PCB中)

严格注意,屏蔽信号:只是将信号处理延后执行(延至解除屏蔽);而忽略表示将信号丢弃。

  • int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);

    • 成功: 0; 失败: -1, 设置 errno
  • set: 传入参数,是一个位图,set 中哪位置 1,就表示当前进程屏蔽哪个信号。

  • oldset: 传入参数,保存旧的信号屏蔽集。

  • how 参数取值:假设当前信号屏蔽字为 mask

    • SIG_BLOCK: 当 how 设置为此值,set 表示需要屏蔽的信号。相当于 mask = mask | set
    • SIG_UNBLOCK: 当 how 设置为此,set 表示需要解除屏蔽的信号。相当于 mask = mask & ~set
    • SIG_SETMASK: 当 how 设置为此,set 表示用于替代原始屏蔽及的新屏蔽集。相当于 mask = set 若,调用 sigprocmask 解除了对当前若干个信号的阻塞,则在 sigprocmask 返回前,至少将其中一个信号递送。

sigpending 函数

读取当前进程的未决信号集

  • int sigpending(sigset_t *set);
    • 成功:0;失败 -1,设置 error.
  • set 传出参数。

练习:编写程序。把所有常规信号的未决状态打印至屏幕。

#include <cstdio>
#include <signal.h>
#include <iostream>
#include <cstdlib>
#include <unistd.h>
using namespace std;
void show_ped(sigset_t *ped) {
    for(int i = 1; i < 31; i++) {
        if(sigismember(ped, i)) {
            cout << 1;
        }
        else cout << 0;
    }
    cout << endl;
}
int main(void)
{
    sigset_t set, old_set, ped;
    sigemptyset(&set);
    sigaddset(&set, SIGINT);
    sigaddset(&set, SIGQUIT);
    sigaddset(&set, SIGTSTP);

    int ret = sigprocmask(SIG_BLOCK, &set, &old_set);
    if(ret == -1) {
       perror("procmask error");
       exit(1);
    }
    while(1) {
        sigpending(&ped);
        show_ped(&ped);
        sleep(1);
    }
    return 0;
}

signal 函数

注册一个信号捕捉函数:

  • typedef void (*sighandler_t)(int);

  • sighandler_t signal(int signum, sighandler_t handler);

该函数由 ANSI 定义,不同版本的 UNIX 可能由不同的行为。因此避免使用它,取而代之使用 sigaction 函数。

void( signal(int signum), void(sighandler_t)(int)))(int);

能看出这个函数代表什么意思吗?注意多在复杂结构中使用 typedef.

sigaction 函数

修改信号处理动作(通常在Linux用其来注册一个信号的捕捉函数)

  • int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);

    • 成功:0; 失败:-1, 设置 errno.
  • **act: **传入参数,新的处理方式。

  • **oldact: **传出参数,旧的处理方式。

  • stuct 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);
      };
      
    • siginfo_t {
          int      si_signo;    /* Signal number /
          int      si_errno;    / An errno value /
          int      si_code;     / Signal code /
          int      si_trapno;   / Trap number that caused
          hardware-generated signal
          (unused on most architectures) /
          pid_t    si_pid;      / Sending process ID /
          uid_t    si_uid;      / Real user ID of sending process /
          int      si_status;   / Exit value or signal /
          clock_t  si_utime;    / User time consumed /
          clock_t  si_stime;    / System time consumed /
          sigval_t si_value;    / Signal value /
          int      si_int;      / POSIX.1b signal */
          void    si_ptr;      / POSIX.1b signal /
          int      si_overrun;  / Timer overrun count; POSIX.1b timers /
          int      si_timerid;  / Timer ID; POSIX.1b timers */
          void    si_addr;     / Memory location which caused fault /
          long     si_band;     / Band event (was int in
          glibc 2.3.2 and earlier) /
          int      si_fd;       / File descriptor /
          short    si_addr_lsb; / Least significant bit of address
          (since Linux 2.6.32) */
      };
      
    • **sa_restorer: **该元素是过时的,不应该使用,POSIX.1标准将不指定该元素。(弃用)

    • **sa_sigaction: **当 sa_flags 被指定为 SA_SIGINFO 标志时,使用该信号处理程序。(很少使用)

    • 重点掌握:

      • **sa_handler: **指定信号捕捉后的处理函数名(既注册函数)。也可赋值为 SIG_IGN 表忽略或 SIG_DFL 表执行默认动作
      • **sa_mask: **调用信号处理函数时,所要屏蔽的信号集合(信号屏蔽字)。注意:仅在处理函数被调用期间屏蔽生效,是临时性设置。
      • **sa_flags: **
        • 通常设置为 0,表示用默认属性。信号捕捉函数执行期间,自动屏蔽本信号
        • SA_SIGINFO: 选用 sa_sigaction 来指定捕捉函数
        • SA_INTERRURT: 被信号中断后系统调用不重启
        • SA_RESTART: 被信号中断后系统调用自动重启
        • SA_DEFER: 不自动屏蔽本信号

信号捕捉特性:

  • 进程正常运行时,默认 PCB 中有一个信号屏蔽字,假定为 X,它决定了进程自动屏蔽哪些信号。当注册了某个信号捕捉函数,捕捉到该信号以后,要调用该函数。而该函数有可能执行很长时间,在这期间所屏蔽的信号不由 X 来指定。而是用 sa_mask 来指定。调用完信号处理函数,再恢复 X.
  • XXX 信号捕捉函数执行期间,XXX 信号自动屏蔽
  • 阻塞的常规信号不支持排队,产生多次只记录一次。(后 32 个实时信号支持排队)

练习 1:为某个信号设置捕捉函数

#include <cstdio>
#include <signal.h>
#include <cstdlib>
#include <unistd.h>
#include <iostream>
using namespace std;
void cat(int signo) {
    sleep(4);
    cout << "--------cat" << endl;
}
int main(void) {
    struct sigaction now;
    now.sa_handler = cat;
    sigemptyset(&now.sa_mask);
    sigaddset(&now.sa_mask, SIGINT);
    sigaddset(&now.sa_mask, SIGTSTP);
    sigaddset(&now.sa_mask, SIGQUIT);
    now.sa_flags = 0;
    int ret = sigaction(SIGINT, &now, NULL);
    if(ret == -1) {
        perror("car error"); 
        exit(1);
    }
    while(1);
    return 0;
}

内核实现信号捕捉过程

用户 -> 内核 -> (用户)执行捕捉函数 -> 内核 -> 用户

pause 函数

调用该函数可以造成进程主动挂起,等待信号唤醒。调用该系统调用的进程处于阻塞状态(主动放弃 CPU)直到由信号递达将其唤醒。

  • int pause(void);
    • 返回值:-1 并设置 errno 为 EINTR.

返回值:

  • (1) 如果信号的默认处理动作是终止进程,则进程终止,pause 函数么有返回值机会

  • (2) 如果信号的默认处理动作是忽略,进程继续处于挂起状态,pause函数不返回

  • (3) 如果信号的处理动作是捕捉,则【调用完信号处理函数之后,pause 返回-1】errno 设置为 EINTR,表示“被信号中断”。想想我们还有哪个函数只有出错返回值。

  • (4) pause 收到的信号不能被屏蔽,如果被屏蔽,那么 pause 就不能唤醒。

注意,unslept = alarm(0) 的用法

时序竞态

时序问题分析:

回顾,借助 pause 和 alarm 实现的 mysleep 函数。设想如下时序:

  • 注册 SIGALRM 信号处理函数 (sigaction…)

  • 调用 alarm(1) 函数设定闹钟 1 秒。

  • 函数调用刚结束,开始倒计时 1 秒。当前进程失去 cpu,内核调度优先级高的进程(有多个)取代当前进程。

  • 1秒后,闹钟超时,内核向当前进程发送 SIGALRM 信号(自然定时法,与进程状态无关),高优先级进程尚未执行完,当前进程仍处于就绪态,信号无法处理(未决)

  • 优先级高的进程执行完,当前进程获得 cpu 资源,内核调度回当前进程执行。

  • SIGALRM 信号递达,信号设置捕捉,执行处理函数 sig_alarm。

  • 信号处理函数执行结束,返回当前进程主控进程,pause()被调用挂起等待。(预等待 alarm 函数发送的 SIGALRM 信号将自己唤醒)

  • SIGALRM 信号已经处理完毕,pause 不会等到。

解决时序问题

可以通过设置屏蔽 SIGALRM 的方法来控制程序执行逻辑,**但无论如何设置,程序都有可能在"解除信号屏蔽"与挂起等待信号"这个两个操作间隙失去 cpu 资源。除非将这两步骤合并成一个"原子操作"。sigsuspend 函数具备这个功能。**在对时序要求严格的场合下都应该使用 sigsuspend 替换 pause。

  • int sigsuspend(const sigset_t *mask) 挂起等待信号。

  • sigsuspend 函数调用期间,进程信号屏蔽字有其参数 mask 指定。

**可将某个信号 (如 SIGALRM) 从临时信号屏蔽字 mask 中删除,这样在调用 sigsuspend 时将解除对该信号的屏蔽,然后挂起等待,**当 sigsuspend 返回时,进程的信号屏蔽字恢复为原来的值。如果原来对该信号是屏蔽态,sigsuspend 函数返回后仍然屏蔽该信号。

练习:使用 sigsuspend 和 alarm 来实现 sleep 函数

#include <cstdio>
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <cstdlib>
#include <errno.h>
using namespace std;
void cat(int signo)
{
    cout << "5s arrived" << endl;
}
unsigned int mysleep(unsigned int seconds)
{
    struct sigaction act, old_act;
    act.sa_handler = cat;
    sigemptyset(&act.sa_mask);
    act.sa_flags = 0; 
    int ret = sigaction(SIGALRM, &act, &old_act);
    if(ret == -1) {
        perror("sigaction error");
        exit(1);
    }
	//屏蔽 SIGALRM 信号
    sigset_t set, old_set, sus_set;
    sigemptyset(&set);
    sigaddset(&set, SIGALRM);
    sigprocmask(SIG_BLOCK, &set, &old_set);
	//计时
    alarm(seconds);
    //解决了:如果这时失去 cpu, 直至 seconds 后回来,优先执行信号,以至于在 pause 后没法收到信号.
    sus_set = old_set;
    sigdelset(&sus_set, SIGALRM);
    //调用解除屏蔽 SIGALRM 信号的 sigsuspend
    ret = sigsuspend(&sus_set);// 原子操作
    if(ret == -1 && errno == EINTR) {
        printf("pause sucess\n");
    }

    sigprocmask(SIG_SETMASK, &old_set, NULL);
    sigaction(SIGALRM, &old_act, NULL);
    ret = alarm(0);
    return ret;
}
int main(void) {
    cout << "start sleep" << endl;
    mysleep(5);
    //sleep();
    cout << "end sleep" << endl;
    return 0;
}

总结

竞态条件,跟系统负载有很紧密的关系,体现出信号的不可靠性。系统负载越严重,信号不可靠性越强。

不可靠由其实现原理所致。信号是通过软件方式实现(跟内核调度高度依赖,延时性强),每次系统调用结束后或中断处理处理结束后,需通过扫描PCB中的未决信号集,来判断是否应处理某个信号。当系统负载过重时,会出现时序混乱。

这种意外情况只能在编写程序过程重,提早预见,主动规避,而无法通过 gdb 程序调试等其他手段弥补。且由于该错误不具规律性,后期捕捉和重现十分困难。

全局变量异步 I/O

分析如下父子进程交替数数程序。

#include <cstdio>
#include <signal.h>
#include <cstdlib>
#include <unistd.h>
#include <sys/types.h>
using namespace std;
int n, flag;
pid_t pid;
void son(int signo) {
    printf("I is son, pid = %d, n = %d\n", getpid(), n);
    n += 2;
    flag = 1;
}
void parent(int signo) {
    printf("I is parent, pid = %d, n = %d\n", getpid(), n);
    n += 2;
    flag = 1;
}
int main(void) {
    pid = fork();
    if(pid == -1) {
        perror("fork error");
        exit(1);
    } else if(pid == 0) {
        n = 2;
        struct sigaction act, old_act;
        act.sa_flags = 0;
        act.sa_handler = son;
        sigemptyset(&act.sa_mask);
        sigaction(SIGUSR1, &act, &old_act);
        while(1) {
            if(flag == 1) {
                flag = 0;//flag = 0,不能在发送信号下面
                kill(getppid(), SIGUSR2);
                //失去了 cpu, 收到信号, 执行信号, flag == 1, 接下来 flag == 0, 导致无法在发信号给父亲
                //flag = 0;
            }
        } 
    } else {
        sleep(1);
        n = 1;
        struct sigaction act, old_act;
        act.sa_flags = 0;
        act.sa_handler = parent;
        sigemptyset(&act.sa_mask);
        sigaction(SIGUSR2, &act, &old_act);
        parent(0);
        while(1) {
            if(flag == 1) { //代表刚刚数完数,需要给对方发信号
                flag = 0;
                kill(pid, SIGUSR1);
                //flag = 0;
            }
        } 
    }
    return 0;
}

如何解决该问题呢?也可以用“锁”机制。当操作全局变量的时候,通过加锁,解锁来解决该问题。

现阶段,我们在编程期间如若使用全局变量,应在主观上注意全局变量的异步 IO 可能造成的问题

可/不可重入函数

可重入函数:递归调用不会导致最终结果变化

不可重入函数:递归调用会导致最终结果变化

注意事项:

  • 函数内不能含有全局变量及 static 变量,不能使用malloc、free

  • 信号捕捉函数应设计为可重入函数

  • 信号处理程序可以调用的可重入函数可参阅 man 7 signal

  • 没有包含在上述列表中的函数大多是不可重入的,其原因为:

    • 使用静态数据结构

    • 调用了 malloc 或 free

    • 是标准 I/O 函数

SIGCHLD信号

SIGCHLD的产生条件

  • 子进程终止时

  • 子进程接收到 SIGSTOP 信号停止时

  • 子进程处于停止态,接收到 SIGCONT 后唤醒时

  • 总结:子进程状态发送变化,都会向父亲发送 SIGCHID

借助 SIGCHLD 信号回收子进程

练习:借助 sigchld 信号回收子进程

#include <cstdio>
#include <cstdlib>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>
using namespace std;
void re_son(int signo) {
    pid_t pid;
    int status;
    while((pid = waitpid(0, &status, WNOHANG)) > 0) {//这里不能用 if,多个子进程同时发送 sigchld 信号,相当于也就收到了一个。所以不能说收到一个信号,回收一个子进程。
        if(WIFEXITED(status)) {
            printf("------child %d exit %d\n", pid, WEXITSTATUS(status));
        }
        else if(WIFSIGNALED(status)) 
            printf("------child %d cancel signal %d\n", pid, WTERMSIG(status));
    }
}
int main(void) {
    int n = 10, i;
    for(i = 1; i <= n; i++) {
        pid_t pid = fork();
        if(pid == -1) {
            perror("fork error");
            exit(1);
        } else if(pid == 0) {
            break;
        } 
    }
    if(i <= n) {
        //sleep(i);
        return i;
    }
    else {
        struct sigaction act;
        act.sa_handler = re_son;
        act.sa_flags = 0;
        sigemptyset(&act.sa_mask);
        int ret = sigaction(SIGCHLD, &act, NULL); 
        if(ret == -1) {
            perror("sigaction error");
            exit(1);
        }
        
        while(1);
    }
    return 0;
}

信号传参

发送信号传参

sigqueue 函数对应 kill 函数,但可在向指定进程发送信号的同时携带参数

  • int sigqueue(pid_t pid, int sig, const union sigval value);

    • 成功: 0; 失败 -1, 设置 errno
  • union sigval{
    int sival_int;
    void *sival_ptr;
    }

向指定进程发送指定信号的同时,携带数据。但,如传地址,需注意,不同进程之间虚拟地址空间各自独立,将当前进程地址传递给另一个进程没有实际意义。

捕捉函数传参

int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);

中断系统调用

系统调用可分为两类:慢速系统调用和其他系统调用

  • **慢速系统调用:**可能会使进程永远阻塞的一类。如果在阻塞期间收到一个信号,该系统调用就被中断,不再继续执行(早期);也可以设定系统调用是否重启。如, read, write, pause, wait…

  • **其他系统调用:**getpid、getppid、fork…

慢速系统调用被中断的相关行为,实际上就是 pause 的行为:如,read

  • 想中断 pause,信号不能被屏蔽

  • 信号的处理方式必须是捕捉(默认、忽略都不可以)

  • 中断后返回 -1,设置 errno 为 EINTR(表“被信号中断”)

可修改 sa_flags 参数来设置被信号中断后系统调用是否重启。SA_INTERRURT 不重启。SA_RESTART 重启。

扩展了解:

  • sa_flags 还有很多可选参数,适用于不同情况。如:捕捉到信号后,在执行捕捉函数期间,不希望自动阻塞该信号,可将 sa_flags 设置为 SA_NODEFER,除非 sa_mask 中包含该信号。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值