ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Linux系统与系统编程(13)——信号

Linux系统与系统编程(13)——信号 前言欢迎观看Linux系列文章第13篇主要重点讲述了信号的产生、保存和处理而且浅谈了内核态与用户态、可重入函数、volatile关键字以及SIGCHLD信号。信号前置概念1.什么是信号信号是一种给进程发送的用来进行时间异步通知的机制。信号的产生相对于进程的运行是异步的。2.信号的处理当信号还没有产生时进程已经知道如何处理对应信号了。在设计时OS已经内置了进程对信号的识别和处理方式。产生信号后进程不是立即处理信号而是在合适的时候处理信号。3.信号源能给进程产生信号的叫做信号源信号源很多。4.信号与信号源两者没有任何关系不要混淆信号的产生产生信号的方式很多1️⃣键盘产生信号Ctrl c就是一个例子他会给目标进程发送信号相当一部分的信号处理动作就是终止当前进程。a.信号有哪些——发送指令 kill -l 可查看所有信号。kill -l它们以宏的方式存放在内核代码中。其中1-31是普通信号34到64是实时信号。其中 Ctrlc 发送的是信号2Ctrl \ 发送的是信号3Ctrlz 发送的是信号20.指令man 7 signal可以查看所有信号的默认处理动作man 7 signalaction动作有Core、Term、Cont和IgnCore和Term终止、Cont继续执行、Ign忽略。b.目标进程是什么进程有后台进程和前台进程。当我们通过命令./XXX启动的进程是前台进程./XXX 启动的进程是后台进程。其中自行启动的shell是前台进程。键盘信号只能发给前台进程也就是说前台进程可以从键盘上获取标准输入而后台进程无法从标准输入中获取内容。但是都可以像标准输出打印。前台进程只能有一个但是后台进程可以有多个。键盘只有一个所以只能给一个确定的进程输入数据。所以前台进程的本质就是要从键盘获取数据。相关指令查看后台进程jobs后台启动一个进程这里的1是任务号。查到一个后台进程。前台进程转后台先Ctrl z暂停前台进程。bg [任务号]指令bg即background后台启动指定进程。后台进程转前台fg [任务号]此时该进程可以获取键盘输入。2️⃣系统调用产生信号由于收到信号后进程不会立即处理所以必须要记录下来信号。收到的信号会记录在PCB中,以位图结构存储。struct task_struct { ··· unsigned int sigs; ··· }所以发送信号的本质是向目标进程写信号修改位图sigs。PCB是OS内部的数据结构对象所以发信号是修改内核数据的操作因此OS要提供发送信号的系统调用在底层让OS来完成发送信号。#include signal.h int kill(pid_t pid, int sig);也可以通过系统调用自己给自己发信号。#include signal.h int raise(int sig);调用 abort() 时进程会向自身发送 SIGABRT 信号系统默认处理 SIGABRT 的方式是终止进程并生成核心转储。可以捕捉执行自定义处理但是当处理函数返回后依旧会执行一次默认处理。#include signal.h int abort();3️⃣硬件异常产生信号如图当出现以上错误时程序会进行异常报错操作系统会根据程序犯错的类型发送信号终止进程。比如除0错误会发送信号8——SIGFPEFloating-point exception野指针错误会发送信号11——SIGSEGV段错误Invalid memory reference。操作系统是如何识别进程错误的呢这些错误本质是CPU在运行对应指令的时候出现的异常就是硬件异常然后产生一个异常向量。CPU 会根据异常向量号强制跳转到操作系统在启动时预设好的中断描述符表IDT中的对应入口。4️⃣软件产生信号以下介绍其中一种情况闹钟。#includeunistd.h unsigned int alarm(unsigned int seconds);计时器函数作用是告诉内核在seconds秒之后给当前进程发送SIGALRM信号该信号的默认处理动作是终止当前进程。传入0时为取消计时器。返回值是上一个计时器剩余时间。#includeunistd.h int pause(void);调用 pause() 的进程会进入阻塞状态休眠主动放弃 CPU直到有信号递送给该进程。示例代码#include iostream #include unistd.h #include signal.h #include sys/types.h #include unistd.h #include vector #include functional using namespace std; void Sched() { cout 我是进程调度! endl; } void MemManger() { cout 我是周期性内存管理正在检查内存问题! endl; } void Fflush() { cout 我是刷新程序正在定期刷新内存数据到磁盘! endl; } using func_t functionvoid(); vectorfunc_t funcs; void handlerSig(int sig) { cout---------------------------------------------------------------endl; for(auto f:funcs) { f(); } cout---------------------------------------------------------------endl; int n alarm(1); } int main() { funcs.push_back(Sched); funcs.push_back(MemManger); funcs.push_back(Fflush); signal(SIGALRM, handlerSig); alarm(1); while (true) { pause(); } return 0; }该程序可以模拟了如何操作系统根据信号来进行处理动作受信号的驱动做出反应。这里的alarm模拟的是操作系统的系统闹钟。操作系统内会同时存在很多的闹钟所以一样要对这些闹钟进行管理。闹钟结构体内存放了闹钟过期时间、闹钟方法等将闹钟过期时间以小堆方式把节点链接在一起。每当时间戳大于闹钟过期时间最小节点时出堆嗲用闹钟方法。信号的保存因为信号不能够立即处理而是要到合适的时候再处理所以要再开始处理前保存信号。·实际执行信号的处理动作称为信号递达。·信号从产生到递达之间的状态称为信号未决信号还存在PCB的位图中。·进程可以选择阻塞屏蔽某个信号被阻塞的信号将保持在未决状态直到进程解除对信号的阻塞才能执行递达。·阻塞不同于忽略信号阻塞就不会被递达忽略是递达的一种处理动作。信号的保存在PCB中以三张表的方式体现block和pending都是位图handler是处理动作的函数指针数组。阻塞表又叫做阻塞信号集或信号屏蔽字未决表又叫做未决信号集或等待信号集。两个信号集都已sigset_t类型存储。信号屏蔽字默认全0。信号集操作函数#include signal.h int sigemptyset(sigset_t *set);//清空信号集 int sigfillset(sigset_t *set);//填满信号集 int sigaddset(sigset_t *set, int signo);//添加一个信号 int sigdelset(sigset_t *set, int signo);//删除一个信号 int sigismember(const sigset_t *set, int signo);//测试指定信号是否在信号集中sigprocmask——获取或更改进程信号屏蔽字#includesignal.h int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);参数how有三个宏选项SIG_BLOCK、SIG_UNBLOCK和SIG_SETMASK。SIG_BLOCK添加当前信号屏蔽字的信号相当于mask mask | setSIG_UNBOLCK解除信号屏蔽子中阻塞的信号相当于maskmask~setSIG_SETMASK设置当前信号屏蔽字为set相当于mask set。set新信号集输入型参数。oldset旧信号集。非空时向该信号集返回修改前的信号屏蔽字用于事后恢复现场输出型参数。返回值成功返回0失败返回-1并设置错误码。sigpending——获取未决信号集#includesignal.h int sigpending(sigset_t *set);参数set未决信号集返回到set中输出型参数。返回值成功返回 0失败返回 -1 并设置错误码。示例代码#include iostream #include unistd.h #include signal.h #include sys/types.h #include unistd.h #include vector #include functional using namespace std; //打印pending表 void PrintPending(sigset_t pending) { cout 我是一个进程 getpid() pending:; for (int signo 31; signo 1; signo--) { if (sigismember(pending, signo)) { cout 1; } else { cout 0; } } cout endl; } //自定义捕捉SIGINT void handler(int sig) { //准备递达时首先将pending的对应信号位图置0. cout 递达 sig 信号! endl; sigset_t pending; int m sigpending(pending); PrintPending(pending); } int main() { signal(SIGINT, handler); sigset_t block, oblock; sigemptyset(block); sigemptyset(oblock); sigaddset(block, SIGINT); int n sigprocmask(SIG_SETMASK, block, oblock); (void)n; int cnt 0; while (true) { sigset_t pending; int m sigpending(pending); PrintPending(pending); if (cnt 10) { sigprocmask(SIG_SETMASK, oblock, nullptr); cout 解除对信号SIGINIT的屏蔽 endl; } sleep(1); cnt; } }1.解除屏蔽后立刻执行信号的处理。2.信号在准备递达前就已经把pending表对应的位置置0了。补充信号有Core信号和Term信号。Term信号一般终止。Core信号终止进程时在当前路径下形成一个文件进程异常退出时进程在内存中的核心数据从内存拷贝到磁盘形成一个文件核心转储。在云服务器上core dump是被默认禁止的因为云服务器一般是服务于生产的当出现bug时会保留下大量的core文件。gdb 可执行文件路径 core文件路径目的是进行调试该文件支持debug可以直接定位到错误行。ulimit -c 102400指令ulimit可以打开这个功能102400时自行设置的文件大小单位是KB。这个修改是临时的重启系统又会恢复默认禁止状态。退出码中可以通过core dump标志了解是否被core信号所杀。信号的处理收到信号之后进程要根据对应信号进行处理。处理方式1.默认处理动作。2.自定义信号处理动作。3.忽略处理。进程收到信号后会在合适的时候处理信号。自定义信号捕捉接口#includesignal.h typedef void(*sighandler_t)(int);//函数指针类型自定义处理函数 sighandler_t signal(int signum, sighandler_t handler);捕捉后可以运行自定义的函数来代替默认处理动作。为了防止恶意进程无法终止运行信号9杀死和信号19停止是无法被自定义捕捉和阻塞的。捕捉信号信号在合适的时候处理判断什么时候合适很重要。如图为处理信号时的步骤逻辑。可以简单理解为以下的简图。sigaction——自定义捕捉处理方法#includesignal.h int sigaction(int signum, const struct sigaction* act, struct sigaction* oldact);作用同signal但是功能更多而且不止能处理普通信号。参数signum信号编号actstruct sigaction* act 结构体如下struct sigaction { void (*sa_handler)(int); void (*sa_sigaction)(int, siginfo_t*, void *); sigset_t sa_mask; int sa_flags; void (*sa_restorer)(void); }sa_handler需要传入一个自定义的处理方法。sa_mask为额外信号屏蔽字当某个信号的处理函数被调用时内核会自动将当前信号加入到进程的信号屏蔽字中当处理函数返回之后自动恢复到原来的信号屏蔽字。如果除此之外还想要屏蔽其他一些信号那就用sa_mask字段说明。可以用信号集处理函数进行设置。演示代码#include iostream #include unistd.h #include signal.h #include cstdlib using namespace std; void handler(int signum) { cout hello signal: signum endl; } int main() { struct sigaction act, oact; act.sa_handler handler; sigaction(SIGINT, act, oact); while(true) { cout hello world endl; sleep(1); } return 0; }内核态与用户态硬件中断图为硬件中断的简单流程简图。虽然实现方法不相同但是逻辑和信号很类似信号其实就是用软件来模拟硬件中断。中断向量表本身是OS的一部分在OS启动时就直接加载到内存。在中断到来之前OS会一直暂停pause。OS不会轮询管理外设当外设有需求的时候才会来找OSOS再中断处理。操作系统就是基于中断进行工作的软件。时钟源的频率就是我们常说的CPU主频。软件中断也可以通过代码让软件在CPU触发中断。主动中断陷阱 / Trap也叫“访管指令”目的用户程序主动“敲门”请求操作系统帮忙比如读写文件、创建进程、申请内存。例子你在代码里调用 printf 或 System.out.println()底层会执行一条特殊指令如 INT 0x80 、system_call 或 SYSENTER触发中断CPU从用户态切换到内核态让操作系统去操作硬件屏幕。当我们进行系统调用时内核1.把系统调用号放入CPU的eax寄存器中。2.从CPU寄存器中获取系统调用号n。3.调用系统调用方法通过系统调用号n在系统调用表中索引调用方法操作系统不提供任何系统调用接口只提供系统调用号。我们使用的系统调用函数其实是已经通过glibc封装过一层了。#define SYS_ify(syscall_name) __NR_##syscall_name一个宏定义用于将系统调用的名称转换为对应的系统调用号。比如SYS_ify(open) 会被展开为 __NR_open而系统调用号不是 glibc 提供的是内核提供的内核提供系统调用入口函数 man 2 syscall或者直接提供汇编级别软中断命令 int or syscall并提供对应的头文件或者开发入口让上层语言的设计者使用系统调用号完成系统调用过程被动中断异常 / Exception目的程序运行时出了“岔子”CPU无法继续执行必须通知操作系统来处理。例子除零错误、访问空指针缺页中断、非法操作码。操作系统收到后通常会终止该进程或通过“缺页中断”把硬盘数据换进内存让程序继续运行。这里异常问题全部会被转换为CPU内部的软件中断然后走中断处理例程完成所有处理。理解内核与用户态·系统调用的过程也是在进程空间上进行的所有的函数调用都是在地址空间之间调转也就是从进程地址空间的代码区跳转到内核区。在代码区时就是用户态跳入内核区就是内核态。·在每台机器里操作系统只有一个每个进程都把操作系统映射到自己的PCB中所以内核页表只有一份而用户页表可以有很多份一个进程一份用户页表。所以无论我们如何进行进程调度都能找到唯一的一个操作系统。·因为用户和内核空间都在同一个4GB地址空间里如果用户可以任意访问虚拟地址空间的所有位置对于内核来说是很不安全的。所以OS为了保护自己不会选择相信任何人而是必须通过系统调用的方式来访问内核。·因此要分开内核态和用户态。内核态下以内核身份去通过系统调用方式访问OS用户态下以用户的身份访问内核空间以外的内容。·那么OS如何知道自己是处于内核态还是用户态呢CPU提供了一个CS寄存器cs0 就是内核态cs3 就是用户态。当我们使用指令集int 0x80或者syscall时就会修改 cs 为3让进程陷入内核。用户态下访问内核将被视为非法操作直接终止。*可重入函数可重入函数是指可以被多个执行流如多个线程、中断处理程序或信号处理器同时调用而不会产生数据错乱或逻辑错误的函数。它的核心特征是函数的执行过程不依赖任何“共享的、可修改的”外部状态。这只是一种特点可重入和不可重入函数要根据情况选择。硬性条件一个函数要做到可重入通常必须满足以下几点1️⃣仅使用局部变量所有数据都保存在栈空间或CPU寄存器中每个调用流独立。2️⃣不修改全局/静态变量不读写任何非const的全局变量或静态局部变量。3️⃣不调用非可重入函数如果内部调用了 malloc、printf 或标准I/O函数则通常不可重入因为这些函数内部使用了全局锁或静态缓冲区。4️⃣不持有静态锁不使用互斥锁Mutex或自旋锁Spinlock去保护临界区因为若持有锁时被中断再次调用会死锁。*volatilevolatile关键字修饰过的变量会保证其内存空间可见性其核心是禁止编译器对涉及该对象的操作进行某些优化。作用1️⃣禁止优化掉访问对volatile对象的每一次访问无论是读还是写都必须执行不能被优化掉。例如在微基准测试中常用 volatile 来防止编译器将一个看似无用的循环优化掉。2️⃣禁止重排序在一个单一线程内对volatile对象的访问不能被优化掉也不能与其它“可见的副作用”如I/O操作进行重排序。volatile 对象包括·类型带有 volatile 限定的对象。·volatile 对象的子对象subobject。·const volatile 对象的 mutable 子对象。*SIGCHLD信号SIGCHLD 是Unix/Linux系统中一个非常重要的信号它在子进程状态发生改变时由系统内核自动发送给其父进程。状态发生改变”具体指以下几种情况1.子进程终止正常退出或异常结束2.子进程收到SIGSTOP信号而暂停3.子进程从暂停状态恢复继续运行作用父进程需要关注子进程的状态主要为了回收子进程资源避免产生僵尸进程Zombie Process。默认行为是“忽略”SIGCHLD信号的默认处理动作是SIG_DFL。这意味着如果父进程不主动处理子进程终止后会变成僵尸进程占用内核资源。因此父进程需要自定义 SIGCHLD 信号的处理函数。在信号处理函数中调用 wait() 或 waitpid() 系统调用来回收子进程。如果你显式地将 SIGCHLD 处理方法设为 SIG_IGN即调用 signal(SIGCHLD, SIG_IGN)在绝大多数 Linux 系统上内核会开启优化自动回收子进程不会产生僵尸进程。这是显式设置忽略它的处理逻辑和默认忽略不同。❤~~本文完结感谢观看接下来更精彩欢迎来我博客做客~~❤
返回列表