问题:如何编写信号安全的应用程序???

信号处理回避模式

Linux 应用程序安全性讨论

  • 场景一:不需要处理信号

    • 应用程序实现单一功能,不需要关注信号

      • 如:数据处理程序,文件加密,科学计算程序,等场景单一的程序
  • 场景二:需要处理信号

    • 应用程序长时间运行,需要关注信号,并及时处理

      • 如:服务端程序,上位机程序,等场景复杂的程序

场景一: 不需要信号处理(单一功能应用程序)

在代码层面,直接阻塞/屏蔽所有可能的信号!!(信号始终是未决状态,无法递达进程)
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>
#include <error.h>

typedef struct {
    int argc;
    void *argv;
    void (*job_func)(int , void *);
}Job;

static void do_job(int argc, void *argv)
{
    int i = 5;

    printf("do_job: %d --> %s\n", argc, (char *)argv);

    while (i-- > 0) {
        sleep(1);
    }
}

static Job g_job[] = {
    {1, "C", do_job},
    {2, "C++", do_job},
    {3, "Java", do_job},
};

static const int g_jlen = sizeof(g_job) / sizeof(*g_job);
static siginfo_t g_sig_arr[65] = {0};
static const int g_slen = sizeof(g_sig_arr) / sizeof(*g_sig_arr);

static void mask_all_signal()
{
    sigset_t set = {0};

    sigfillset(&set);
    sigprocmask(SIG_SETMASK, &set, NULL);
}

int main(int argc, char *argv[])
{
    int i = 0;

    mask_all_signal();                   // 屏蔽所有信号,使所有信号无法递达进程, 依次避免主程序执行流被打断

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

    while (i < g_jlen) {                 // 任务分解,子任务串行执行
        int argc = g_job[i].argc;
        char *argv = g_job[i].argv;

        g_job[i].job_func(argc, argv);

        ++i;
    }

    return 0;
}
输出:
wu_tiansong@ubuntu-server:~/test$ ./a.out 
current pid(3295494) ..
do_job: 1 --> C
do_job: 2 --> C++
^C^C^C^C^C^Cdo_job: 3 --> Java         // 信号不再被处理

信号处理标记模式

场景二:需要处理信号(长时间运行的应用)

  • 同步方案

    • 通过标记同步处理信号,整个应用中只有一个执行流 【主任务代码与信号处理代码,交替执行】
  • 异步方案

    • 专用任务处理,应用中存在多个执行流(多线程应用)
    • 设置专用信号处理任务(线程),其它任务忽略信号,专注功能实现

同步解决方案(单任务)

  • 信号处理逻辑 与 程序逻辑 位于同一个上下文

    • 即:信号处理函数 与 主函数 不存在资源竞争关系
  • 方案设计一

    • 将任务分解为子任务(每个任务可对应一个函数)
    • 信号递达时,信号处理函数中仅标记递达状态 【类似于linux驱动的软中断】
    • 子任务处理结束后,真正执行信号处理
main.c

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>
#include <error.h>

typedef struct {
    int argc;
    void *argv;
    void (*job_func)(int , void *);
}Job;

static void do_job(int argc, void *argv)
{
    int i = 5;

    printf("do_job: %d --> %s\n", argc, (char *)argv);

    while (i-- > 0) {
        sleep(1);
    }
}

static Job g_job[] = {
    {1, "C", do_job},
    {2, "C++", do_job},
    {3, "Java", do_job},
};

static const int g_jlen = sizeof(g_job) / sizeof(*g_job);
static siginfo_t g_sig_arr[65] = {0};
static const int g_slen = sizeof(g_sig_arr) / sizeof(*g_sig_arr);

static void signal_handler(int signo, siginfo_t *info, void *context)  // 仅作标记,不做具体信号处理
{
    g_sig_arr[signo] = *info;
    g_sig_arr[0].si_signo++;
}

static void do_sig_process(siginfo_t *info)
{
    printf("do_sig_process: %d --> %d\n", info->si_signo, info->si_value.sival_int);

    // do process for the obj signal
    switch (info->si_signo) {
        case SIGINT:
            // call process function for SIGINT
            break;

        case 40:
            // call process function for 40
            break;

        default:
            break;
    }
}

// 根据标记,执行信号处理逻辑
static void process_signal()
{
    if (g_sig_arr[0].si_signo) {
        int i = 0;

        for (i=1; i<g_slen; ++i) {
            if (g_sig_arr[i].si_signo) {
                do_sig_process(&g_sig_arr[i]);

                g_sig_arr[i].si_signo = 0;
                g_sig_arr[0].si_signo--;
            }
        }
    }
}

static void app_init()
{
    struct sigaction act = {0};
    sigset_t set = {0};
    int i = 0;

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

    for (i = 1; i <= 64; ++i) {
        sigaddset(&set, i);
    }

    for (i = 1; i <= 64; ++i) {
        sigaction(i, &act, NULL);
    }
}

int main(int argc, char *argv[])
{
    int i = 0;

    app_init();

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

    while (i < g_jlen) {
        int argc = g_job[i].argc;
        char *argv = g_job[i].argv;

        g_job[i].job_func(argc, argv);

        process_signal();

        ++i;
    }

    return 0;
}
输出:
wu_tiansong@ubuntu-server:~/test$ ./a.out 
current pid(175844) ..
do_job: 1 --> C
^Cdo_sig_process: 2 --> 0     // 在子任务处理完成后,被标记的信号被处理
do_job: 2 --> C++
do_job: 3 --> Java
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]);
    int num = atoi(argv[3]);
    union sigval sv = {0};
    int i = 0;

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

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

    return 0;
}
第 1 次输出:
test.out 输出:
wu_tiansong@ubuntu-server:~/test$ ./test.out 176311 40 100  // 40 信号发送 100 次
current pid(176352) ..
send sig(40) to process(176311) ...

a.out 输出:
wu_tiansong@ubuntu-server:~/test$ ./a.out                  // 信号丢失,只接收到最后 1 次
current pid(176311) ..
do_job: 1 --> C
do_sig_process: 40 --> 99
do_job: 2 --> C++
do_job: 3 --> Java

存在问题

由于给每个信号唯一的标记位置,因此,所有信号转变为不可靠信号;并且仅保留最近递达的信号信息

总结

对于不可靠信号[1-31],可以使用此方案

信号处理主动模式

针对上述问题可能的改进方案:
方案:标记位置设计为链表,信号递达后在对应位置的链表处增加结点保留信号信息。
讨论:在节点创建时,可能会使用到 malloc ,意味着信号的安全性并未解决

同步解决方案(单任务)

  • 方案设计二

    • 将任务分解为子任务(每个任务可对应一个函数)
    • 创建信号文件描述符,并阻塞所有信号(可靠信号递达前位于内核队列中
    • 子任务处理结束后,通过 select
    • 机制判断是否有信号需要处理

      • true -> 处理信号 ; false -> 等待超时

关键系统函数

#include <sys/select.h>
#include <sys/signalfd.h>

int signalfd(int fd, const sigset_t *mask, int flags);  // 创建信号文件描述符
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);  // 信号监听

使用 signalfd 处理信号

先屏蔽所有信号(无法递达进程),之后为屏蔽信号创建文件描述符
当时机成熟,通过 read() 系统调用读取未决信号(主动接收信号)。

/// ===> 屏蔽所有信号, 避免被动的接收信号
sigset_t set = {0};

sigfillset(&set);
sigprocmask(SIG_SETMASK, &set, 0);


/// ===> 创建信号描述符
g_sig_fd = signalfd(-1, &set, 0);


/// ===> 读取未决信号
struct signalfd_siginfo si = {0};

if (read(g_sig_fd, &si, sizeof(si)) > 0) {
    do_sig_process(&si);
}

使用 slelct 监听文件描述符

fd_set reads = {0};
fd_set temps = {0};
struct timeval timeout = {0};

FD_ZERO(&reads);
FD_SET(0, &read);  // 关注 0 描述符

while (1) {
    int r = 1;    
    temps = reads;

    timeout.tv_sec = 0;
    timeout.tv_usec = 5000;

    r = select(1, &temps, 0, 0, &timeout);

    if (r > 0) {
         if (FD_ISSET(0, &temps)) {
            // read data from console
        }
    } 
    else if (r == 0) {
        usleep(10000);
    }
    else {
        break;
    };
}

使用 select 处理信号

static int max = 0;

fd_set reads = {0};
fd_set rset = {0};
struct timeval timeout = {0};

FD_ZERO(&reads);
FD_SET(g_sig_fd, &reads);

max = g_sig_fd;
rset = reads;

timeout.tv_sec = 0;
timeout.tv_usec = 5000;

while (select(max + 1, &rset, 0, 0, &timeout) > 0) {
    max = select_handler(&rset, &reads, max);
}

编程实验

main.c

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/signalfd.h>
#include <unistd.h>
#include <signal.h>
#include <error.h>

typedef struct {
    int argc;
    void *argv;
    void (*job_func)(int , void *);
}Job;

static void do_job(int argc, void *argv)
{
    int i = 5;

    printf("do_job: %d --> %s\n", argc, (char *)argv);

    while (i-- > 0) {
        sleep(1);
    }
}

static Job g_job[] = {
    {1, "C", do_job},
    {2, "C++", do_job},
    {3, "Java", do_job},
};

static const int g_jlen = sizeof(g_job) / sizeof(*g_job);
static siginfo_t g_sig_arr[65] = {0};
static const int g_slen = sizeof(g_sig_arr) / sizeof(*g_sig_arr);

static int g_sig_fd = -1;

static int mask_all_signal()
{
    sigset_t set = {0};

    sigfillset(&set);
    sigprocmask(SIG_SETMASK, &set, 0);

    return signalfd(-1, &set, 0);
}

static void do_sig_process(struct signalfd_siginfo *info)
{
    printf("do_sig_process: %d --> %d\n", info->ssi_signo, info->ssi_int);

    // do process for the obj signal
    switch (info->ssi_signo) {
        case SIGINT:
            // call process function for SIGINT
            break;

        case 40:
            // call process function for 40
            break;

        default:
            break;
    }
}

static int select_handler(fd_set *rset, fd_set *reads, int max)
{
    int ret = max;
    int i = 0;

    for (i=0; i<=max; ++i) {
        if (FD_ISSET(i, rset)) {
            if (i == g_sig_fd) {
                struct signalfd_siginfo si = {0};

                if (read(g_sig_fd, &si, sizeof(si)) > 0) {
                    do_sig_process(&si);
                }
            }
            else {
                // handle other fd
                // the return value should be the max fd
                // max = (max < fd) ? fd : max;
            }

            FD_CLR(i, reads);

            ret = max;
        }
    }
}

static void process_signal()
{
    static int max = 0;

    fd_set reads = {0};
    fd_set rset = {0};
    struct timeval timeout = {0};

    FD_ZERO(&reads);
    FD_SET(g_sig_fd, &reads);

    max = g_sig_fd;
    rset = reads;

    timeout.tv_sec = 0;
    timeout.tv_usec = 5000;

    while (select(max+1, &rset, 0, 0, &timeout) > 0) {
        max = select_handler(&rset, &reads, max);
    }
}

static void app_init()
{
    g_sig_fd = mask_all_signal();
}

int main(int argc, char *argv[])
{
    int i = 0;

    app_init();

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

    while (i < g_jlen) {
        int argc = g_job[i].argc;
        char *argv = g_job[i].argv;

        g_job[i].job_func(argc, argv);

        process_signal();

        ++i;
    }

    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]);
    int num = atoi(argv[3]);
    union sigval sv = {0};
    int i = 0;

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

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

    return 0;
}
输出
// ===> test.out 输出:
wu_tiansong@ubuntu-server:~/test$ ./test.out 3945826  40 100 
current pid(3945895) ...
send sig(40) to process(3945826)...


// ===> a.out 输出:
wu_tiansong@ubuntu-server:~/test$ ./a.out 
current pid(3945826) ..
do_job: 1 --> C
do_sig_process: 40 --> 0
do_sig_process: 40 --> 1
do_sig_process: 40 --> 2
do_sig_process: 40 --> 3
...
...
do_sig_process: 40 --> 97
do_sig_process: 40 --> 98
do_sig_process: 40 --> 99    ===> 没有出现信号丢失
do_job: 2 --> C++
do_job: 3 --> Java

信号处理专职模式

存在问题

由于使用了 select 机制,即便没有信号要处理,也需要等待 select 超时,任务实时性收到影响。
关于信号处理主动模式的缺点,有使用 `struct timeval timeout = {0};`,导致程序得实时性存在问题(信号和子任务得不到实时处理)。
思考:是否可以兼顾 信号处理 与 任务执行 的实时性??

异步解决方案(多任务)

  • 使用独立任务处理信号,程序逻辑在其它任务中执行
  • 即:通过多线程分离 信号处理 与 程序逻辑

    • 主线程:专用处理信号
    • 其它线程:完成程序功能
问题:信号递达进程后,在哪一个执行流(线程)中进行处理?

多线程信号处理

  • 信号的发送目标是进程,而不是某个特定的线程
  • 发送给进程的信号仅递送给一个线程
  • 每个线程拥有独立的信号屏蔽掩码
  • 内核从不会阻塞目标信号的线程中随机选择

  • 主线程:对目标信号设置信号处理的方式

    • 当信号递达进程时,只可能是主线程进行信号处理
  • 其它线程:首先屏蔽所有可能的信号,之后执行任务代码

    • 无法接收到信号,不具备信号处理能力

进程与线程

  • 进程:应用程序的一次加载执行(系统进行资源分配的基本单位)
  • 线程:进程中的程序执行流

    • 一个进程中可以存在多个线程(至少存在一个线程)
    • 每个线程执行不同的任务(多个线程可并行执行)
    • 同一个进程中的多个线程共享进程的系统资源
进程
    主线程,专门处理信号
    子线程1, 执行任务
    子线程2, 处理任务
    ...

Linux 多线程 API 函数

  • 头文件: #include <pthread.h>
  • 线程创建函数: int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void*), void *arg);

    • thread: pthread_t 变量的地址,用于返回线程标识
    • attr: 线程的属性,可设置为 NULL, 即, 使用默认属性
    • start_routine: 线程入口函数
    • arg: 线程入口函数参数
  • 线程标识

    • pthread_t pthread_self(void);
    • 获取当前线程的 ID 标识
  • 线程等待

    • int pthread_join(pthread thread, void **retval);
    • 等待目标线程执行结束

多线程编程示例

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

void *thread_entry(void *arg)
{
    pthread_t id = pthread_self();
    int n = (long)arg;
    int i = 0;

    while (i < n) {
        printf("id = %ld, i = %d\n", id, i);
        sleep(1);
        i++;
    }

    return NULL;
}

int main()
{
    pthread_t t1 = {0};
    pthread_t t2 = {0};
    int arg1 = 5;
    int arg2 = 10;

    pthread_create(&t1, NULL, thread_entry, (void*)arg1);
    pthread_create(&t2, NULL, thread_entry, (void*)arg2);

    printf("t1 = %ld\n", t1);
    printf("t2 = %ld\n", t2);

    pthread_join(t1, NULL);
    pthread_join(t2, NULL);

    return 0;
}
输出:
wu_tiansong@ubuntu-server:~/test$ ./a.out 
t1 = 140333129008896
t2 = 140333120616192
id = 140333129008896, i = 0
id = 140333120616192, i = 0
id = 140333129008896, i = 1
id = 140333120616192, i = 1
id = 140333129008896, i = 2
id = 140333120616192, i = 2
id = 140333120616192, i = 3
id = 140333129008896, i = 3
id = 140333120616192, i = 4
id = 140333129008896, i = 4
id = 140333120616192, i = 5
id = 140333120616192, i = 6
id = 140333120616192, i = 7
id = 140333120616192, i = 8
id = 140333120616192, i = 9

异步方案示例

main.c

#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>
#include <error.h>
#include <sys/signalfd.h>
#include <pthread.h>

typedef struct {
    int argc;
    void *argv;
    void (*job_func)(int , void *);
}Job;

static void do_job(int argc, void *argv)
{
    int i = 5;

    printf("do_job: %d --> %s\n", argc, (char *)argv);

    while (i-- > 0) {
        sleep(1);
    }
}

static Job g_job[] = {
    {1, "C", do_job},
    {2, "C++", do_job},
    {3, "Java", do_job},
};

static const int g_jlen = sizeof(g_job) / sizeof(*g_job);
static siginfo_t g_sig_arr[65] = {0};
static const int g_slen = sizeof(g_sig_arr) / sizeof(*g_sig_arr);

static int mask_all_signal()
{
    sigset_t set = {0};

    sigfillset(&set);
    sigprocmask(SIG_SETMASK, &set, 0);

    return signalfd(-1, &set, 0);
}

static void do_sig_process(siginfo_t *info)
{
    printf("do_sig_process: %d --> %d\n", info->si_signo, info->si_value.sival_int);

    // do process for the obj signal
    switch (info->si_signo) {
        case SIGINT:
            // call process function for SIGINT
            break;

        case 40:
            // call process function for 40
            break;

        default:
            break;
    }
}

static void signal_handler(int sig,
                           siginfo_t* info,
                           void* ucontext)
{
    do_sig_process(info);
}

static void *thread_entry(void *arg)
{
    int i = 0;

    mask_all_signal();

    while (i < g_jlen) {
        int argc = g_job[i].argc;
        char *argv = g_job[i].argv;

        g_job[i].job_func(argc, argv);

        ++i;
    }
}

static void app_init()
{
    struct sigaction act = {0};
    sigset_t set = {0};
    int i = 0;

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

    for (i = 1; i <= 64; ++i) {
        sigaddset(&set, i);
    }

    for (i = 1; i <= 64; ++i) {
        sigaction(i, &act, NULL);
    }
}

int main(int argc, char *argv[])
{
    pthread_t tid = 0;

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

    app_init();

    pthread_create(&tid, NULL, thread_entry, NULL);

    pthread_join(tid, NULL);

    mask_all_signal();  // printf 为不可重入函数,屏蔽所有信号进行保护

    printf("add end\n");

    return 0;
}
输出:对于可靠信号可得到及时处理
// ===> test.out 输出:
wu_tiansong@ubuntu-server:~/test$ ./test.out 4031244  40 100 
current pid(4031381) ...
send sig(40) to process(4031244)...

// ===> a.out 输出:
wu_tiansong@ubuntu-server:~/test$ ./a.out 
current pid(4031244) ..
do_job: 1 --> C
do_sig_process: 40 --> 0     // ===> 可靠信号被得到及时处理
do_sig_process: 40 --> 1
do_sig_process: 40 --> 2
do_sig_process: 40 --> 3
...
...
do_sig_process: 40 --> 98
do_sig_process: 40 --> 99
do_job: 2 --> C++
do_job: 3 --> Java
add end
输出:对于不可靠信号可以得到技术处理
wu_tiansong@ubuntu-server:~/test$ ./a.out 
current pid(4034521) ..
do_job: 1 --> C
^Cdo_sig_process: 2 --> 0     // ===> 不可靠信号被得到及时处理
^Cdo_sig_process: 2 --> 0
^Cdo_sig_process: 2 --> 0
^Cdo_sig_process: 2 --> 0
^Cdo_sig_process: 2 --> 0
do_job: 2 --> C++
do_job: 3 --> Java
add en

信号设计模式小结

  • 多数程序不需要处理信号,因此可以直接屏蔽信号
  • 需要处理信号的程序,重点考虑信号安全性问题

    • 同步处理方案,通过设计让任务代码和信号处理代码交替执行

      • 问题:信号处理是否及时?任务执行是否及时
    • 异步处理方案,任务代码与信号处理代码位于不同执行流

      • 问题:将信号安全性问题转换为线程安全性问题。因此,程序本身是否做到线程安全
  • 没有完美的解决方案,之后适合的解决方案

  • 下一专题预告

    • 深入进程与线程专题
    • Linux 进程调度深度剖析
    • Linux 多任务 / 多线程 程序设计

TianSong
737 声望139 粉丝

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