当前位置: 首页 > news >正文

信号处理设计模式

问题

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

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 行,主线程等待子线程运行结束,在这期间,如果产生了信号,由于主线程没有屏蔽信号,子线程屏蔽了所有信号,所以只能在主线程中去处理信号,这样就可以确保子线程在处理任务的过程中不会被信号打断,解决了信号安全性的问题,但是引入了线程安全性的问题

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

这样子任务和信号实时性问题也得到了解决

信号设计模式小结

多数模式不需要处理信号,因此可直接屏蔽信号

需要处理信号的程序,重点考虑信号安全性问题

  • 同步处理方案,通过设计让任务代码和信号处理代码交替执行
    • 问题:信号处理是否及时?任务执行是否实时?
  • 异步处理方案,任务代码与信号处理代码位于不同执行流
    • 问题:将信号安全性问题转换为线程安全性问题
    • 因此,程序本身是否能做到线程安全?

相关文章:

  • 【Linux】修复 Linux 错误 - 权限被拒绝
  • Pycharm引用其他文件夹的py
  • docker学习(十九、network使用示例bridge)
  • 16-网络安全框架及模型-BiBa完整性模型
  • TypeError: control character ‘delimiter‘ cannot be a newline (`\r` or `\n`)
  • OpenGL FXAA抗锯齿算法(Qt)
  • css中的BFC
  • LLM应用的分块策略
  • 记矩阵基础概念
  • OSG读取和添加节点学习
  • 渗透测试 | php的webshell绕过方法总结
  • 《MySQL系列-InnoDB引擎01》MySQL体系结构和存储引擎
  • 黑豹程序员-vue3下载文件
  • 学习笔记 k8s常用kubectl命令
  • LabVIEW各版本安装指南
  • [LeetCode] Wiggle Sort
  • CSS盒模型深入
  • emacs初体验
  • java取消线程实例
  • KMP算法及优化
  • Python学习笔记 字符串拼接
  • seaborn 安装成功 + ImportError: DLL load failed: 找不到指定的模块 问题解决
  • Spring思维导图,让Spring不再难懂(mvc篇)
  • vue:响应原理
  • vue-cli在webpack的配置文件探究
  • 从0搭建SpringBoot的HelloWorld -- Java版本
  • 第13期 DApp 榜单 :来,吃我这波安利
  • 罗辑思维在全链路压测方面的实践和工作笔记
  • 如何实现 font-size 的响应式
  • 使用Envoy 作Sidecar Proxy的微服务模式-4.Prometheus的指标收集
  • 使用SAX解析XML
  • Nginx惊现漏洞 百万网站面临“拖库”风险
  • ​直流电和交流电有什么区别为什么这个时候又要变成直流电呢?交流转换到直流(整流器)直流变交流(逆变器)​
  • #vue3 实现前端下载excel文件模板功能
  • #我与Java虚拟机的故事#连载13:有这本书就够了
  • (0)Nginx 功能特性
  • (4)事件处理——(7)简单事件(Simple events)
  • (c语言)strcpy函数用法
  • (poj1.3.2)1791(构造法模拟)
  • (阿里云万网)-域名注册购买实名流程
  • (附源码)ssm捐赠救助系统 毕业设计 060945
  • (考研湖科大教书匠计算机网络)第一章概述-第五节1:计算机网络体系结构之分层思想和举例
  • (全注解开发)学习Spring-MVC的第三天
  • (四)docker:为mysql和java jar运行环境创建同一网络,容器互联
  • (转)AS3正则:元子符,元序列,标志,数量表达符
  • *_zh_CN.properties 国际化资源文件 struts 防乱码等
  • ..thread“main“ com.fasterxml.jackson.databind.JsonMappingException: Jackson version is too old 2.3.1
  • .bat批处理(六):替换字符串中匹配的子串
  • .net core 控制台应用程序读取配置文件app.config
  • .Net Core缓存组件(MemoryCache)源码解析
  • .NET Core实战项目之CMS 第一章 入门篇-开篇及总体规划
  • .NET Core中的去虚
  • .Net Memory Profiler的使用举例
  • .net 生成二级域名
  • .NET/C# 的字符串暂存池