信号处理设计模式
问题
如何编写信号安全的应用程序?
Linux 应用程序安全性讨论
场景一:不需要处理信号
- 应用程序实现单一功能,不需要关注信号
- 如:数据处理程序,文件加密程序,科学计算程序
场景二:需要处理信号
- 应用程序长时间运行,需要关注信号,并即使处理
- 如:服务端程序,上位机程序
场景一:不需要信号处理 (单一功能应用程序)
main0.c
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>
#include <errno.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, "Delphi", do_job},{2, "D.T.Software", do_job},{3, "Tang", 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;
}
由于我们不关心信号,所以可以在程序执行开始处将所有信号屏蔽掉
这里的 mask_all_signal() 函数用于屏蔽所有的信号
程序运行结果如下图所示:
我们键入 Ctrl + C,发送 SIGINT 信号给当前进程,但是正常进程还是正常的在运行,说明屏蔽信号已经生效了
场景二:需要处理信号 (长时间运行的应用)
同步方案
- 通过标记同步处理信号,整个应用中只有一个执行流
异步方案
- 专用任务处理,应用中存在多个执行流 (多线程应用)
- 设置专用信号处理任务,其它任务忽略信号,专注功能实现
同步解决方案 (单任务)
信号处理逻辑与程序逻辑位于同一个上下文
- 即:信号处理函数和主函数不存在资源竞争关系
方案设计一
- 将任务分解为子任务(每个任务可对应一个函数)
- 信号递达时,信号处理函数中仅标记递达状态
- 子任务处理结束后,真正执行信号处理
同步方案示例一
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;
}
该程序用于给指定进程发送 n 个指定信号,并且发送时会携带数据,数据为发送信号的次序
main1.c
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>
#include <errno.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, "Delphi", do_job},{2, "D.T.Software", do_job},{3, "Tang", 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_count = 0;static void signal_handler(int sig, siginfo_t* info,void* ucontext)
{g_sig_arr[sig] = *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 signalswitch(info->si_signo){case SIGINT:// call process function for SIGINTbreak;case 40:// call process function for 40break;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_RESTART | SA_SIGINFO;for(i=1; i<=64; i++){sigaddset(&act.sa_mask, i);}for(i=1; i<=64; i++){sigaction(i, &act, NULL);}
}int main(int argc, char* argv[])
{int i = 0;printf("current pid(%d) ...\n", getpid());app_init();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;
}
signal_handler() 信号处理函数只是做了一个标记,标记某个信号触发了,并没有执行真正的信号处理
真正的信号处理是 process_signal() 函数,在每个子任务执行完毕之后会去执行这个函数,这个函数根据标记是否触发来判断是否执行信号处理任务,这样做的好处是确保任务执行的过程中不会去执行信号处理,确保了信号处理时的安全性
但是存在的问题是信号处理变得不及时,信号处理的时间必须是某个子任务执行结束才会去处理,并且如果发送了多个可靠信号,这里也只会处理一次,使得可靠信号变为了不可靠信号
程序运行结果如下图所示:
发送方发送了 10 次信号值为 40 的可靠信号,但接收方只处理了最后一次接收到的信号
存在的问题
由于给每个信号唯一的标记位置,因此,所有信号转变为不可靠信号;并且仅保留最近递达的信号信息
使用链表可以解决这里的可靠信号变为不可靠信号的问题,但是由于链表可能使用 malloc() 来分配结点内存空间,所以这样会使得信号处理函数变得不安全
方案设计二
将任务分解为子任务 (每个任务可对应一个函数)
- 创建信号文件描述符,并阻塞所有信号 (可靠信号递达前位于内核队列中)
- 子任务处理结束后,通过 select 机制判断是否有信号需要处理
- true => 处理信号 false => 等待超时
关键系统函数
#include <sys/select.h>
#include <sys/signalfd.h>
int signalfd(int fd, const sigset_t* mask, int flag);
int select(int nfds, fd_set* readfds, fd_set* writefds, fd_set* exceptfds, struct timeval* timeout);
使用 signalfd() 处理信号
先屏蔽所有信号 (无法递达进程),之后为屏蔽信号创建文件描述符;当时机成熟,通过 read() 系统调用读取未决信号 (主动接收信号)
使用 select() 监听文件描述符
使用 select() 处理信号
main2.c
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/signalfd.h>
#include <unistd.h>
#include <signal.h>
#include <errno.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, "Delphi", do_job},{2, "D.T.Software", do_job},{3, "Tang", 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, NULL);return signalfd(-1, &set, 0);
}static void signal_handler(int sig, siginfo_t* info,void* ucontext)
{g_sig_arr[sig] = *info;g_sig_arr[0].si_signo++;
}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 signalswitch(info->ssi_signo){case SIGINT:// call process function for SIGINTbreak;case 40:// call process function for 40break;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 value// max = (max < fd) ? fd : max;}FD_CLR(i, reads);ret = max;}}return ret;
}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()
{struct sigaction act = {0};sigset_t set = {0};int i = 0;act.sa_flags = SA_RESTART | SA_SIGINFO;for(i=1; i<=64; i++){sigaddset(&act.sa_mask, i);}for(i=1; i<=64; i++){sigaction(i, &act, NULL);}g_sig_fd = mask_all_signal();
}int main(int argc, char* argv[])
{int i = 0;printf("current pid(%d) ...\n", getpid());app_init();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;
}
app_init() 函数中会屏蔽所有信号,并使用 signalfd() 函数创建信号文件描述符,这里的信号文件描述符包含了所有的信号
屏蔽所有的信号以后,process_signal() 函数中通过调用 select() 函数来监听之前创建的信号文件描述符,来观察是否有信号触发,有触发则去处理
由于我们屏蔽掉了所有信号,所以不用担心信号触发来打断当前程序的执行流,我们在子任务执行结束后,主动去查看是否有信号触发,确保了信号处理的安全性,并且之前可靠信号变为不可靠信号的问题也解决掉了,每来一个可靠信号都会去处理
程序运行结果如下图所示:
可以看出发送的 10 次值为 40 的信号每次都去处理了
存在的问题
由于使用了 select 机制,即便没有信号需要处理,也需要等待 select 超时,任务实时性受到影响
异步解决方案 (多任务)
使用独立任务处理信号,程序逻辑在其他任务中执行
即:通过多线程分离信号处理与程序逻辑
- 主线程:专用于信号处理
- 其他线程:完成程序功能
多线程信号处理
信号的发送目标是进程,而不是某个特定的线程
发送给进程的信号仅递送给一个进程
内核从不会阻塞目标信号的线程中随机选择
每个线程拥有独立的信号屏蔽掩码
异步解决方案 (多任务)
主线程:对目标信号设置信号处理的方式
- 当信号递达进程时,只可能时主线程进行信号处理
其他线程:首先屏蔽所有可能的信号,之后执行任务代码
- 无法接收到信号,不具备信号处理能力
进程与线程
进程:应用程序的一次加载执行 (系统执行资源分配的基本单位)
线程:进程中的程序执行流
- 一个进程中可以存在多个线程 (至少存在一个线程)
- 每个线程执行不同的任务 (多个线程可并行执行)
- 同一个进程中的多个线程共享进程的系统资源
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_t thread, void** retval);
- 等待目标线程执行结束
多线程编程示例
异步方案示例 -- 主线程
异步方案示例 -- 任务线程
main3.c
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <pthread.h>
#include <unistd.h>
#include <signal.h>
#include <errno.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, "Delphi", do_job},{2, "D.T.Software", do_job},{3, "Tang", 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 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 signalswitch(info->si_signo){case SIGINT:// call process function for SIGINTbreak;case 40:// call process function for 40break;default:break;}
}static void signal_handler(int sig, siginfo_t* info,void* ucontext)
{do_sig_process(info);
}static void app_init()
{struct sigaction act = {0};sigset_t set = {0};int i = 0;act.sa_sigaction = signal_handler;act.sa_flags = SA_RESTART | SA_SIGINFO;for(i=1; i<=64; i++){sigaddset(&act.sa_mask, i);}for(i=1; i<=64; i++){sigaction(i, &act, NULL);}
}static void mask_all_signal()
{sigset_t set = {0};sigfillset(&set);sigprocmask(SIG_SETMASK, &set, NULL);
}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++;}
}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("app end\n");return 0;
}
第 116 行,我们创建了子线程,子线程先屏蔽所有信号,然后去处理子任务
第 118 行,主线程等待子线程运行结束,在这期间,如果产生了信号,由于主线程没有屏蔽信号,子线程屏蔽了所有信号,所以只能在主线程中去处理信号,这样就可以确保子线程在处理任务的过程中不会被信号打断,解决了信号安全性的问题,但是引入了线程安全性的问题
程序运行结果如下图所示:
这样子任务和信号实时性问题也得到了解决
信号设计模式小结
多数模式不需要处理信号,因此可直接屏蔽信号
需要处理信号的程序,重点考虑信号安全性问题
- 同步处理方案,通过设计让任务代码和信号处理代码交替执行
- 问题:信号处理是否及时?任务执行是否实时?
- 异步处理方案,任务代码与信号处理代码位于不同执行流
- 问题:将信号安全性问题转换为线程安全性问题
- 因此,程序本身是否能做到线程安全?