Linux IPC 进程间通信——信号 signal

信号是异步的,它是一种软件中断,Unix 系统起初设计主要目的是用于终止进程。所以发送信号使用 kill。

信号的三种处理方式

  • 忽略,对信号不采取任何处理
  • 捕获,暂停正在执行的代码,跳转到注册的信号处理函数。执行完后,跳回捕获信号的代码位置继续执行。
  • 默认,默认动作通常是终止进程

Tips: 有两个特殊信号必须保持默认动作,不可更改。它们是:SIGKILLSIGSTOP

信号标识符

每一个信号都以 SIG 前缀开头,这些标识符定义在 <signal.h> 头文件中,其实质是一个正整数。在不同 Unix 系统中信号名称到整数的映射可能不同,但是前 12 种信号通常是相同的。
信号的编号从 1 开始,总共有 31 个信号。

# 查看信号编号
kill -l

常见的信号产生方式

信号              产生方式              默认动作
SIGABRT    —>    abort()               终止且进行内存转储
SIGALRM    —>    alarm()               终止  
SIGCHLD    —>    子进程终止             忽略
SIGCONT    —>    进程停止后继续执行      忽略
SIGHUP     —>    控制终端关闭           终止
SIGKILL    —>    kill -9               终止
SIGPIPE    —>    管道破裂               终止
SIGINT     —>    Ctrl+C                终止
SIGQUIT    —>    Ctrl+\                终止且进行内存转储
SIGTSTP    —>    Ctrl+Z                挂起
SIGSTOP    —>    kill -19              挂起
SIGPROF    —>    定时器到期             终止
SIGILL     —>    非法指令               终止且进行内存转储

信号的继承

  • 调用 fork 创建子进程时,子进程继承父进程的所有信号。进程的处理方式与父进程相同。子进程不能继承挂起信号。
  • 调用 exec 创建进程时,进程继承所有信号,被原进程忽略的信号新进程也忽略,其它信号恢复为默认处理。

TipsCtrl+Z 时挂起信号使 fork 创建的子进程也挂起了,因为终端接收到 Ctrl+Z 时,是将信号发送给进程组的所有进程。

信号的权限

一个进程要给另一个进程发送信号,需要合适的权限,拥有 CAP_KILL 权限的进程可以向任何进程发送信号。

/* 判断是否有权限向目标进程发信号 */
int ret = kill(2711, 0);
if(ret)
  printf("have permission\n");
else
  printf("no permission\n");

相关编程接口

1. 信号处理的接口

#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signo, sighandler_t handler);

// 忽略信号
signal(SIGINT, SIG_IGN);

// 恢复默认处理
signal(SIGINT, SIG_DFL);

// 函数出错返回 SIG_ERR

2. 等待信号

#include <unistd.h>
int pause(void);

// 只有收到可捕获的信号才返回
// 如果信号是被忽略的进程不会被唤醒

3. 信号编号映射字符串

#include <signal.h>

void signal_handler(int signo){
  printf("%s\n", sys_siglist[signo]);
  psignal(signo, "signal");

  // #define _GNU_SOURCE
  // #include <string.h>
  printf("%s\n", strsignal(signo));
}

4. 发送信号

#include <sys/type.h>
#include <signal.h>

int kill(pid_t pid, int signo);
/*
pid > 0  向进程发送信号
pid = 0  向进程组的每一个进程发送信号
pid = -1 向除自身及 init 进程外的所有有权限发送信号的进程发送信号
pid < -1 向进程组的每一个进程发送信号,进程组的 ID 为 |pid|
*/

int raise(int signo);
/* 给进程自身发送信号 */
kill(getpid(), signo);

int killpg(int pgrp, int signo);
/* 给进程组发送信号 */
kill(-1 * pgrg, signo);

int sigqueue(pid_t pid, int signo, const union sigval value);
/* 带外数据信号发送,使用 sigaction 注册 sa_sigaction 处理函数
union sigval {
  int   sival_int;
  void *sival_ptr;
};
*/

// 案例1
int pid;
void sig_handler(int signo){
  if(pid == 0){
    if(SIGUSR1 == signo)
      printf("ok\n");
  }else{
    kill(pid, SIGUSR1);
  }
}

int main(){
  pid = fork();
  if(0 == pid){
    signal(SIGUSR1, sig_handler);
    while(1);
  }else if(0 < pid){
    signal(SIGTSTP, sig_handler);
    while(1);
  }
}

// 案例2 带外数据发送信号
sigval val = { 100 };
int ret = sigqueue(1777, SIGUSR2, val);

5. 信号的重入

一个信号可以打断之前的信号处理,如果两个信号的处理程序中,执行了相同的代码,代码必须确保重入安全。

那么对于重入不安全的函数,我们希望这样的信号进行串行处理,即阻塞后来的信号。

#include <signal.h>

// 阻塞的方式就是将信号放到一个阻塞集合中

/* 将集合初始化为空集 */
int sigemptyset(sigset_t * set);
/* 将集合初始化为满集 */
int sigfillset(sigset_t * set);
/* 将信号加入到指定的集合中 */
int sigaddset(sigset_t * set, int signo);
/* 将信号从指定的集合中删除 */
int sigdelset(sigset_t * set, int signo);
/* 判断信号是否在指定的集合中 */
int sigismember(sigset_t * set, int signo);

// 还有一些非标准的函数,为了保证移植性,应该避免使用

// 每一个进程都有一个信号掩码,这个信号掩码表示当前进程阻塞的信号

/* 管理当前进程信号掩码 */
int sigprocmask(int how, const sigset_t * set, sigset_t oldset);
/*
how : SIG_SETMASK 将 set 设置为当前信号掩码
      SIG_BLOCK   将 set 中的信号加入到信号掩码中
      SIG_UNBLOCK 将 set 中的信号从信号掩码中移出
如果 oldset 非空,oldset 为原来的信号掩码
*/ 

// 当产生了一个被阻塞的信号,内核不会发送该信号,该信号被称为待处理信号,当解除一个待处理信号的阻塞时,内核就会把它发送给进
/* 获取待处理信号集 */
int sigpending(sigset_t * set);

// POSIX 定义了一个允许进程临时更改信号掩码,并一直处于等待状态,直到产生一个终止该进程的信号,或该进程处理完等待的信号
/* 等待信号集 */
int sigsuspend(const sigset_t * set);

6. 高级信号管理

/* 改变信号的行为 */
int sigaction(int signo, const struct sigaction * act, struct sigaction * oldact);
/*
signo : 除 SIGKILL 和 SIGSTOP 外的所有值
act   : 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_flags 被指定 SA_SIGINFO, 那么信号的动作是执行 sa_sigaction
        sa_mask 指定了应该在信号处理函数执行时阻塞的信号集,除非设置了 SA_NODEFER 标志
    
*/

posted on 2023-09-19 12:41  kahle  阅读(82)  评论(0)    收藏  举报

导航