信号发送与处理-下

问题

三种注册信号与处理函数的方法有什么区别???

信号的 OneShot 特性

System V 风格的 signal 函数,注册的信号处理是一次性的

进程收到信号后,调用由 signal 注册的处理函数

处理函数一旦执行,进程通过默认的方式处理后续相同信号

如果想要重复触发,那么必须再次调用 signal 注册处理函数

信号自身的屏蔽特性

在信号处理函数执行期间,很可能再次收到当前信号

  • 即:处理 A 信号的时候,再次收到 A 信号

对于 System V 风格的 signal 函数,会引起信号处理函数的重入

  • 即:调用处理函数的过程中,再次触发处理函数的调用

在注册信号处理函数时:

  • System V 风格的 signal 不屏蔽任何信号
  • BSD 风格的 signal 会屏蔽当前注册的信号

信号发送与处理-下_第1张图片

会打断当前 A 信号的信号处理函数,去执行 B 信号的信号处理函数,等 B 信号的信号处理函数执行完成返回后,再继续去执行 A 信号的信号处理函数

系统调用重启特性

系统调用期间,可能收到信号,此时进程必须从系统调用中返回

对于执行时间较长的系统调用 (write / read),被信号中断的可能性很大

如果希望信号处理之后,被中断的系统调用能够重启,则:可以通过 errno == EINTR 判断重启系统调用

系统调用重启示例代码

信号发送与处理-下_第2张图片

系统调用重启特性

System V 风格的 signal 函数

  • 系统调用被信号中断后,直接返回 -1,并且 errno == EINTR

BSD 风格的 signal 函数

  • 系统调用被中断,内核在信号处理函数结束后,自动重启系统调用

注意事项

并非所有的系统调用对信号中断都表现同样的行为

  • 一些系统调用支持信号中断后自动重启
    • read(),write(),wait(),waitpid(),ioctl(),...
  • 一些系统调用完全不支持信号中断后自动重启
    • poll(),select(),usleep(),...

三种方法的区别

信号发送与处理-下_第3张图片

现代信号处理注册函数

信号发送与处理-下_第4张图片

sigaction() 函数比 signal() 函数更加灵活,sigaction() 函数可以指定信号的一些特性,比如:ONTSHOT、屏蔽自身和其他信号、重启系统调用等

由于 signal() 函数在不同的 Linux 发行版中可能存在不同的特性,所以不推荐使用 signal() 来处理信号,推荐使用 sigaction() 函数来处理信号

现代信号处理语义分析

信号发送与处理-下_第5张图片

信号屏蔽与标记

sigset_t sa_mask

  • 信号屏蔽:sa_mask = SIGHUP | SIGINT | SIGUSR1;
  • 注意:并不是所有信号都可以被屏蔽,如:SIGKILL,SIGSTOP

int sa_flags

  • 信号特性:sa_flags = SA_ONESHOT | SA_RESTART;
  • 特殊特性 (SA_SIGINFO),信号处理时能够收到额外的附加信息

sa_flags 默认参数是不具备 ONESHOT 并且屏蔽自身、不会重启系统调用

信号状态小知识

信号产生

  • 信号来源,如:SI_KERNEL,SI_USER,SI_TIMER,...

信号未决

  • 从信号产生到信号被进程接收的状态 (处于未决状态的信号必然已经存在)

信号递达

  • 信号送达进程,被进程接收 (忽略,默认处理,自定义处理)

信号屏蔽 vs 信号阻塞

信号屏蔽

  • 信号处理函数执行期间,被屏蔽的信号不会被递送给进程 (针对多个信号)
  • sa_mask = SIGHUP | SIGINT | SIGUSR1;

信号阻塞

  • 信号处理函数执行期间,当前信号不会递送给进程 (当前信号)
  • act.sa_flags = SA_RESTART | SA_NODEFER;

main.c

// #define _GNU_SOURCE 
// #define _XOPEN_SOURCE   600
// #define _POSIX_C_SOURCE 200800L


#include 
#include 
#include 
#include 
#include 
#include 

void delay_handler(int sig)
{
    int i = 0;
    
    // sysv_signal(SIGINT, delay_handler);
    
    printf("begin delay handler...\n");
    
    for(i=0; i<5; i++)
    {
        printf("sleep %d ...\n", i);
        sleep(1);
    }
    
    printf("end delay handler...\n");
}

void signal_handler(int sig)
{
    printf("handler : sig = %d\n", sig);
}

int r_read(char* data, int len)
{
    int ret = -1;
    
    while( data &&
           ((ret = read(STDIN_FILENO, data, len-1)) == -1) &&
           (errno == EINTR) )
    {
        printf("restart syscall mannually...\n");
    }
    
    if( ret != -1 )
    {
        data[ret] = 0;
    }
    
    return ret;
}

int main(void)
{
    char buf[32] = {0};
    signal(SIGINT, signal_handler);
    // bsd_signal(40, signal_handler);
    // sysv_signal(SIGINT, signal_handler);
    // bsd_signal(SIGINT, delay_handler);
    
    r_read(buf, 32);
    
    printf("input: %s\n", buf);
    
    return 0;
}

第 57 行,我们为用 signal() 函数为 SIGINT 信号注册了信号处理函数

我们使用 strace 来跟踪这个进程的系统调用

信号发送与处理-下_第6张图片

可以看出 signal() 函数就是调用 sigaction() 函数来实现的

现代信号处理注册示例

信号发送与处理-下_第7张图片

现代信号发送

信号发送与处理-下_第8张图片

sigqueue(...) 的黄金搭档是 sigaction(...)

sa_flags 设置 SA_SIGINFO 标志位,可使用三参数信号处理函数

信号发送与处理-下_第9张图片

现代信号处理函数的关键参数

信号发送与处理-下_第10张图片

其中有几个常用的参数,si_signo: 信号值;si_code: 信号是怎么产生的;si_pid: 信号是由哪个进程发送过来的;si_value: 发送方发送信号传递过来的参数

现代信号发送处理示例

信号发送与处理-下_第11张图片

test2.c

#include 
#include 
#include 
#include 
#include 


int main(int argc, char* argv[])
{
    int pid = atoi(argv[1]);
    int sig = atoi(argv[2]);
    union sigval sv = {1234567};
    
    printf("current pid(%d) ...\n", getpid());
    printf("send sig(%d) to process(%d)...\n", sig, pid);
    
    // kill(pid, sig);
    sigqueue(pid, sig, sv);
    
    raise(SIGINT);
    
    while( 1 )
    {
        printf("while...\n");
        sleep(1);
    }
    
    return 0;
}

第 18 行,使用 sigqueue() 函数给指定进程发送指定信号,并且携带了一个 int 类型参数

main2.c

// #define _GNU_SOURCE 
//#define _XOPEN_SOURCE   600
//#define _POSIX_C_SOURCE 200800L


#include 
#include 
#include 
#include 
#include 
#include 

void delay_handler(int sig)
{
    int i = 0;
    
    printf("begin delay handler...\n");
    
    for(i=0; i<5; i++)
    {
        printf("sig(%d) - sleep %d ...\n", sig, i);
        sleep(1);
    }
    
    printf("end delay handler...\n");
}

void signal_handler(int sig, siginfo_t* info, void* ucontext)
{
    printf("handler : sig = %d\n", sig);
    printf("handler : info->si_signo = %d\n", info->si_signo);
    printf("handler : info->si_code = %d\n", info->si_code);
    printf("handler : info->si_pid = %d\n", info->si_pid);
    printf("handler : info->si_value = %d\n", info->si_value.sival_int);
    
}

int r_read(char* data, int len)
{
    int ret = -1;
    
    while( data &&
           ((ret = read(STDIN_FILENO, data, len-1)) == -1) &&
           (errno == EINTR) )
    {
        printf("restart syscall mannually...\n");
    }
    
    if( ret != -1 )
    {
        data[ret] = 0;
    }
    
    return ret;
}

int main(void)
{
    char buf[32] = {0};
    struct sigaction act = {0};

    
    act.sa_sigaction = signal_handler;
    act.sa_flags = SA_RESTART | SA_SIGINFO;
    
    sigaddset(&act.sa_mask, 40);
    sigaddset(&act.sa_mask, SIGINT);
    
    sigaction(40, &act, NULL);
    sigaction(SIGINT, &act, NULL);
  
    r_read(buf, 32);
    
    printf("input: %s\n", buf);
    
    return 0;
}

我们想要收到发送方发送信号携带的 4 个字节的数据时,必须使用 sigaction() 函数来注册信号,并且 sa_flags 要指定 SA_SIGINFO 参数

程序运行结果如下图所示:

信号发送与处理-下_第12张图片

main.out 进程在接收到信号值为 40 的信号后,成功获取到信号所携带的数据

思考

利用信号搞进程间通信靠谱吗???

你可能感兴趣的:(Linux系统编程,linux)