Linux系统编程 Part7(信号)

相关概念

信号是软件层面上的“中断”。一旦信号产生,无论程序执行到什么位置,必须立即(用户感觉是立即,其实是要等待一个从用户区进入内核区的契机,站在CPU的时间量级上具有延迟性)停止运行,转而去处理信号,等信号处理结束,再继续执行后续指令。

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

与信号相关的事件和状态

产生信号:

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

递达:内核产生信号后,递送并且送达到进程,直接被内核处理

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

从宏观上看,可以把递达与处理划等号,信号被递达了内核就处理,但从cpu时间量级的角度看,每个信号都会经历未决状态,因为当信号产生后,未决信号集中描述该信号的位立刻翻转为1(此时该信号就处于未决状态),如果此信号在阻塞信号集中未被屏蔽,未决信号集中描述该信号的位会再翻转为0,内核就去处理该信号,这个过程及其短暂。

信号处理方式:

  • 执行默认处理动作(每一个信号都有自己的默认处理动作)
  • 忽略(丢弃)
  • 捕捉(自定义处理函数)

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

阻塞信号集(信号屏蔽字):本质是位图,用来记录信号的屏蔽状态。被屏蔽的信号(0→1)会一直处于未决态,直到解除屏蔽(1→0)再对该信号进行处理

未决信号集:本质是位图,用来记录信号的处理状态。信号产生后由于某些原因(主要是阻塞)不能抵达(即未被处理),这类信号的集合称为未决信号集。当信号产生,未决信号集中描述该信号的位立刻翻转为1(默认为0),表示信号处于未决状态,当信号被处理后再翻转为0,这一时刻非常短暂。

信号四要素

  • 编号(即value,不同操作系统可能不同)
    • 可通过命令kill -l查看各个信号的名称和对应编号
    • 1~31是常规信号,34~64是实时信号
  • 名称:宏名(可屏蔽平台差异)
  • 对应事件(只有对应事件发生后,该信号才会被内核发送,但不一定被递达,也就无法处理)
  • 默认处理动作
    • Term:终止进程
    • Ign:忽略信号(默认即时对该种信号忽略操作)
    • Core:终止进程,生成Core文件(查验进程死亡原因,用于gdb调试)
    • Stop:停止(暂停)进程
    • Cont:继续运行进程

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

常规信号一览表

可通过命令man 7 signal查看帮助文档获取各个信号的四要素

信号的产生

终端按键产生信号

  • Ctrl+c → 2)SIGINT(终止/中断) “INT”即interrupt
  • Ctrl+z → 20)SIGTSTP(暂停/停止) “T”即terminal
  • Ctrl+\ → 3)SIGQUIT(退出)

硬件异常产生信号

非法访问内存(段错误)、除0(浮点数例外)、内存对齐出错(总线错误)

函数调用产生信号

kill函数

给指定进程发送指定信号(不一定是杀死)

int kill(pid_t pid, int signum)

参数:

  • signum:待发送的信号(不推荐使用数字,应该使用宏名,因为不同操作系统信号编号可能不同,但名称一致)
  • pid:被发送信号的进程的PID
    • >0:发送信号给指定的进程
    • =0:发送信号给与调用kill函数进程属于同一进程组的所有进程
    • <0:取绝对值,发送信号给该绝对值所对应的进程组的所有进程
    • =-1:发送信号给系统中有权限发送的所有进程

返回值:

  • 0:成功
  • -1:失败

kill命令产生信号:kill -SIGKILL pid 比如:kill -9 10426

raise函数

int raise(int sig); 给当前进程发送指定信号

abort函数

void abort(void); 给当前进程发送SIGABRT信号

软件条件产生信号

alarm函数

unsigned int alarm(unsigned int seconds); 使用自然计时法,定时发送14)SIGALRM信号给当前进程

  • 参数seconds:定时秒数
  • 返回值:上次定时的剩余时间(无错误情况)

常用alarm(0)来取消定时器

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

time 命令 : 查看程序执行时间,比如:time ./my_alarm

实际时间 = 用户时间 + 内核时间 + 等待时间(最长) ——> 程序运行的瓶颈在于IO,因此优化程序,首先优化IO

setitimer函数

int setitimer(int which, const struct itimerval *new_value, struct itimerval *old_value);

设置定时器,可以实现周期定时,时间控制可以达到微秒级别

参数:

  • which
    • ITIMER_REAL: 采用自然计时 ——> 14)SIGALRM 即计算自然时间
    • ITIMER_VIRTUAL: 采用用户(虚拟)空间计时 ——> 26)SIGVTALRM 即计算进程占用cpu的时间
    • ITIMER_PROF: 采用内核+用户空间计时 ——> 27)SIGPROF 即计算进程占用cpu以及执行系统调用的时间
  • old_value:传出参数,上次定时剩余时间
  • new_value:定时秒数
struct itimerval {
    struct timeval {
    time_t      tv_sec;         /* seconds */
    suseconds_t tv_usec;        /* microseconds */

    }it_interval;---> 周期定时秒数

    struct timeval {
    time_t      tv_sec;         
    suseconds_t tv_usec;        

    }it_value;  ---> 第一次定时秒数  
};

返回值:

  • 成功:0
  • 失败:-1,errno

信号集操作函数

信号集设定

sigset_t set;  自定义信号集 

sigemptyset(sigset_t *set);  信号集中所有位全部置0

sigfillset(sigset_t *set);  信号集中所有位全部置1

sigaddset(sigset_t *set, int signum);  将一个信号添加到集合中,0→1

sigdelset(sigset_t *set, int signum);  将一个信号从集合中移除,1→0

sigismember(const sigset_t *set,int signum);  判断一个信号是否在集合中, 在→1, 不在→0

sigprocmask函数

设置信号屏蔽字或者解除屏蔽

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

参数:

  • how:
    • SIG_BLOCK:设置阻塞
    • SIG_UNBLOCK:取消阻塞
    • SIG_SETMASK:用自定义set替换mask
  • 传入参数set:自定义的set
  • 传出参数oldset:将老的mask保存

返回值:

  • 成功:0
  • 失败:-1,设置errno

sigpending函数

读取当前进程的未决信号集(未决信号集不能直接修改,只能通过修改阻塞信号集来影响它)

int sigpending(sigset_t *set);

传出参数set:传出的未决信号集

返回值:

  • 成功:0
  • 失败:-1,设置errno

编程练习:使用信号集操作函数把Ctrl+z设置为屏蔽

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<fcntl.h>
#include<unistd.h>
#include<pthread.h>
#include<sys/stat.h>
#include<sys/mman.h>
#include<dirent.h>
#include<errno.h>
#include<signal.h>

void sys_err(const char *str) {
    perror(str);
    exit(1);
}

void print_set(const sigset_t *set) {
    int i = 0;
    for(i = 1; i <= 32; i++) {
        if(sigismember(set, i)) putchar('1');
        else putchar('0');
    }
    printf("\n");
}

int main(int argc, char *argv[]) {
    sigset_t set, oldset, pedset;
    sigemptyset(&set);
    sigaddset(&set, SIGTSTP);

    int ret = 0;
    ret = sigprocmask(SIG_BLOCK, &set, &oldset);
    if(ret == -1) sys_err("sigprocmask error");

    while(1) {
        ret = sigpending(&pedset);
        if(ret == -1) sys_err("sigpending error");
        print_set(&pedset);
        sleep(1);
    }

    return 0;
}

ps:这时使用Ctrl+z已经无法终止该程序了,只能先ps aux找到进程PID,再用kill -9 pid来杀死

※信号捕捉

signal函数

注册一个信号捕捉函数

typedef void (*sighandler_t)(int); //函数指针

sighandler_t signal(int signum, sighandler_t handler);

ps:signal函数使用的不是通用标准,因此尽量避免使用,用更规范的sigaction函数

※sigaction函数

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

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

返回值:

  • 成功:0
  • 失败:-1,设置errno

参数:

  • signum:要修改的信号
  • 传入参数act:新处理动作
  • 传出参数oldact:旧处理动作(如果不需要旧处理动作,可以传NULL)

结构体:

struct sigaction {
    void (*sa_handler)(int); //捕捉函数名(同signal函数的参数),也可以用宏:SIG_IGN表忽略,SIG_DFL表执行默认动作
    void (*sa_sigaction)(int, siginfo_t *, void *); //指定带参数的信号捕捉函数(基本用不到)
    sigset_t sa_mask; //只作用于信号捕捉函数执行期间的信号屏蔽字
    int sa_flags; //一般设为0,表示本信号屏蔽
};

编程练习:用sigaction函数完成信号捕捉

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<fcntl.h>
#include<unistd.h>
#include<pthread.h>
#include<sys/stat.h>
#include<sys/mman.h>
#include<dirent.h>
#include<errno.h>
#include<signal.h>

void sys_err(const char *str) {
    perror(str);
    exit(1);
}

void sig_catch(int signo) { //捕捉回调函数
    printf("catch you!%d\n", signo);
    return ;
}

int main(int argc, char *argv[]) {
    struct sigaction act, oldact;
    act.sa_handler = sig_catch; //设置捕捉回调函数
    sigemptyset(&act.sa_mask); //设置屏蔽字清空,只在捕捉函数工作时有效
    act.sa_flags = 0; //设置默认属性

    int ret = sigaction(SIGTSTP, &act, &oldact); //注册信号捕捉函数
    if(ret == -1) {
        sys_err("sigaction error!");
    }

    while(1);

    return 0;
}

信号捕捉特性

  • 捕捉函数执行期间,信号屏蔽字由PCB中默认的mask转为sa_mask,捕捉函数执行结束后,再恢复回mask
  • 捕捉函数执行期间,被捕捉的这个信号自动被屏蔽(前提是设置sa_flags = 0)
  • 捕捉函数执行期间,被屏蔽的信号即使发送多次,解除屏蔽后只处理一次,即常规信号不支持排队

内核实现信号捕捉

SIGCHLD信号

SIGCHLD的产生条件

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

总结:子进程基本上稍微有点动静就要发SIGCHLD信号告诉他爹

借助SIGCHLD信号回收子进程

子进程结束运行,其父进程会收到SIGCHLD信号。该信号的默认处理动作是忽略,可以捕捉该信号,在捕捉函数中完成子进程状态的回收

使用SIGCHLD信号回收子进程比直接调用wait函数来回收子进程有几个明显的优势:

  • 非阻塞操作:在等待子进程结束时,如果直接调用 wait,父进程会被阻塞,直到有子进程终止。而使用 SIGCHLD 信号,父进程可以继续执行其他任务,而不必等待子进程结束。
  • 响应性:通过使用信号机制,父进程可以对子进程的状态变化作出更迅速的反应。例如,当子进程终止时,父进程可以立即处理这个事件,而不需要定期检查子进程的状态。
  • 避免忙等:如果使用轮询的方法(例如循环调用 wait),可能会导致 CPU 资源的浪费。使用 SIGCHLD 信号可以避免这种情况,因为它是基于事件驱动的。

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

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<fcntl.h>
#include<unistd.h>
#include<pthread.h>
#include<sys/stat.h>
#include<sys/mman.h>
#include<dirent.h>
#include<errno.h>
#include<signal.h>

void sys_err(const char *str) {
    perror(str);
    exit(1);
}

void catch_child(int signo) { //有子进程终止,发送SIGCHLD信号时,该函数会被内核回调
    pid_t wpid;
    int status;

    while((wpid = waitpid(-1, &status, 0)) != -1) { //循环回收,防止多个子进程同时死亡,导致僵尸进程
        if(WIFEXITED(status)) {
            printf("-------catch child, id = %d, ret = %d\n", wpid, WEXITSTATUS(status)); //回收成功
        }
    }
    return ;
}

int main(int argc, char *argv[]) {
    pid_t pid;

    //先把SIGCHLD信号设置阻塞,防止父进程捕捉函数还没注册成功,子进程就先死了
    sigset_t set;
    sigemptyset(&set);
    sigaddset(&set, SIGCHLD);

    sigprocmask(SIG_BLOCK, &set, NULL);

    int i = 0;
    for(i = 0; i < 5; i++) {
        if((pid = fork()) == 0) break;
    }

    if(i == 5) {
        struct sigaction act;
        act.sa_handler = catch_child;
        sigemptyset(&act.sa_mask);
        act.sa_flags = 0;

        sigaction (SIGCHLD, &act, NULL);

        //如果在捕捉函数注册成功前,有子进程死亡,但是SIGCHLD信号已经被屏蔽
        //因此父进程不予理会,等待函数注册完毕,解除阻塞后,如果有多个SIGCHLD信号发送给父进程
        //处理程序也只会会被调用一次,因为普通信号不支持排队,但是因为给wait()函数加上了while循环
        //这一次调用就可以处理在那个时间段所有已终止的子进程

        //捕捉函数注册好了,解除阻塞
        sigprocmask(SIG_UNBLOCK, &set, NULL);

        printf("i am parent, pid = %d\n", getpid());
        while(1); //让父进程先别结束
    } else {
        printf("i am child, pid = %d\n", getpid());
        return i;
    }

    return 0;
}

慢速系统调用被信号中断

系统调用可分为两类:

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

慢速系统调用阻塞期间,中断慢速系统调用的信号不能被屏蔽、忽略,该信号必须被捕捉,信号处理完再回到系统调用

被信号中断后返回-1,设置errno为EINTR,可以修改sa_flags参数来设置被信号中断后是否重启系统调用:

  • 不重启:SA_INTERRURT
  • 重启:SA_RESTART


不准投币喔 👆

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇