尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

linux内核原理-共享内存,信号

linux内核原理-共享内存,信号 1.共享内存(1). 定义在Linux中进程间通信IPC有多种方式其中共享内存Shared Memory是一种非常高效的方式。它允许两个或多个进程共享同一块物理内存区域从而可以快速地传递大量数据。共享内存的基本概念共享内存允许两个或多个进程共享一个给定的存储区。因为数据不需要在不同的进程之间复制所以这是一种最快的一种IPC方式。通常是通过使用某种同步手段如信号量来同步对共享内存的访问。(2). 实例基于 POSIX 共享内存 环形缓冲区 三个 POSIX 无名信号量sem_init with pshared1支持多生产者 / 多消费者并发。common.h#ifndefCOMMON_H#defineCOMMON_H#includesemaphore.h#defineSHM_NAME/ringbuf_ipc_demo#defineBUF_SIZE8// 环形缓冲区大小structringbuf{sem_tmutex;// 互斥锁保护缓冲区索引sem_tempty;// 空槽位计数sem_tfull;// 已填充槽位计数intwrite_idx;// 生产者写入位置intread_idx;// 消费者读取位置intdata[BUF_SIZE];};#endifproducer.c#includecommon.h#includestdio.h#includestdlib.h#includestring.h#includeunistd.h#includeerrno.h#includefcntl.h#includesys/mman.h#includetime.hstaticstructringbuf*rbNULL;voidcleanup(void){if(rb)munmap(rb,sizeof(structringbuf));}intmain(intargc,char*argv[]){intcount(argc1)?atoi(argv[1]):20;intfd;intis_creator0;atexit(cleanup);/* 尝试以 O_EXCL 创建判断是否首次创建 */fdshm_open(SHM_NAME,O_CREAT|O_EXCL|O_RDWR,0666);if(fd0){is_creator1;printf([Producer] 首次创建共享内存并初始化信号量\n);}elseif(errnoEEXIST){fdshm_open(SHM_NAME,O_RDWR,0666);if(fd0){perror(shm_open);exit(1);}}else{perror(shm_open);exit(1);}if(ftruncate(fd,sizeof(structringbuf))0){perror(ftruncate);exit(1);}rbmmap(NULL,sizeof(structringbuf),PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);if(rbMAP_FAILED){perror(mmap);exit(1);}close(fd);/* 只有首次创建者初始化信号量和索引 */if(is_creator){sem_init(rb-mutex,1,1);// pshared1, value1sem_init(rb-empty,1,BUF_SIZE);// pshared1, valueBUF_SIZEsem_init(rb-full,1,0);// pshared1, value0rb-write_idx0;rb-read_idx0;memset(rb-data,0,sizeof(rb-data));}srand(time(NULL)^getpid());for(inti0;icount;i){intitemrand()%900100;// 100~999sem_wait(rb-empty);// P(empty) — 等待空槽sem_wait(rb-mutex);// P(mutex) — 进入临界区rb-data[rb-write_idx]item;printf([Producer PID:%d] 生产 %d → [%d]\n,getpid(),item,rb-write_idx);rb-write_idx(rb-write_idx1)%BUF_SIZE;sem_post(rb-mutex);// V(mutex) — 离开临界区sem_post(rb-full);// V(full) — 通知消费者usleep(200000);// 200ms模拟生产耗时}printf([Producer PID:%d] 完成退出\n,getpid());return0;}consumer.c#includecommon.h#includestdio.h#includestdlib.h#includeunistd.h#includefcntl.h#includesys/mman.hstaticstructringbuf*rbNULL;voidcleanup(void){if(rb)munmap(rb,sizeof(structringbuf));}intmain(intargc,char*argv[]){intcount(argc1)?atoi(argv[1]):20;atexit(cleanup);intfdshm_open(SHM_NAME,O_RDWR,0666);if(fd0){perror(shm_open);fprintf(stderr,提示请先启动 producer 创建共享内存\n);exit(1);}rbmmap(NULL,sizeof(structringbuf),PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);if(rbMAP_FAILED){perror(mmap);exit(1);}close(fd);for(inti0;icount;i){sem_wait(rb-full);// P(full) — 等待有数据sem_wait(rb-mutex);// P(mutex) — 进入临界区intitemrb-data[rb-read_idx];printf([Consumer PID:%d] 消费 %d ← [%d]\n,getpid(),item,rb-read_idx);rb-read_idx(rb-read_idx1)%BUF_SIZE;sem_post(rb-mutex);// V(mutex) — 离开临界区sem_post(rb-empty);// V(empty) — 通知生产者usleep(300000);// 300ms模拟消费耗时比生产慢可观察阻塞}printf([Consumer PID:%d] 完成退出\n,getpid());return0;}(3). 共享内存生命期shm_open 创建的共享内存对象生命周期独立于创建进程。即使创建该内存的所有进程都退出了这块共享内存依然存在其他进程仍然可以通过同名 shm_open 打开它。为什么—— Linux 的实现机制Linux 通过 tmpfs 实现 POSIX 共享内存# 你能直接在文件系统中看到它 $ ls-la/dev/shm/-rw-------1user user4096Aug1609:54my_shmshm_open(“my_shm”, O_CREAT | O_RDWR, 0666) 本质上是在 /dev/shm/ 下创建了一个特殊文件这个文件不随进程退出而自动删除系统重启后 /dev/shm/ 作为 tmpfs 会被清空对象消失如何销毁必须通过显式调用 shm_unlink()shm_unlink(my_shm);// 类似 rm /dev/shm/my_shm引用计数降为 0 且 unlink 后内存才真正释放如果进程崩溃或正常退出但忘记 shm_unlink对象会泄漏对比总结特性shm_open(POSIX)shmget(System V)生命周期随系统/文件系统需显式删除随系统需显式删除 (shmctlIPC_RMID)是否随进程退出自动销毁❌ 否❌ 否持久化位置/dev/shm/(tmpfs)内核 IPC 命名空间删除方式shm_unlink()shmctl(id, IPC_RMID, NULL)2.信号(1). 信号的阻塞与屏蔽在Linux系统中信号的阻塞和忽略是处理信号时的两种不同策略。信号阻塞意味着操作系统在信号被进程解除阻塞之前不会将信号传递出去。每个进程都有一个信号掩码这是一个“位图”用来描述哪些信号传送来将被阻塞。如果位图中某位标识为1表示该位对应的信号被暂时屏蔽即被阻塞。当信号处于阻塞状态时信号只是暂时被阻止传递不会影响进程的行为。进程可以使用各种系统调用来对其信号掩码添加和移除信号从而控制哪些信号被阻塞。与信号阻塞不同当进程忽略一个信号时信号仍然会被传递出去但进程将信号丢弃即不对其进行任何处理。这意味着信号已经到达了进程但进程选择不响应它。在Linux中有多种信号和信号处理方式。标准信号用于内核向进程通知事件而实时信号则可以在稍后被传递给某进程进程也会采取某措施来响应信号。在信号产生和到达期间信号处于pending等待状态。通过阻塞或忽略信号进程可以更加灵活地控制其行为和响应方式。总的来说Linux信号的阻塞和忽略是两种不同的信号处理策略它们允许进程根据具体需求来控制对信号的响应方式。需要注意的是即使信号被阻塞它们仍然会保留在系统中等待进程解除阻塞后进行处理。因此即使进程在一段时间内没有解除对某个信号的阻塞这个信号也不会丢失或被忽略。只有当进程显式地忽略了某个信号时该信号才会被丢弃不会触发任何处理程序。(2). 信号处理与多线程a. 通过kill向一个多线程的进程发信号信号处理会在哪个线程的上下文中执行在Linux中当一个信号被发送到多线程进程时信号处理函数如果已设置将在进程的一个线程上下文中执行。但是这个线程上下文并不是预先确定的也不是由信号的发送者选择的。确切地说操作系统会选择一个线程来执行信号处理函数。通常情况下操作系统会选择任意一个没有阻塞该信号的线程来执行信号处理函数。这意味着如果有多个线程没有阻塞该信号那么操作系统可能会选择其中任何一个线程。具体选择哪个线程通常是由操作系统的调度策略和当前的系统状态决定的。重要的是要理解信号处理函数是在进程上下文中执行的而不是在特定的线程上下文中。这意味着信号处理函数可以访问进程的全局变量和共享资源但是它不会获得任何特定线程的局部变量或栈状态。由于信号处理函数的执行是异步的它可能打断正在执行的线程包括持有重要锁或资源的线程。因此在编写信号处理函数时需要格外小心确保它不会破坏进程的状态或导致数据不一致。特别是在信号处理函数中访问或修改共享资源时必须使用适当的同步机制来避免竞态条件。总结来说通过kill向一个多线程进程发送信号时信号处理函数会在进程的一个线程上下文中执行但具体是哪个线程是不确定的由操作系统决定。因此在编写信号处理逻辑时需要确保它是线程安全的并且能够处理潜在的并发问题。b. 信号处理的进程与线程在Linux中每个进程有一个进程控制块PCB其中包含了关于该进程的信息包括其信号掩码和信号处理函数。当一个信号被发送给进程时操作系统会根据该进程的信号掩码来确定是否应该传递该信号。如果信号没有被阻塞那么操作系统会查找该进程的PCB中的信号处理函数并调用该函数来处理信号。对于多线程进程虽然每个线程有其自己的线程控制块TCB但是它们共享同一个进程控制块。这意味着它们共享相同的信号掩码和信号处理函数。因此当一个信号到达进程时无论是发送给整个进程还是特定的线程信号处理函数只会被执行一次而不是在每个线程中分别执行。此外线程可以拥有自己独立的信号掩码这意味着它们可以选择性地阻塞或解除阻塞某些信号。因此尽管信号在概念上是发送给整个进程的但实际上每个线程都可以根据自己的信号掩码来决定是否响应某个信号。(3). 信号处理程序编写原则在信号处理程序中有一些操作是安全的而有一些操作则是不安全的甚至可能导致未定义的行为或程序崩溃。下面是一些建议可以执行的操作a. 设置全局变量的值这通常是安全的只要这些变量不涉及到多线程间的同步问题b. 记录日志将信号到达的信息记录到日志文件中通常是安全的。c. 调用sigaction来更改信号处理函数可以在信号处理程序中更改信号的处理方式但这需要小心以避免产生递归信号。d. 调用raise或kill发送信号给其他进程或线程只要确保不会发送会导致递归的信号。e. 执行一些快速且简单的清理工作例如关闭文件描述符或释放某些资源。不可以执行的操作a. 调用大多数库函数许多库函数都不是异步信号安全的AS-unsafe。这意味着在信号处理程序中调用它们可能会导致未定义的行为。这包括大多数I/O函数如printf、fread、fwrite等、内存分配函数如malloc、free、以及许多其他常见的库函数。b. 修改全局数据结构如果这些数据结构可能被其他线程访问或修改那么在信号处理程序中修改它们可能会导致数据不一致或竞态条件。c. 长时间运行的操作信号处理程序的执行应该尽可能快以避免阻塞其他信号或导致其他问题。d. 调用不是异步信号安全的系统调用一些系统调用在信号处理程序中是不安全的。在编写信号处理代码时应该查阅相关文档以确定哪些系统调用是安全的。e. 创建或销毁线程在信号处理程序中创建或销毁线程通常是不安全的因为这涉及到复杂的线程管理和同步问题。f. 访问线程局部存储访问线程局部存储TLS中的数据在信号处理程序中通常是不可靠的因为信号处理函数可能在一个与原始线程不同的线程上下文中执行。总的来说信号处理程序的编写应该非常小心并且应该尽可能简单和快速。在信号处理程序中执行的操作应该被限制在最小的范围内并且应该避免任何可能导致未定义行为或程序崩溃的操作。最好的做法是将信号处理函数限制为设置标志或记录日志然后在主程序的安全上下文中处理这些标志或日志。以为何不建议在信号处理中执行malloc为例深入分析假设某线程正在执行malloc此时先是遭遇中断又在中断返回引发信号处理信号处理中也执行了malloc这就可能引发单线程下死锁malloc内部是通过一个全局锁来保持互斥的linux下信号处理范式Linux 下信号处理的核心矛盾在于信号是异步中断而大多数程序逻辑是同步顺序执行的。因此范式本质上就是如何安全地将异步事件桥接到同步主循环中。以下是 Linux 信号处理的几种经典范式从原始到现代基础范式极简 Handler仅设标志位原则信号处理函数signal handler中几乎什么都不做只设置一个 volatile sig_atomic_t 标志主循环轮询检测。#includesignal.h#includeunistd.h#includestdio.hstaticvolatilesig_atomic_t g_stop0;voidhandler(intsig){g_stop1;// 唯一操作写原子标志}intmain(){structsigactionsa{0};sa.sa_handlerhandler;sigemptyset(sa.sa_mask);sigaction(SIGINT,sa,NULL);sigaction(SIGTERM,sa,NULL);while(!g_stop){// 主循环正常业务逻辑pause();// 或 epoll_wait/select 等阻塞调用}printf(Graceful exit.\n);return0;}为什么只能写标志信号可能在任何时刻打断主程序包括 malloc、stdio 缓冲区操作期间只有 async-signal-safe 函数才能在 handler 中调用见 man 7 signal-safetyprintf、malloc、pthread_mutex_lock 等都不安全自管道技巧Self-Pipe Trick适用场景主循环基于 select/poll/epoll 的事件驱动架构需要将信号转换为文件描述符事件。原理在 handler 中向管道写端写入一个字节主循环监听管道读端的可读事件。#includesignal.h#includeunistd.h#includesys/epoll.hstaticintg_pipefd[2];voidhandler(intsig){intsaved_errnoerrno;// 写一个字节通知主循环忽略 EAGAINwrite(g_pipefd[1],sig,sizeof(sig));errnosaved_errno;}intmain(){pipe(g_pipefd);// 设为非阻塞防止 handler 中阻塞fcntl(g_pipefd[0],F_SETFL,O_NONBLOCK);fcntl(g_pipefd[1],F_SETFL,O_NONBLOCK);structsigactionsa{0};sa.sa_handlerhandler;sigaction(SIGINT,sa,NULL);intepfdepoll_create1(0);structepoll_eventev{.eventsEPOLLIN,.data.fdg_pipefd[0]};epoll_ctl(epfd,EPOLL_CTL_ADD,g_pipefd[0],ev);while(1){structepoll_eventevents[10];intnfdsepoll_wait(epfd,events,10,-1);for(inti0;infds;i){if(events[i].data.fdg_pipefd[0]){intsig;read(g_pipefd[0],sig,sizeof(sig));printf(Received signal %d in main loop\n,sig);// 这里可以安全地做任何事printf、malloc、释放资源...}}}}优势完全融入事件循环无需轮询信号处理逻辑在主线程上下文中执行无 reentrancy 问题signalfdLinux 特有现代首选原理将信号集转换为文件描述符通过 read 获取 signalfd_siginfo 结构。#define_GNU_SOURCE#includesys/signalfd.h#includesignal.h#includeunistd.h#includesys/epoll.hintmain(){sigset_t mask;sigemptyset(mask);sigaddset(mask,SIGINT);sigaddset(mask,SIGTERM);// 关键先阻塞信号再创建 signalfdsigprocmask(SIG_BLOCK,mask,NULL);intsfdsignalfd(-1,mask,SFD_NONBLOCK|SFD_CLOEXEC);intepfdepoll_create1(EPOLL_CLOEXEC);structepoll_eventev{.eventsEPOLLIN,.data.fdsfd};epoll_ctl(epfd,EPOLL_CTL_ADD,sfd,ev);while(1){structepoll_eventevents[10];epoll_wait(epfd,events,10,-1);for(inti0;i...;i){structsignalfd_siginfofdsi;read(sfd,fdsi,sizeof(fdsi));printf(Got signal %d from PID %u\n,fdsi.ssi_signo,fdsi.ssi_pid);}}}注意signalfd 的信号必须在进程级别被阻塞sigprocmask否则默认 handler 仍会执行。同步等待sigwaitinfo / sigtimedwait适用场景多线程程序中专门创建一个信号处理线程其他线程屏蔽信号。#includesignal.h#includepthread.h#includestdio.hvoid*signal_thread(void*arg){sigset_t mask;sigemptyset(mask);sigaddset(mask,SIGINT);while(1){siginfo_t info;intsigsigwaitinfo(mask,info);printf(Thread caught signal %d from PID %d\n,sig,info.si_pid);// 安全地执行清理、通知其他线程等}returnNULL;}intmain(){sigset_t mask;sigfillset(mask);pthread_sigmask(SIG_BLOCK,mask,NULL);// 主线程屏蔽所有信号pthread_t tid;pthread_create(tid,NULL,signal_thread,NULL);pthread_join(tid,NULL);}优势无需 handler完全同步逻辑清晰天然适合多线程架构信号只发给不屏蔽该信号的线程之一关键决策矩阵场景推荐范式原因简单 CLI 工具只需优雅退出范式一原子标志最简单无依赖基于 epoll/select 的单线程服务signalfd 或 自管道完美融入事件循环多线程服务器sigwaitinfo 专用线程避免信号与线程调度的不确定性需要兼容非 LinuxBSD/macOS自管道技巧signalfd是 Linux 特有铁律与常见陷阱Handler 中只调用 async-signal-safe 函数安全列表很短write, _exit, kill, sigaction, sem_post 等。printf, malloc, pthread_mutex_lock, exit 绝对不行。errno 保存与恢复Handler 中若调用系统调用必须保存/恢复 errno否则可能破坏主程序的错误检查。信号不会排队除实时信号标准信号如 SIGINT如果连续触发多次handler 可能只执行一次。如果需要计数用 signalfd 或 sigwaitinfo。SA_RESTART 的陷阱sigaction 设置 SA_RESTART 可让被中断的慢速系统调用自动重启但不适用于所有调用如 sleep, select, epoll_wait 在 Linux 上行为各异。多线程中信号的发送目标进程定向的信号如 kill(pid, SIGUSR1)只发给不屏蔽该信号的某个线程具体哪个不确定。如需定向用 pthread_kill。signalfd 和 sigwaitinfo 都要求在信号到达前完成屏蔽sigprocmask 或 pthread_sigmask。如果屏蔽设置有漏洞信号仍可能触发默认行为如终止进程。
返回列表