深度剖析信号可靠性

问题:基于信号发送的进程间通讯方式可靠吗?

信号查看 (kill -l)

 1) SIGHUP       2) SIGINT       3) SIGQUIT      4) SIGILL       5) SIGTRAP
 6) SIGABRT      7) SIGBUS       8) SIGFPE       9) SIGKILL     10) SIGUSR1
11) SIGSEGV     12) SIGUSR2     13) SIGPIPE     14) SIGALRM     15) SIGTERM
16) SIGSTKFLT   17) SIGCHLD     18) SIGCONT     19) SIGSTOP     20) SIGTSTP
21) SIGTTIN     22) SIGTTOU     23) SIGURG      24) SIGXCPU     25) SIGXFSZ
26) SIGVTALRM   27) SIGPROF     28) SIGWINCH    29) SIGIO       30) SIGPWR
31) SIGSYS      34) SIGRTMIN    35) SIGRTMIN+1  36) SIGRTMIN+2  37) SIGRTMIN+3
38) SIGRTMIN+4  39) SIGRTMIN+5  40) SIGRTMIN+6  41) SIGRTMIN+7  42) SIGRTMIN+8
43) SIGRTMIN+9  44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9  56) SIGRTMAX-8  57) SIGRTMAX-7
58) SIGRTMAX-6  59) SIGRTMAX-5  60) SIGRTMAX-4  61) SIGRTMAX-3  62) SIGRTMAX-2
63) SIGRTMAX-1  64) SIGRTMAX

信号的分类

  • 不可靠信号 (传统信号,也是已经被证用赋予含义的信号)

    • 信号值在 [1, 31] 之间的所有信号
  • 可靠信号 (实时信号,未被证用且赋予含义,可自定义使用)

    • 信号值在 [SIGRTMIN, SIGRTMAX] , 即: [34, 64]
    • SIGRTMIN 👉 34
    • SIGRTMAX 👉 64

信号小知识

  • 信号 32 与 信号 33 (SIGCANCEL & SIGSETXID)被 NPTL 线程库证用
  • NPTL 👉 Native Posix Threading Library

    • 即: POSIX 线程标准库,Linux 可以使用这个线程库进行多线程编程
  • 对于 Linux 内核,信号 32 是最小的可靠信号
  • SIGRTMIN 在 signal.h 中定义,不同平台的 linux 可能不同 (arm linux)

不可靠信号 VS 可靠信号

  • 不可靠信号

    • 内核不保证信号可以传递到目标进程(内核对信号状态进行标记)
    • 如果信号处于未决状态,并且相同信号被发送,内核丢弃后续相同信号

      • 即,在信号处理函数调用过程中,又有相同信号发出,那只会有其中一次的信号被处理
      • 原理,在 linux 内部,当信号处于未决态时又有相同信号发出,那么对应信号的 1bit 会被标记为 1 [... 0 0 1 0 0 ...] 表示又有信号递达(无法统计次数,因此会被丢弃),等到当前信号处理完成后,处理被标记抵达的信号,同时对应 bit 标记为 0
  • 可靠信号

    • 内核维护信号队列,未决信号位于队列中,因此信号不会被丢弃
    • 严格意义上,信号队列有上限,因此不能无限制保存可靠信号

一些注意事项

  • 不可靠信号的默认处理行为可能不同 (忽略,结束)
  • 可靠信号的默认处理行为都是结束进程
  • 信号的可靠性由信号数值决定,与发送方式无关
  • 信号队列的上线可通过命令设置

    • 查询信号队列上限: ulimit -i
    • 设置信号队列上限: ulimit -i 10000

关于不可靠信号的默认处理行为都是结束进程的实验
main.c
#include <stdio.h>
#include <unistd.h>
#include <signal.h>

int main(void)
{
    while (1) {
        sleep(1);
    }

    return 0;
}
test.c
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>

int main(int argc, char *argv[])
{
    int pid = atoi(argv[1]);
    int sig = atoi(argv[2]);
    union sigval sv = {123456};

    printf("send sig(%d) to process (%d)...\n", sig, pid);

    sigqueue(pid, sig, sv);

    raise(SIGINT);

    while (1) {
        printf("while...\n");
        sleep(1);
    }

    return 0;
}
输出:
wu_tiansong@ubuntu-server:~/test$ ps -ajx | grep "a.out"
3775537 3777006 3777005 3775537 pts/19   3777005 S+    1007   0:00 grep --color=auto a.out
wu_tiansong@ubuntu-server:~/test$ ./a.out &
[1] 3777087
wu_tiansong@ubuntu-server:~/test$ ps -ajx | grep "a.out"
3775537 3777087 3777087 3775537 pts/19   3777105 S     1007   0:00 ./a.out
3775537 3777106 3777105 3775537 pts/19   3777105 S+    1007   0:00 grep --color=auto a.out
wu_tiansong@ubuntu-server:~/test$ ./test.out 3777087 40
current pid(3777303) ..
send sig(40) to process (3777087)...

[1]+  Real-time signal 6      ./a.out              ====>>>>> SIGABRT 6 C 由abort(3)发出的退出指令 
wu_tiansong@ubuntu-server:~/test$ ps -ajx | grep "a.out"
3775537 3777319 3777318 3775537 pts/19   3777318 S+    1007   0:00 grep --color=auto a.out

信号可靠性实验设计

  • 目标: 验证信号可靠性(不可靠信号 or 可靠信号)
  • 方案: 对目标进程 ”疯狂“ 发送 N 次信号,验证信号处理函数调用次数
  • 预备函数:

    • int sigaddset(sigset_t *set, int signum);
    • int sigdelset(sigset_t *set, int signum);
    • int sigfillset(sigset_t *set);
    • int sigemptyset(sigset_t *set);
    • int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
sigfillset()函数是标准C库(libc)中用于操作自定义信号集的一个函数。sigfillset()函数用于初始化一个自定义信号集,将其所有信号都填充满,也就是将信号集中的所有的标志位置为1,使得这个集合包含所有可接受的信号,也就是阻塞所有信号。这个函数可以用于快速创建一个包含所有信号的信号集,然后可以根据需要删除其中的某些信号。


sigemptyset()函数是标准C库(libc)中用于操作自定义信号集(signal set)的一个函数。信号集是一个包含多个信号的集合,通常用于信号处理和控制。sigemptyset()函数的作用是初始化一个自定义信号集,将其所有信号都清空,也就是将信号集中的所有的标志位置为0,使得这个集合不包含任何信号,也就是不阻塞任何信号。此函数通常在设置信号处理程序(signal handler)之前调用,以确保信号集的正确初始化。

信号可靠性实验

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

static int g_count = 0;
static int g_obj_sig = 0;

void signal_handler(int sig, siginfo_t *info, void *ucontext)
{
    if (sig == g_obj_sig) {
        ++g_count;
    }
}

int main(int argc, char *argv[])
{
    struct sigaction act = {0};
    sigset_t set = {0};
    int i = 0;

    g_obj_sig = atoi(argv[1]);

    printf("current pid(%d) ..\n", getpid());

    act.sa_sigaction = signal_handler;
    act.sa_flags = SA_SIGINFO;

    sigaction(g_obj_sig, &act, NULL);

    sigfillset(&set);                           // 屏蔽所有信号
    sigprocmask(SIG_SETMASK, &set, NULL);       // 即,发送给进程的信号,无法递达,处于未决态

    for (i=0; i<15; ++i) {
        sleep(1);  // 给发送端预留 15 S 发送时间
        printf("i = %d\n", i);
    }

    sigemptyset(&set);                          // 解除对信号的屏蔽
    sigprocmask(SIG_SETMASK, &set, NULL);       // 未决态的信号,将递达进程

    printf("g_count = %d\n", g_count);

    return 0;
}
test.c
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>

int main(int argc, char *argv[])
{
    int pid = atoi(argv[1]);
    int sig = atoi(argv[2]);
    union sigval sv = {123456};
    int i = 0;

    printf("current pid(%d) ..\n", getpid());
    printf("send sig(%d) to process (%d)...\n", sig, pid);

    for (i=0; i<20000; ++i) {
        sigqueue(pid, sig, sv);
    }

    return 0;
}
对于不可靠信号,使用 2 进行测试
终端 1:
wu_tiansong@ubuntu-server:~/test$ ./test.out 3791786  2
current pid(3791909) ..
send sig(2) to process (3791786)...

终端 2:
wu_tiansong@ubuntu-server:~/test$ ./a.out 2
current pid(3791786) ..
i = 0
i = 1
i = 2
i = 3
i = 4
i = 5
i = 6
i = 7
i = 8
i = 9
i = 10
i = 11
i = 12
i = 13
i = 14
g_count = 1  ========>>>>> 信号处理函数只执行了 1 次,因为 2 为不可靠信号 (如果不能及时处理,内核仅标记一次)
对于可靠信号,使用 40 进行测试 (发送信号数量在信号队列上限内)
终端 1:
wu_tiansong@ubuntu-server:~/test$ ./test.out 3794384  40
current pid(3794523) ..
send sig(40) to process (3794384)...

终端 2:
wu_tiansong@ubuntu-server:~/test$ ulimit -i
513893
wu_tiansong@ubuntu-server:~/test$ ./a.out 40
current pid(3794384) ..
i = 0
i = 1
i = 2
i = 3
i = 4
i = 5
i = 6
i = 7
i = 8
i = 9
i = 10
i = 11
i = 12
i = 13
i = 14
g_count = 20000  ========>>>>> 信号处理函数被调用了 20000 次,与发送端一致

对于可靠信号,使用 40 进行测试 (发送信号数量在信号队列上限外)

test.c

for (i=0; i<600000; ++i) {  // 修改信号发送数量
    sigqueue(pid, sig, sv);
}
终端 1 :
wu_tiansong@ubuntu-server:~/test$ ./test.out 3796163  40
current pid(3796247) ..
send sig(40) to process (3796163)...

终端 2 :
wu_tiansong@ubuntu-server:~/test$ ulimit -i
513893
wu_tiansong@ubuntu-server:~/test$ ./a.out 40
current pid(3796163) ..
i = 0
i = 1
i = 2
i = 3
i = 4
i = 5
i = 6
i = 7
i = 8
i = 9
i = 10
i = 11
i = 12
i = 13
i = 14
g_count = 513893   =====> 超过信号丢列上限,出现数据丢失

进程间通讯实验设计

基于信号的进程间通讯实验

  • A 进程将 TLV 类型的数据通过可靠信号传递给 B 进程

    • TLV 👉 (type, length, value)
    • 由于可靠信号的限制,每次传输 4 字节数据
  • B 进程首先接收 4 字节数据 (type 或 type + length)

    • 根据接收到的 length 数据,多次接收后续的字节数据
    • 每次只接收 4 字节数据,设计层面需要进行状态处理

状态设计

image.png

实战进程间通讯

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


static int g_type = -1;
static int g_length = -1;
static int g_current = -1;
static char *g_data = NULL;
static int g_obj_sig = 0;

static void ipc_data_handler(char *data, int length)
{
    printf("ipc data : %s\n", data);
}

static void signal_handler(int sig, siginfo_t *info, void *ucontext)
{
    if (sig == g_obj_sig) {
        int data = info->si_value.sival_int;

        if (g_current == -1) {
            g_type = data & 0xFFFF;
            g_length = (data >> 16) & 0xFFFF;
            g_current = 0;
            g_data = malloc(g_length);

            if (!g_data) {
                exit(-1);
            }
        } else {
            int i = 0;

            while ((i < 4) && (g_current < g_length)) {
                g_data[g_current++] = (data >> (i * 8)) & 0xFF;
                i++;
            }
        }

        if (g_current == g_length) {
            ipc_data_handler(g_data, g_length);
            g_type = -1;
            g_length = -1;
            g_current = -1;
            free(g_data);
        }
    }
}

int main(int argc, char *argv[])
{
    struct sigaction act = {0};
    sigset_t set = {0};
    int i = 0;

    g_obj_sig = atoi(argv[1]);

    printf("current pid(%d) ..\n", getpid());

    act.sa_sigaction = signal_handler;
    act.sa_flags = SA_SIGINFO | SA_RESTART;

    sigaddset(&act.sa_mask, g_obj_sig);
    sigaction(g_obj_sig, &act, NULL);

    while (1) {
        sleep(1);
    }

    return 0;
}
test.c
#include <bits/types/siginfo_t.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>
#include <string.h>

typedef struct _tlv_t
{
    short type;
    short length;
    char data[];
}Message;

int main(int argc, char *argv[])
{
    int pid = atoi(argv[1]);
    int sig = atoi(argv[2]);
    char *data =argv[3];
    int len = (sizeof(Message) + strlen(data) + 1);
    int size = ((len / 4) + !!(len % 4)) * 4;
    Message *msg = malloc(size);

    if (msg) {
        union sigval sv = {0};
        int i = 0;
        int *pi = (int*)msg;

        printf("current pid(%d) ..\n", getpid());
        printf("send sig(%d) to process (%d)...\n", sig, pid);

        msg->type = 0;
        msg->length = len - sizeof(Message);

        strcpy(msg->data, data);

        for (i=0; i<size; i+=4) {
            sv.sival_int = *pi++;
            sigqueue(pid, sig, sv);
        }
    }

    free(msg);

    return 0;
}

输出:

a.out 输出:
wu_tiansong@ubuntu-server:~/test$ ./a.out 40
current pid(2508614) ..
ipc data : D.T.Software
ipc data : D.T.Software


test.out 输出:
wu_tiansong@ubuntu-server:~/test$ ./test.out 2508614 40 D.T.Software
current pid(2508752) ..
send sig(40) to process (2508614)...
wu_tiansong@ubuntu-server:~/test$ ./test.out 2508614 40 D.T.Software
current pid(2508809) ..
send sig(40) to process (2508614)...

TianSong
734 声望138 粉丝

阿里山神木的种子在3000年前已经埋下,今天不过是看到当年注定的结果,为了未来的自己,今天就埋下一颗好种子吧