linux ---- 信号的机制

信号是由用户、系统或者进程发送给目标进程的信息,以通知目标进程某个状态的改变或系统异常。
信号是事件发生时对进程的通知机制,有时也称之为软件中断,它是在软件层次上对中断机制的一种模拟,是一种异步通信的方式。
信号可以导致一个正在运行的进程被另一个正在运行的异步进程中断,转而处理某一个突发事件。

信号的产生

  • 对于前台进程,用户可以通过输入特殊的终端字符来给它发送信号。比如输入Ctrl + C通常会给进程发送一个中断信号。
  • 系统异常。比如浮点异常和非法内存段访问。
  • 系统状态变化。比如 alarm 定时器到期引起的SIGALRM信号,进程执行的CPU时间超限,或者该进程的某个子进程退出。
  • 运行kill命令调用 kill 函数

服务器程序必须处理(或至少忽略)一些常见的信号,以免异常终止。

信号相关命令

查看系统定义的信号列表:kill -l(前31个信号为常规信号,其余为实时信号)
查看信号的详细信息:man 7 signal
列出所有的参数选项:ulimit -a

常用信号

  1. SIGHUP:本信号在用户终端结束时发出,通常是在终端的控制进程结束时,通知同一会话期内的各个作业,这时他们与控制终端不在关联。比如,登录Linux时,系统会自动分配给登录用户一个控制终端,在这个终端运行的所有程序,包括前台和后台进程组,一般都属于同一个会话。当用户退出时,所有进程组都将收到该信号,这个信号的默认操作是终止进程。此外对于与终端脱离关系的守护进程,这个信号用于通知它重新读取配置文件。
  2. SIGINT:程序终止信号。当用户按下CRTL+C时通知前台进程组终止进程。
  3. SIGQUIT:Ctrl+\控制,进程收到该信号退出时会产生core文件,类似于程序错误信号。
  4. SIGILL:执行了非法指令。通常是因为可执行文件本身出现错误,或者数据段、堆栈溢出时也有可能产生这个信号。
  5. SIGTRAP:由断点指令或其他陷进指令产生,由调试器使用。
  6. SIGABRT:调用abort函数产生,将会使程序非正常结束。
  7. SIGBUS:非法地址。包括内存地址对齐出错。比如访问一个4个字长的整数,但其地址不是4的倍数。它与SIGSEGV的区别在于后者是由于对合法地址的非法访问触发。
  8. SIGFPE:发生致命的算术运算错误。
  9. SIGKILL:用来立即结束程序的运行。不能被捕捉、阻塞或忽略,只能执行默认动作。
  10. SIGUSR1:留给用户使用,用户可自定义。
  11. SIGSEGV:访问未分配给用户的内存区。或操作没有权限的区域。
  12. SIGUSR2:留给用户使用,用户可自定义。
  13. SIGPIPE:管道破裂信号。当对一个读进程已经运行结束的管道执行写操作时产生。
  14. SIGALRM:时钟定时信号。由alarm函数设定的时间终止时产生。
  15. SIGTERM:程序结束信号。shell使用kill产生该信号,当结束不了该进程,尝试使用SIGKILL信号。
  16. SIGSTKFLT:堆栈错误。
  17. SIGCHLD:子进程结束,父进程会收到。如果子进程结束时父进程不等待或不处理该信号,子进程会变成僵尸进程。
  18. SIGCONT:让一个停止的进程继续执行。
  19. SIGSTOP:停止进程执行。不能被捕捉、阻塞或忽略,只能执行默认动作。
  20. SIGTSTP:停止终端交互运行,可以被忽略。按下Ctrl+z发出这个信号。
  21. SIGTTIN:当后台进程需要从终端接收数据时,所有进程会收到该信号,暂停执行。
  22. SIGTTOU:与SIGTTIN类似,在后台的进程向终端输出数据时产生。
  23. SIGURG:套接字上出现紧急情况时产生。向当前正在运行的进程发出些信号,报告有紧急数据到达,如网络带外数据到达。
  24. SIGXCPU:超过CPU时间资源限制时产生的信号。
  25. SIGXFSZ:当进程企图扩大文件以至于超过文件大小资源限制时产生。
  26. SIGVTALRM:虚拟使用信号。计算的是进程占用CPU调用的时间。
  27. SIGPROF:包括进程使用CPU的时间以及系统调用的时间。
  28. SIGWINCH:窗口大小改变时。
  29. SIGIO:文件描述符准备就绪,表示可以进行输入输出操作。
  30. SIGPWR:电源失效信号,即关机。
  31. SIGSYS:非法的系统调用。

信号处理

信号处理方式

信号的几种状态:产生、未决、抵达。

信号的5种默认处理动作
1. Term终止进程
2. Ign当前进程忽略此信号
3. Core终止进程,并生成一个Core文件
4. Stop暂停当前进程
5. Cont继续执行当前被暂停的进程

信号处理相关函数

int kill (pid_t pid, int sig);

  • 头文件:
    • #include<sys/types.h>
    • #include<signal.h>
  • 功能:把信号sig发送给目标进程pid。
  • 参数:
    • pid
      • > 0 信号发送给PID为pid的进程
      • = 0 信号发送给本进程组内的其他进程
      • = -1 信号发送给除了init进程外的所有进程, 但发送者需要拥有对目标进程发送信号的权限
      • < -1 信号发送给组ID为-pid的进程组中的所有成员
    • sig
      • 需要发送的信号编号或者宏值,0表示不发送任何信号。
    • 返回值:成功返回0,失败返回-1并设置errno:
      • EINVAL 无效的信号
      • EPERM 该进程没有权限发送任何信号给目标进程
      • ESRCH 目标进程或进程组不存在
kill(getppid(), 9); 	// 给父进程发送9号信号
kill(getpid(), 0); 		// 给本进程发送9号信号

int raise (int sig);

  • 头文件:
    • #include<signal.h>
  • 功能:把信号sig发送给当前进程。
  • 参数:
    • sig
      • 需要发送的信号编号或者宏值,0表示不发送任何信号。
    • 返回值:returns 0 on success, and nonzero for failure.

void abort (void);

  • 头文件:
    • #include <stdlib.h>
  • 功能:发送SIGABRT信号给当前进程,终止当前进程。

信号捕捉
sighandler_t signal (int signum, sighandler_t handler);

  • 头文件:
    • #include<signal.h>
  • 功能:设置某个信号的捕捉行为。
  • 参数:
    • signum :要捕捉的信号
    • handler :捕捉到的信号如何处理
      • SIG_IGN :忽略信号
      • SIG_DFL :使用信号默认行为
      • 回调函数 :程序员只负责定义函数捕捉到信号如何处理, 由内核调用。
    • 返回值:成功返回信号处理程序的先前值,失败返回 SIG_ERR 错误并设置 errno 。

注意:信号 SIGKILL 和 SIGSTOP 不能被捕获或忽略。


定时器

unsigned int alarm (unsigned int seconds);

  • 头文件:
    • #include <unistd.h>
  • 功能:设置闹钟以传递信号SIGALRM。非阻塞。再次调用定时器,之前的定时器失效。
  • 参数:
    • seconds:倒计时时长, 单位秒。=0,定时器无效。
  • 返回值:
    • 之前有定时器,返回倒计时剩余时间。
    • 之前无定时器,返回0。

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

  • 头文件:
    • #include <sys/time.h>
  • 功能:获取或设置间隔计时器的值,周期性闹钟。
  • 参数:
    • which : 定时器以什么时间计时
      • ITIMER_REAL : 真实时间,时间到达发送 SIGALRM
      • ITIMER_VIRTUAL : 用户时间,时间到达发送SIGVTALRM
      • ITIMER_PROF : 以该进程在用户和内核态下所消耗的时间来计算,时间到达发送SIGPROF
    • new_value : 设置定时器的属性
      • 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
        };
    • old_value : 上一次的定时时间参数,一般指定NULL
  • 返回值:
    • On success, zero is returned. On error, -1 is returned, and errno is set appropriately.

信号集

许多信号相关的系统调用都需要能表示一组不同的信号,Linux使用数据结构信号集sigset_t来表示一组信号。

在PCB中有两个非常重要的信号集:“阻塞信号集”、“未决信号集”。“未决”是一种状态,指的是从信号产生到信号被处理前的这一段时间。“阻塞”是一个开关动作,指的是阻止信号被处理,但不是阻止信号产生。

信号集相关函数

头文件: #include <signal.h>

int sigfillset (sigset_t *_set)

  • 功能:在信号集中设置所有信号, 将信号集中的所有的标志位置为1。

int sigemptyset (sigset_t *set);

  • 功能:清空信号集, 将信号集中的所有的标志位置为0。
  • 参数:
    • set :传出参数。需要操作的信号集。
  • 返回值:return 0 on success and -1 on error. 设置errno。

int sigaddset (sigset_t *set, int signum);

  • 功能:阻塞信号signum,设置信号集中某个信号对应的标志位为1

int sigdelset (sigset_t *set, int signum);

  • 功能:不阻塞信号signum,设置信号集中某个信号对应的标志位为0
  • 参数:
    • set :传出参数。需要操作的信号集。
    • signum :需要设置不阻塞的信号
  • 返回值:return 0 on success and -1 on error. 设置errno。

int sigismember (const sigset_t *set, int signum);

  • 功能:判断信号signum是否为信号集set中的成员
  • 参数:
    • set:需要判断的信号集set
    • signum:需要判断的信号signum
  • 返回值:returns 1 if signum is a member of set, 0 if signum is not a member, and -1 on error.

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

  • 功能:(设置阻塞,解除阻塞,替换)将自定义信号集中的数据设置到内核中
  • 参数:
    • how :对内核阻塞信号集如何处理
      • SIG_BLOCK :设置阻塞。将用户设置的阻塞信号集添加到内存中,内核中原数据不变 mask | set
      • SIG_UNBLOCK :解除阻塞。根据用户设置的数据,对内核中的数据进行解除阻塞 mask &= ~set
      • SG_SETMASK : 覆盖内核中原来的值
    • set:已经初始化好的用户自定义的信号集
    • oldset :保存设置之前内核中阻塞信号集的状态,基本是NULL。
  • 返回值:returns 0 on success and -1 on error. In the event of an error, errno is set to indicate the cause.
    • EFAULT
    • EINVAL

int sigpending (sigset_t *set);

  • 功能:获取内核中的未决信号集
  • 参数:
    • set:传出参数,保存内核中未决信号集中的信息。
  • 返回值:returns 1 if signum is a member of set, 0 if signum is not a member, and -1 on error.

案例:

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

int main() {
	int num = 0; // 计数
    // 1、创建信号集
    sigset_t set;

    // 2、将2 3 号信号添加到信号集中
    sigemptyset(&set);    // 清空信号集
    sigaddset(&set, SIGINT);
    sigaddset(&set, SIGQUIT);
    // 3、修改内核中信号集的阻塞状态
    sigprocmask(SIG_BLOCK, &set, NULL);

    while (num < 10) {
    	num++;
        // 获取当前未决信号集数据
        sigset_t pendingset;
        sigemptyset(&pendingset);    // 清空信号集
        // 将未决信号集中的数据放入pendingset中
        sigpending(&pendingset);

        // 遍历前32位
        for (int i = 1; i <= 31; i++) {
            if (sigismember(&pendingset, i) == 1) {
                // i号信号在未决信号集中
                printf("1");
            }
            else if (sigismember(&pendingset, i) == 0) {
                printf("0");
            }
            else {
                perror("sigismember");
                exit(-1);
            }
        }
        printf("\n");
        sleep(1);
        // if(num == 10) {
        //     // 解除阻塞
        //     sigprocmask(SIG_UNBLOCK, &set, NULL);
        // }
    }
    return 0;
}

运行结果:

无输入时,内核中前31个信号标志位均位0

输入Ctrl + C,信号SIGINT处于未决状态

输入Ctrl + \,信号SIGQUIT处于未决状态

添加计数变量num后,当num=10,sigprocmask()函数解除信号集set阻塞,Ctrl + C产生的信号SIGINT结束当前进程。


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

  • 功能:检查或者改变信号的处理。信号捕捉
  • 参数:
    • signum : 需要捕捉的信号的编号或者宏值(信号的名称)
    • act :捕捉到信号之后的处理动作
    • oldact : 上一次对信号捕捉相关的设置,一般使用NULL
  • 返回值:returns 0 on success and -1 on error. In the event of an error, errno is set to indicate the cause.
struct sigaction {
    void (*sa_handler)(int);                           // 函数指针,指向的函数就是信号捕捉到之后的处理函数
    void (*sa_sigaction)(int, siginfo_t *, void *);    // 不常用
    sigset_t sa_mask;                                  // 临时阻塞信号集,在信号捕捉函数执行过程中,临时阻塞某些信号。
    int sa_flags;                                      // 使用哪一个信号处理对捕捉到的信号进行处理
                                                       // 0,表示使用sa_handler; SA_SIGINFO表示使用sa_sigaction
    void (*sa_restorer)(void);                         // 被废弃掉了
};

案例:

#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/time.h>

void myalarm(int num) {
    printf("捕捉到了信号的编号是:%d\n", num);
    printf("xxxxxxx\n");
}

// 过3秒以后,每隔2秒钟定时一次
int main() {

    struct sigaction act;
    act.sa_flags = 0;             // 使用sa_handler处理
    act.sa_handler = myalarm;     // 使用回调函数
    sigemptyset(&act.sa_mask);    // 清空默认临时阻塞信号集

    // 捕捉 SIGALRM 信号
    sigaction(SIGALRM, &act, NULL);

    struct itimerval new_value;
    // 设置延迟的时间, 即3秒之后开始第一次定时
    new_value.it_value.tv_sec = 3;
    new_value.it_value.tv_usec = 0;
    // 设置间隔的时间, 即每次倒计时2秒
    new_value.it_interval.tv_sec = 2;
    new_value.it_interval.tv_usec = 0;

    int ret = setitimer(ITIMER_REAL, &new_value, NULL);    // 真实时间、非阻塞的
    printf("定时器开始了...\n");

    if (ret == -1) {
        perror("setitimer");
        exit(0);
    }

    while (1)
        ;

    return 0;
}

运行结果:

SIGCHLD信号

SIGCHLD信号产生的3个条件:
1. 子进程结束
2. 子进程暂停
3. 子进程继续运行
都会给父进程发送该信号,父进程默认忽略该信号。
可以使用SIGCHLD信号解决子进程退出产生僵尸进程的问题。

案例:

#include <signal.h>
#include <stdio.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>

void myFun(int num) {
    printf("捕捉到的信号 :%d\n", num);
    // 回收子进程PCB的资源,彻底结束子进程
    while (1) {
        // 回收所有子进程资源,设置为非阻塞
        int ret = waitpid(-1, NULL, WNOHANG);
        if (ret > 0) {
            // 返回值 >0, 已回收ret子进程资源
            printf("chiled die, pid = %d\n", ret);
        }
        else if (ret == 0) {
            // options=WNOHANG, 表示还有子进程存在
            break;
        }
        else if (ret == -1) {
            // 调用失败
            break;
        }
    }
}
int main() {
    // 防止出现子进程运行结束,父进程还没有注册完信号捕捉
    // 提前设置好阻塞信号集,阻塞SIGCHLD
    sigset_t set;
    sigemptyset(&set);
    sigaddset(&set, SIGCHLD);
    sigprocmask(SIG_BLOCK, &set, NULL);
    // 创建一些子进程
    pid_t pid;
    for (int i = 0; i < 5; i++) {
        pid = fork();
        if (pid == 0) {
            // 子进程
            // break防止子进程再去进入循环生成子进程,保证只有5个子进程
            break;
        }
    }

    if (pid > 0) {
        // 父进程
        // 捕捉子进程死亡时发出的SIGCHLD信号
        struct sigaction act;
        act.sa_flags = 0;
        act.sa_handler = myFun;
        sigemptyset(&act.sa_mask);
        sigaction(SIGCHLD, &act, NULL);
        // 信号捕捉注册完成,解除SIGCHLD阻塞
        sigprocmask(SIG_UNBLOCK, &set, NULL);

        while (1) {
            printf("parent process pid = %d\n", getpid());
            sleep(1);
        }
    }
    else if (pid == 0) {
        // 子进程
        printf("child process pid = %d\n", getpid());
    }
    return 0;
}

运行结果:

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐