问题:如何编写信号安全的应用程序???
信号处理回避模式
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 多任务 / 多线程 程序设计
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。