Linux信号(有代码,有示例图,有知识点整理,有信号的妙用)

Linux系列文章目录

第一章 Linux信号


        服务程序运行在后台,如果想让中止它,强行杀掉不是个好办法,因为程序被杀的时候,程序突然死亡,没有释放资源,会影响系统的稳定,用”Ctrl+c“中止与杀程序是相同的效果。

如果能向后台程序发送一个信号,后台程序收到这个信号后,调用一个函数,在函数中编写释放资源的代码,程序就可以有计划的退出,安全而体面。


一、如何让服务程序运行在后台

1. 加“&”符号

        如果想要程序在后台运行,执行的时候,命令后面加“&”符号。

代码:

#include <stdio.h>
#include <unistd.h>

void exectask() {
    printf("执行了一次任务!\n");
}

int main() {
    while (1) {
        exectask();
        sleep(5);
    }
    return 0;
}
king@ubuntu:~/share/Student/Linux/Signal$ ./signal1 & 

        可见程序signal1在shell的一个子程序下运行,如果该终端退出,后台程序将由系统托管。

关闭后台程序:

king@ubuntu:~$ ps -ef | grep signal1

2. fork()

采用fork,让主程序执行fork,生成一个子进程,然后父进程退出,留下子进程继续运行,子进程将由系统托管。

代码:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

void exectask() {
    printf("执行了一次任务!\n");
}

int main() {
    if (fork() > 0) exit(0);    // 父进程退出子进程继续运行
    while (1) {
        exectask();
        sleep(5);
    }
    return 0;
}
king@ubuntu:~/share/Student/Linux/Signal$ gcc -o signal1 signal1.c 
king@ubuntu:~/share/Student/Linux/Signal$ ./signal1

 可见当父进程退出后,子进程将由系统托管。

二、信号的妙用

        signal信号是进程之间相互传递消息的一种方法,信号全称为软中断信号,也有人称为软中断。

        程序在运行的过程中,用Ctrl+c、kill、killall中止其本质是向程序发送信号,程序对这两个信号的缺省行为是程序中止运行。在程序中,可以捕获信号,编写信号处理函数,即收到信号后执行的代码。

1. 从代码认识信号

代码:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>

void exectask() {
    printf("执行了一次任务!\n");
}

void func(int sig) {
    printf("收到了信号:%d\n", sig);
}

int main() {
    signal(SIGINT, func);       // 键盘中断“Ctrl + c”   SIGINT = 2
    signal(SIGTERM, func);      // 采用“kill 进程编号”或“killall 程序名”通知程序    SIGTERM = 15
    //if (fork() > 0) exit(0);    // 父进程退出子进程继续运行
    while (1) {
        exectask();
        sleep(5);
    }
    return 0;
}

 

 2. 信号基本概念

        软中断信号(signal,又简称为信号)用来通知进程发生了事件。进程之间可以通过调用kill库函数发送软中断信号。Linux内核也可能给进程发送信号,通知进程发生了某个事件(例如内存越界)。

        注意,信号只是用来通知某进程发生了什么事件,无法给进程传递任何数据,进程对信号的处理方法有三种:

  1. 忽略某个信号,对该信号不做任何处理,就像未发生过一样。

  2. 设置中断的处理函数,收到信号后,由该函数来处理。

  3. 对该信号的处理采用系统默认操作,大部分信号的默认操作是终止进程。

3. 信号的类型

信号名信号值默认处理动作发出信号的原因
SIGHUP1A终端挂起或者控制进程终止
SIGINT2A键盘中断“Ctrl + c”
SIGQUIT3C键盘的退出键被按下
SIGILL4C非法指令
SIGABRT6C由abort(3)发出的退出指令
SIGFPE8C浮点异常
SIGKILL9AEF采用“kill -9 进程编号”强制杀死程序
SIGSEGV11C无效的内存引用
SIGPIPE13A管道破裂: 写一个没有读端口的管道
SIGALRM14A由alarm(2)发出的信号
SIGTERM15A采用“kill 进程编号”或“killall 程序名”通知程序
SIGUSR130,10,16A用户自定义信号1
SIGUSR231,12,17A用户自定义信号2
SIGCHLD20,17,18B子进程结束信号
SIGCONT19,18,25进程继续(曾被停止的进程)
SIGSTOP17,19,23DEF终止进程
SIGTSTP18,20,24D控制终端(tty)上按下停止键
SIGTTIN21,21,26D后台进程企图从控制终端读
SIGTTOU22,22,27D后台进程企图从控制终端

处理动作一项中的字母含义如下:

        A 缺省的动作是终止进程

        B 缺省的动作是忽略此信号

        C 缺省的动作是终止进程并进行内核映像转储(dump core)

        D 缺省的动作是停止进程

        E 信号不能被捕获

        F 信号不能被忽略

4. signal库函数

sighandler_t signal(int signum, sighandler_t handler);

第一个参数signum:处理的信号类型,它可以取除了SIGKILL和SIGSTOP外的任何一种信号。  

第二个参数handler:信号处理的动作,有以下三种值:

  1. SIG_IGN:忽略参数signum所指的信号

  2. SIG_DFL:恢复参数signal所指信号的处理方法为默认值

  3. 一个自定义的处理信号的函数,信号的编号为这个自定义函数的参数

一般不关心signal的返回值

5. 信号有什么用

        服务程序运行在后台,如果想让中止它,强行杀掉不是个好办法,因为程序被杀的时候,程序突然死亡,没有释放资源,会影响系统的稳定,用”Ctrl+c“中止与杀程序是相同的效果。

        如果能向后台程序发送一个信号,后台程序收到这个信号后,调用一个函数,在函数中编写释放资源的代码,程序就可以有计划的退出,安全而体面。

        信号还可以用于网络服务程序抓包等......

6. 信号应用示例

代码:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>

void EXIT(int sig) {
    printf("收到信号%d,程序退出。\n", sig);

    // 在这里添加释放资源的代码

    exit(0);    // 程序退出
}

int main() {

    for (int i = 0; i < 100; ++ i)  signal(i, SIG_IGN);     // 屏蔽全部的信号
    
    signal(SIGINT, EXIT);       // 键盘中断“Ctrl + c”   SIGINT = 2
    signal(SIGTERM, EXIT);      // 采用“kill 进程编号”或“killall 程序名”通知程序    SIGTERM = 15
    
    while (1) {
        sleep(1);
    }
    return 0;
}

运行结果:

 7. 发送信号

        Linux操作系统提供了kill命令向程序发送信号,C语言也提供了kill库函数,用于在程序中向其他进程或者线程发送信号。

int kill(pid_t pid, int sig);

kill()函数将参数sig指定的信号发送给参数pid指定的进程。

参数pid有以下三种情况:

  1. pid > 0 信号发送给进程号为pid的进程

  2. pid = 0 将进程发送给和目前进程相同进程组的所有进程,常用于父进程给子进程发送信号。(注意,发送者进程也会受到自己发出的信号)

  3. pid = -1 将信号广播发送给系统内所有进程(例如系统关机时,会向所有的登录窗口广播关机信息)

参数sig:准备发送信号的类型,假如其值为零则没有任何信号发送,但是系统会执行错误检查通常会利用sig值为 零来检验某个进程是否仍在运行。

返回值说明:成功执行,返回0;失败,返回-1;errno被设为以下某值。

EINVAL:指定的信号无效(参数sig不合法)

EPERM:权限不够无法发送信号给指定进程

ESRCH:参数pid所指定的进程或进程组不存在

三、可靠信号与不可靠信号

信号分为不可靠信号(1~32)和可靠信号(34~64)

不可靠信号主要有以下问题:

  1. 每次处理完信号之后,就会恢复成默认处理(早期的signal函数,linux2.6.3 5.6内核经验证已经不在恢复默认动作)

  2. 存在信号丢失的问题(进程收到信号不作排队处理,相同的信号多次到来会合并为一个)

现在的Linux对信号机制进行了改进,因此,不可靠信号主要是指信号丢失

信号丢失示例:

代码:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>

// 子进程退出时调用的函数
void hdfunc(int sig) {
    printf("sig=%d\n", sig);
    for (int i = 1; i <= 5; ++ i) {
        printf("i(%d)=%d\n", sig, i);
        sleep(1);
    }
}

int main() {
    signal(15, hdfunc);
    signal(34, hdfunc);

    for (int i = 1; i < 100; ++ i) {
        printf("i=%d\n", i);
        sleep(2);
    }
    return 0;
}

运行结果(不可靠信号):

运行结果(可靠信号):

四、信号处理函数被中断

        当一个信号到达后,调用处理函数,如果这时候有其他的信号发生,会中断之前的处理函数,等新的信号处理函数执行完成后再继续执行之前的处理函数。

        但是,同一个信号会排队阻塞。

代码:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>

// 子进程退出时调用的函数
void hdfunc(int sig) {
    printf("sig=%d\n", sig);
    for (int i = 1; i <= 5; ++ i) {
        printf("i(%d)=%d\n", sig, i);
        sleep(1);
    }
}

int main() {
    signal( 2, hdfunc);
    signal(15, hdfunc);
    
    signal(34, hdfunc);
    signal(35, hdfunc);

    for (int i = 1; i < 100; ++ i) {
        printf("i=%d\n", i);
        sleep(2);
    }
    return 0;
}

示例1(不可靠信号)

示例2(可靠信号)

五、信号的阻塞

        如果不希望在接到信号时中断当前的处理函数,也不希望忽略该信号,而是延时一段时间再处理这个信号,这种情况可以通过阻塞信号实现。

        信号的阻塞和忽略信号是不同的,被阻塞的信号也不会影响进程的行为,信号只是暂时被阻止传递。

        进程忽略一个信号时,信号会被传递出去但进程会将信号丢弃。

        执行信号的处理动作称为信号递达(Delivery),信号从产生到递达之间的状态,称为信号未决(Pending)。

代码:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>

// 子进程退出时调用的函数
void hdfunc1(int sig) {
    sigset_t set;       // 定义一个信号集
    sigemptyset(&set);      // 清空信号集
    sigaddset(&set, 15);    // 将15加入信号集
    sigprocmask(SIG_BLOCK, &set, NULL);     // 阻塞信号集(set)里的所有信号

    printf("sig=%d\n", sig);
    for (int i = 1; i <= 5; ++ i) {
        printf("i(%d)=%d\n", sig, i);
        sleep(1);
    }

    sigprocmask(SIG_UNBLOCK, &set, NULL);   // 停止阻塞信号集(set)里的所有信号
}

// 子进程退出时调用的函数
void hdfunc2(int sig) {
    sigset_t set;       // 定义一个信号集
    sigemptyset(&set);      // 清空信号集
    sigaddset(&set, 2);    // 将2加入信号集
    sigprocmask(SIG_BLOCK, &set, NULL);     // 阻塞信号集(set)里的所有信号

    printf("sig=%d\n", sig);
    for (int i = 1; i <= 5; ++ i) {
        printf("i(%d)=%d\n", sig, i);
        sleep(1);
    }

    sigprocmask(SIG_UNBLOCK, &set, NULL);   // 停止阻塞信号集(set)里的所有信号
}

int main() {
    signal( 2, hdfunc1);
    signal(15, hdfunc2);
    
    for (int i = 1; i < 100; ++ i) {
        printf("i=%d\n", i);
        sleep(2);
    }
    return 0;
}

运行结果:

六、功能更强大的sigaction

代码(阻塞信号):

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <string.h>

// 子进程退出时调用的函数
void hdfunc(int sig) {
    printf("sig=%d\n", sig);
    for (int i = 1; i <= 5; ++ i) {
        printf("i(%d)=%d\n", sig, i);
        sleep(1);
    }
}

int main() {
    struct sigaction stact;
    memset(&stact, 0, sizeof(stact));   // 初始化
    stact.sa_handler = hdfunc;          // 指定信号处理函数
    sigaddset(&stact.sa_mask, 15);      // 阻塞信号15
    sigaddset(&stact.sa_mask,  2);      // 阻塞信号2
    sigaction( 2, &stact, NULL);        // 设置信号2的处理行为
    sigaction(15, &stact, NULL);        // 设置信号15的处理行为
    for (int i = 1; i < 100; ++ i) {
        printf("i=%d\n", i);
        sleep(2);
    }  
    return 0;
}

        运行结果(阻塞信号):

代码(重启系统调用):

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <string.h>

// 子进程退出时调用的函数
void hdfunc(int sig) {
    printf("sig=%d\n", sig);
    for (int i = 1; i <= 5; ++ i) {
        printf("i(%d)=%d\n", sig, i);
        sleep(1);
    }
}

int main() {
    struct sigaction stact;
    memset(&stact, 0, sizeof(stact));   // 初始化
    stact.sa_handler = hdfunc;          // 指定信号处理函数
    
    sigaddset(&stact.sa_mask, 15);      // 阻塞信号15
    sigaddset(&stact.sa_mask,  2);      // 阻塞信号2
    
    //stact.sa_flags = SA_RESTART;        // 如果信号中断了进程的某个系统调用,则系统自动启动该系统调用
    sigaction( 2, &stact, NULL);        // 设置信号2的处理行为
    sigaction(15, &stact, NULL);        // 设置信号15的处理行为

    char str[50]; memset(str, 0, sizeof(str));
    scanf("%s", str);
    printf("str=%s\n", str);

    return 0;
}

运行结果(重启系统调用)有注释的:

运行结果(重启系统调用)无注释的:


总结

        在指令后加“&”符号或者用fork()可以让程序在后台运行,我们可以发送信号可以让处于后台运行的程序产生相应的关联(执行函数、退出程序、给其他进程发送信号...)。后来的信号可以将信号处理函数中断,可以阻塞信号防止被中断。相同的信号短时间多次发送可能会丢失。使用更能更强大的sigaction可以让我们更有效的使用信号。


    推荐一个零声学院免费教程,个人觉得老师讲得不错,分享给大家:Linux,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK等技术内容

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值