佛山智能模板建站,旅游网站模板库,成都优化网站推广,江西医疗网站建设并发
并发是指两个或多个同时独立进行的活动。在计算机系统中#xff0c;并发指的是同一个系统中多个独立活动同时进行#xff0c;而非依次进行。
并发在计算机系统中的表现#xff1a; 一个时间段中有几个程序都处于已启动运行到运行完毕之间#xff0c;且这几个程序都是…并发
并发是指两个或多个同时独立进行的活动。在计算机系统中并发指的是同一个系统中多个独立活动同时进行而非依次进行。
并发在计算机系统中的表现 一个时间段中有几个程序都处于已启动运行到运行完毕之间且这几个程序都是在同一个处理机上运行但任一个时刻点上只有一个程序在处理机上运行。 虽然每个任务的部分操作在时间上重叠但从微观上看这些任务是交替执行的。 任务切换对使用者和应用软件自身都制造出并发的表象。 总之并发是一种同时进行的概念可以存在于计算机系统的不同层面。
补充同步和异步的概念
同步和异步是两种不同的处理方式它们在计算机系统中有着不同的含义和应用。
1、同步Synchronous 同步是指发送方发出数据后等接收方发回响应以后才发下一个数据包的通讯方式。在同步通信中发送方和接收方之间的数据传输是按照一定的时间顺序进行的。发送方会等待接收方对每个请求进行响应然后才能继续发送下一个请求。因此同步通信具有可靠性和顺序性的特点。 在同步通信中由于发送方需要等待接收方的响应因此数据的传输速度相对较慢。此外如果接收方在处理请求时出现错误发送方可能需要重新发送请求。因此同步通信适用于对可靠性要求较高的场景如文件传输、邮件通信等。 2、异步Asynchronous 异步是指发送方发出数据后不等接收方发回响应接着发送下个数据包的通讯方式。在异步通信中发送方和接收方之间的数据传输是独立进行的不需要按照时间顺序进行。发送方在发送请求后可以继续执行其他操作而不需要等待接收方的响应。当接收方处理完请求后会通知发送方并返回结果。 异步通信具有高效性和灵活性的特点。由于发送方不需要等待接收方的响应因此数据的传输速度相对较快。此外异步通信适用于对实时性要求较高的场景如网络通信、事件驱动的系统等。在异步通信中需要注意处理并发访问和数据竞争等问题。 总之同步和异步是两种不同的处理方式它们在计算机系统中有着不同的含义和应用。根据不同的需求和场景可以选择适合的处理方式来提高系统的性能和可靠性。
对于异步处理方式我们有两种常用的方法查询法(轮询法) 和 通知法(条件成熟再来进行操作)后面会再详细展开。
对于并发主要有两种方式的并发分别是信号实现并发和多线程实现并发。
信号
信号概念
信号是软件层面的中断更确切的说是信号的响应依赖于中断硬件层面的中断。
在Linux中信号是一种进程间通信的方式用于通知某个进程发生了某个事件。信号是一种异步的通知机制当一个事件发生时操作系统会向进程发送一个信号中断该进程的正常控制流程。
进程可以定义信号的处理函数当接收到信号时执行相应的处理操作。如果没有定义处理函数系统会默认处理信号如终止进程或执行特定的系统操作。
在Linux系统中有多种类型的信号例如 SIGINT当用户按下中断键如CTRLC时会向当前进程发送SIGINT信号用于终止进程。 SIGTERM当使用kill命令向进程发送信号时默认发送的是SIGTERM信号用于请求进程正常终止。 SIGKILL当需要立即终止进程时可以向进程发送SIGKILL信号它会无条件地终止进程。 SIGSTOP当需要暂停进程时可以向进程发送SIGSTOP信号进程会停止执行。 SIGCONT当需要恢复暂停的进程时可以向进程发送SIGCONT信号进程会继续执行。 除了用户触发的信号外内核也可以因为内部事件而向进程发送信号通知进程发生了某个事件。例如当进程执行非法操作时内核会向进程发送SIGSEGV信号表示发生了段错误。 总之信号是Linux系统中一种重要的进程间通信方式用于通知进程发生了某个事件并且可以定义相应的处理函数来处理信号。
我们可以使用 kill -l 的命令来看一下信号 其中编号1 到 31的信号叫做标准信号从 34 到 64 称为实时信号这些信号名字中的RT表示real time实时的意思。
signal()系统调用 在Linux中signal系统调用是一种用于处理信号、注册信号的方式。它允许进程捕获、忽略或改变特定信号的默认处理行为。
signal系统调用的第一个参数 signum 是信号行为第二个参数是对于指定的 signum信号行为 所要采取的措施 handler然后返回值指的是这个信号之前的行为这个行为是由void (*sighandler_t) (int) 函数指定的。
其原型还可以写成下面这种形式下面这种形式才是我们最常写的形式其实就是把signal这个函数直接放到了 sighandler_t 的位置嘛这样写的好处是可以防止命名空间冲突
void (*signal(int sig, void (*func)(int)))(int);这个原型定义了一个函数指针该函数指针指向一个处理信号的函数。参数sig指定要处理的信号而func是一个指向处理函数的指针。如果func为SIG_IGN则忽略信号如果func为SIG_DFL则使用默认处理行为。
当进程接收到信号时会执行相应的处理函数。处理函数可以是一个自定义的函数也可以是系统定义的一些特殊值如SIG_IGN或SIG_DFL。
signal系统调用返回之前为指定信号设置的处理函数的指针。如果成功它返回之前的信号处理函数指针否则返回SIG_ERR并设置errno以表示错误原因。
信号机制是一种异步通知机制进程不必等待信号的到来。当进程接收到信号时会立即执行相应的处理函数。进程可以捕获、忽略或改变特定信号的处理行为以便在接收到信号时执行自定义的操作。
在Linux中有多种类型的信号例如SIGINT、SIGTERM、SIGKILL、SIGSTOP和SIGCONT等。这些信号有不同的含义和默认处理行为进程可以根据需要捕获和处理这些信号。
总之signal系统调用是Linux中用于处理信号的一种方式它允许进程自定义信号的处理函数以便在接收到信号时执行相应的操作。
来写个小例子 这个程序的作用是让其在终端输出设备上打印十个* 此时我们在它打印的过程中直接使用 Ctrl C 给中断了其实这就是在发送信号CtrlC是信号 SIGINT INT是Interrupt的缩写的快捷方式。
SIGINT是终端中断符其默认功能是终止一个进程。
接下来我们来验证该程序确实是接收到了一个 SIGINT 才被结束的
可以看到此时我们发送的SIGINT信号就不再起作用了。
继续进行验证 可以看见我们的信号处理函数起了作用它输出了感叹号。
我们来试一下按住ctrlc不放 但其实静态的图片显示不了这个操作的效果因为这样按住不放的话原本十秒才能打印完的 *瞬间就会全部打印完这意味着 信号会打断阻塞的系统调用! 这意味着我们之前写的所有程序可能都是错误的因为我们都是在没用涉及信号的前提下写的如果涉及了信号那么就可能都会出错。
一个最简单的例子比如read系统调用其出错时可能是代码逻辑导致的也可能是信号导致的如果是信号导致的比如最经典的 上图中的EINTR在读到任何内容之前就被信号打断了但此时读操作只是被阻塞了因为还没有读到内容若有信号到来就会打断从而返回了形式上的错误信息比如读到0字节真实含义确实是读失败但是实际上是还没开始读就被打断了这就是信号打断了阻塞的系统调用很明显这是一个假的错误因为并不是读不到而是还没开始读被打断了那么我们就可以做如下的改进
如果是假错误那么就再给其一次机会直到正常运行结束为止。
信号的不可靠性
标准信号其实有个特点就是一定会丢失但其不属于信号的不可靠性 而实时信号是不会丢失的。
这里所说的信号不可靠性指的是信号的行为不可靠。
其意思是一个信号在处理这个信号行为的同时又来了另外一个相同的信号那么由于这个执行现场是由内核来帮忙布置的如果是我们自己程序中写的调用函数的话那么就是OS来帮我们布置的所以我们函数a调用函数b调用函数c都一点问题没用但信号行为处理函数的执行线程则是内核来帮忙布置的就很有可能不会是在同一个位置那么第二次的执行现场就把第一次的给覆盖掉了这就是信号行为的不可靠性。
可重入函数
什么是可重入
就是第一次调用还没完成紧接着又发生了第二次调用这样就可能导致第一次调用发生一些不可预料的问题。
所有的系统调用都是可重入的一部分库函数也是可重入的如 memcpy。
举例 比如上面这个函数其有一个对应的 _r 版本这就是一个可重入的版本如果一个函数有其对应的 _r 可重入版本那么就说明该函数则一定不能用在信号处理函数当中既无 _r 不可重入的版本。
另外 _r 的函数版本是线程安全的 在Linux下asctime函数有一个asctime_r版本的原因是为了提供线程安全。 asctime函数是将结构化时间转换为字符串的函数它将一个struct tm结构体转换为一个格式化的字符串表示为Day Mon 2 Hour:Minute:Second YYYY\n的形式。 然而asctime函数有一个问题它是非线程安全的。这意味着在多线程环境下多个线程可能会同时调用asctime函数导致竞争条件和未定义的行为。 为了解决这个问题Linux提供了asctime_r函数。asctime_r函数是一个线程安全的版本它接受一个额外的参数struct tm结构体和一个字符数组将结构体转换为一个格式化的字符串并将结果存储在字符数组中。这样每个线程都可以使用自己的输入和输出缓冲区避免了竞争条件。 因此使用asctime_r函数可以确保在多线程环境下安全地使用asctime函数的功能。 注意区分二者的概念
可重入函数一定是线程安全的但线程安全的函数不一定是可重入的。
可重入函数的特点在于它们被多个线程调用时不会引用任何共享数据。而线程安全函数要解决的问题是多个线程调用函数时访问资源冲突。
如果一个函数中有全局变量那么这个函数既不是线程安全也不是可重入的。如果将对临界资源的访问加上锁则这个函数是线程安全的但如果这个重入函数若锁还未释放则会产生死锁因此是不可重入的。
总的来说可重入和线程安全是两个不同的概念它们之间有区别也有联系。
信号的响应过程
线程与进程的响应过程有一点区别这里先说进程
内核为每个进程维护了最少两个位图一个是Mask信号屏蔽字一个是pending理论上说二者都是 32 位的。
还记得我们之前写的star.c的程序吗就是每一秒钟打印一个*号连续打印十个然后遇到ctrlc就被打断打印一个! 上图的左侧是我们的main函数另外一个就是我们的信号处理函数。
mask位图是我们的信号屏蔽字它用来表示当前信号的状态而pending位图用来记录当前这个进程接收到了哪些信号。 而mask位图的值一般情况下全部都为1我们可以改的 而pending位图在进程一开始时则全部为0 在每次进程被中断不是信号中断而是其它类型的中断比如时间片用完的中断时进程会携带着自己的现场扎进内核态中然后等待着下一次被调度时回到用户态重新恢复现场继续运行从内核切换到用户态这个时间点进程会执行一个表达式mask 按位与上 pending通过上图的初识状态我们得知按位与的结果为 0说明没有收到任何信号所以就正常运行即可。
注意信号并非是一发送进程就能够收到的信号从收到到响应有一个不可避免的延迟。
思考问题如何忽略掉一个信号的标准信号为什么要丢失
刚刚说了没有信号的情况那么再来看有信号的情况。
假如说某个时刻有一信号比如interrupt到来该信号反应到pending位图上某一个位上那么该位就被置为1 此时进程收到了信号但却还不知道因为尚未在内核态中进程还在用户态呢什么时候才会知道一定要等到有中断来对进程执行中断之后进程带着自己的上下文现场扎进内核态中的就绪队列中等待下一次被调度时又带着自己的现场从内核态回到用户态的时候进行了之前说的表达式mask 按位与上 pending此时可以发现按位与后的结果值中的 interrupt 信号的那一位变成 1 了此时进程才真正知道了自己收到的信号是什么即信号从收到到响应有一个不可避免的延迟。
即收到的时候就pending位图对应的信号位值变为 1 的时候而响应则是在进程从内核态回到用户态时通过mask与pending进行了按位与之后才能进行响应。
收到这个信号之后进程会将这个mask位置成0pending位也置成0然后进程的上下文现场中肯定有一个地址值不然它怎么返回用户态对吧此时这个地址值会被改变成信号处理函数init_handler的地址而非原来的main函数地址 响应完信号处理函数之后还要返回内核态将地址值又改回原来的main函数继续完成剩下的任务对于我们的star.c程序来说就是打印完信号处理函数中的 之后还要返回继续打印main函数中的*号除此之外还要将mask刚刚被改为0的位置重新置为 1。 而pending是0是1不知道不用管此时进程从内核态又回到用户态时又会进行mask与pending的按位与操作此时就能知道刚才的信号已经被响应掉了然后进程正常返回到main函数继续完成后续操作。
这就是一个信号的响应过程。
而标准信号是有缺陷的因为在收到多个标准信号的时候此时先响应哪个其实是不知道的即标准信号的响应没有严格的顺序。
所以对于刚刚的思考问题我们应该有了答案如何忽略掉一个信号
其实就是把mask位图对应于pending信号位图上所对应的那个位改成0这样当有信号到来时哪怕是pending上该信号对应的位为变为了 1 那么进行按位与的时候该信号的按位与结果永远为 0 那我们自然就不会响应该信号啦。这也就意味着其实我们无法阻止信号的到来但是我们可以决定是否响应。
还有一个问题为什么标准信号会丢失
很简单因为这是位图位图意味着无法叠加比如有十万个相同的信号来临时因为从内核态返回用户态时会进行mask与pending的按位与嘛按位与后响应信号最后会将mask和pending置为0但此时又来一个相同信号的话这个pending又会被置为1但此时mask是为0的呀0和1与上结果为0所以这一次的结果就不会为1了也就意味着该次信号不会被响应也就造成了标准信号丢失的原因。
信号相关的常用函数kill()、raise()、alarm()、pause()、abort()、system()、sleep()
kill() 在Linux操作系统中kill是一种系统调用system call它是应用程序与操作系统内核交互的一种方式。通过kill系统调用应用程序可以向操作系统发送信号signal以请求操作系统终止一个进程、重新启动等。
在C语言中你可以使用kill系统调用来发送信号。它的函数原型如下
int kill(pid_t pid, int sig);其中pid是进程IDsig是要发送的信号。
kill系统调用的作用是向指定的进程发送指定的信号。如果进程无法被终止你可以使用SIGKILL信号编号为9强制终止进程。另外如果你不指定信号kill系统调用将默认发送SIGTERM信号编号为15请求进程终止。
需要注意的是只有具有足够权限的用户通常是root用户才能向其他用户的进程发送信号。在非root用户的情况下你只能向自己的进程发送信号。
raise() 在Linux中raise函数是用于发送一个信号给当前进程的。它允许当前进程主动终止自己或者请求操作系统的注意。
raise函数的函数原型通常为
int raise(int sig);其中sig是要发送的信号的标识符。
raise函数的作用是向当前进程发送一个信号。如果成功函数返回0如果失败返回-1。
在Linux中有许多不同的信号可以使用包括 SIGTERM信号编号15这是默认的终止信号。如果进程不捕获此信号那么它将被终止。 SIGINT信号编号2这是键盘中断信号通常由用户按下CtrlC产生。 SIGKILL信号编号9这是一个强制终止信号无论进程是否捕获它都将导致进程终止。然而进程可以捕获这个信号并执行一些清理操作然后再退出。 使用raise函数可以主动发送这些信号给自己这样就可以请求操作系统终止自己或者其他操作。例如你可以使用raise(SIGTERM)来请求操作系统终止当前进程。
需要注意的是不是所有的信号都可以被捕获和处理。例如SIGKILL就不能被捕获所以使用raise(SIGKILL)将立即终止当前进程而无法执行任何清理操作。
在编写程序时使用raise函数可以提供一种优雅地结束进程的方式例如在需要释放资源或者执行一些清理工作时。
alarm() Linux系统中的alarm系统调用是一种在指定时间后发送一个SIGALRM信号给当前进程的方式。它通常用于在进程完成某项任务后触发一个定时器以便在特定时间点执行其他操作。
alarm系统调用的原型如下
#include unistd.h int alarm(unsigned int seconds);这里的seconds参数指定了定时器的持续时间单位为秒。当指定的时间过去后系统会自动向当前进程发送一个SIGALRM信号。
alarm系统调用可以用于多种情况例如
超时处理当进程需要在特定时间内完成某项任务时可以使用alarm设置一个定时器。如果在定时器触发之前进程没有完成任务那么系统会发送一个SIGALRM信号给进程进程可以捕获该信号并进行相应的处理例如超时处理或重新尝试任务。
定期任务进程可以使用alarm系统调用设置一个定期触发的时间点。在每个触发时间点上系统会发送一个SIGALRM信号给进程进程可以捕获该信号并执行相应的操作。这种方式可以用于实现定期任务例如每隔一段时间检查文件更新、统计系统资源使用情况等。
唤醒休眠的进程当进程进入休眠状态时可以使用alarm系统调用设置一个唤醒时间。当休眠的进程到达指定的唤醒时间时系统会发送一个SIGALRM信号将其唤醒。
需要注意的是如果指定的时间设置为0则alarm系统调用会立即返回而不发送任何信号。此外如果进程没有捕获SIGALRM信号并对其做出处理那么默认情况下进程将被终止。因此在使用alarm系统调用时通常需要编写代码来捕获和处理SIGALRM信号。
我们可以写一个小例子 五秒钟之后该进程就会收到一个sigalarm信号然后打印上面的输出。
注意alarm系统调用是无法实现多任务计时器的效果的只能一个任务一个计数器比如刚刚的程序如果有三个alarm 实际上只会执行第三个alarm一秒钟就打印了alarm clock和秒数无关只会执行最下面的那个alarm。
这就意味着如果程序当中出现多个alarm时可能就会出错这里涉及到下一个系统调用的使用pause()
pause() Linux系统中的pause系统调用是一种让当前进程进入等待状态直到接收到某种信号为止的机制。它的函数原型如下
#include unistd.h int pause(void);pause系统调用会使当前进程进入等待状态直到收到一个信号为止。收到信号后进程会返回-1并设置errno为收到信号的编号。
pause系统调用的主要作用是让进程暂停执行等待接收信号。它通常用于实现进程间的同步、延时操作或等待某个条件满足等场景。
下面是一些使用pause系统调用的示例
进程间同步如果有多个进程需要按照一定的顺序执行可以使用pause系统调用来实现同步。例如一个进程在完成某项任务之前先调用pause等待另一个进程完成其他任务。当另一个进程完成任务后发送一个信号给第一个进程第一个进程收到信号后继续执行后续任务。
延时操作pause系统调用也可以用于实现简单的延时操作。通过调用pause函数进程会进入等待状态直到接收到信号。通过设置等待的时间可以实现一定时间间隔的延时效果。
等待某个条件满足当进程需要等待某个条件满足才能继续执行时可以使用pause系统调用。例如进程在执行某个操作之前需要等待文件就绪此时可以先调用pause等待文件就绪。当文件就绪后其他进程可以发送一个信号给该进程该进程收到信号后继续执行后续操作。
需要注意的是在使用pause系统调用时需要谨慎处理信号的处理方式。如果进程没有捕获信号并对其做出处理那么默认情况下进程将被终止。因此通常需要编写代码来捕获信号并进行相应的处理例如忽略信号、处理信号或者进行其他操作。
有了pause系统调用我们就可以解决刚刚程序中的CPU忙等问题啦 该程序的执行顺序为先执行alarm此时会有一个计时器在计时然后程序继续向下执行打印hahah字符串然后就会进入while(1)死循环中之前CPU会卡在这里一直忙等直到alarm时间到发送中断信号而终止程序但现在有了pause之后则会直接让进程进入等待阻塞的状态而不会让CPU一直跑这个循环直到alarm计时器到了之后发送sigalarm信号给该程序该程序就被终止了。pause所起到的作用也就是释放了CPU。
另外之前提过sleep函数有缺陷最好别用这是因为各个系统上的sleep函数的底层实现有可能不一样这就会导致意外的错误比如有些系统的sleep就是使用 alarm pause 进行封装的那假如我们的程序中也有alarm那刚刚才说过alarm是不支持多任务计时的这程序逻辑不就出错了吗所以考虑到移植问题我们最好别轻易使用sleep函数。
接下来我们来写几个例子。
例子1定时循环即让一个数疯狂的自增五秒钟复习一个之前学习过的time函数
在Linux中time函数是C语言中的一个标准库函数它用于获取当前的系统时间。
time函数的原型如下
time_t time(time_t *tloc);它接受一个指向 time_t 类型的指针作为参数并将当前的系统时间以从1970年1月1日00:00:00 UTC开始的秒数存储在该指针指向的位置上。如果参数tloc为NULL则time函数只返回当前时间的秒数而不保存到任何变量中。
补充这个函数是因为第一个例子我们先不用信号来处理 接下来用信号来处理一遍 可以看见使用信号来进行处理会更加的精确另外这一块内容可以从汇编的角度来看这个具体就参考一些其它的内容吧我就再补充一个关键字volatile的用法
在Linux C中volatile关键字用于告诉编译器不要对变量进行优化即使这个变量没有被修改。
在C语言中编译器通常会对代码进行优化以提升程序的运行效率。这种优化可能会导致某些变量的值被缓存或者寄存器中而不是直接从内存中读取。这对于一些循环或者条件语句中使用的变量很常见。
然而有些情况下我们希望编译器始终从内存中读取变量的值而不是使用缓存的值。这种情况下我们就可以使用volatile关键字来告诉编译器不要对这个变量进行优化。
volatile关键字告诉编译器这个变量可能会被意外地修改因此不能使用缓存的值。例如在多线程编程中一个线程修改了一个变量的值而另一个线程需要读取这个变量的值。如果编译器对这个变量进行了优化那么另一个线程可能无法获取到修改后的值。因此在这种情况下我们需要将这个变量声明为volatile。
需要注意的是volatile关键字只能保证编译器不会对变量进行优化但并不能保证变量的值不会被修改。因此在多线程编程中我们还需要使用其他的同步机制来确保变量的值不会被意外地修改。
abort() 在Linux中abort函数是一个系统调用用于终止当前进程并生成一个异常。它的原型如下
#include stdlib.h void abort(void);abort函数的作用是立即终止当前进程并产生一个异常信号。默认情况下该信号将被进程的信号处理程序捕获并导致程序异常终止。如果进程没有安装信号处理程序或者信号处理程序未能终止进程则进程可能会继续执行但这并不是一个推荐的做法。
当调用abort函数时会执行以下操作 1、生成一个异常信号通常是SIGABRT。 2、如果进程有未捕获的异常信号则立即终止进程。 3、如果进程有已捕获的异常信号处理程序则执行相应的处理程序。 4、如果异常信号处理程序未能终止进程则进程继续执行直到下一个异常或正常终止。 abort函数通常用于在程序中检测到无法处理的错误条件时终止进程。它是一种快速终止进程的方法但需要注意的是它不会执行任何清理操作如调用atexit函数注册的函数。因此在使用abort函数时需要确保程序能够正确地处理异常情况并尽可能进行必要的清理工作。
信号集
信号集相关的函数如下 上述函数族都涉及一个信号集类型sigset_t 。
sigemptyset函数及其相关函数是Linux下信号处理signal handling中的重要组成部分。它们用于对信号进行操作包括添加、移除和检查信号的处理器。这些函数主要用于处理系统发出的不同类型的信号如中断、异常等。
以下是对这些函数的详细解释
sigemptyset此函数用于清空信号集。它接受一个sigset_t类型的参数该参数是一个信号集所有信号都被移除。 例如
#include signal.h
sigset_t signal_set;
sigemptyset(signal_set);sigfillset此函数与sigemptyset相反它接受一个sigset_t类型的参数并将其所有信号设置为已添加到该集。 例如
#include signal.h
sigset_t signal_set;
sigfillset(signal_set);sigaddset此函数将指定的信号添加到给定的信号集中。它接受两个参数一个是sigset_t类型的信号集另一个是要添加的信号。 例如
#include signal.h
sigset_t signal_set;
sigemptyset(signal_set); // 清空信号集
sigaddset(signal_set, SIGINT); // 添加SIGINT信号到信号集中sigdelset此函数从给定的信号集中删除指定的信号。它接受两个参数一个是sigset_t类型的信号集另一个是要从集中删除的信号。 例如
#include signal.h
sigset_t signal_set;
sigfillset(signal_set); // 添加所有信号到信号集中
sigdelset(signal_set, SIGINT); // 从信号集中删除SIGINT信号sigismember此函数检查指定的信号是否存在于给定的信号集中。它接受两个参数一个是sigset_t类型的信号集另一个是要检查的信号。如果信号存在于集中则返回1否则返回0。 例如
#include signal.h
sigset_t signal_set;
sigfillset(signal_set); // 添加所有信号到信号集中
if (sigismember(signal_set, SIGINT)) { // 检查SIGINT信号是否在信号集中 // SIGINT信号在信号集中执行相应的代码
} else { // SIGINT信号不在信号集中执行相应的代码
}信号屏蔽字 / pending集的处理
还记得之前说过的mask位图吗我们可以使用 sigprocmask() 这个系统调用来进行人为的对mask位图的干涉。 sigprocmask是Linux系统中的一个系统调用用于在进程级别设置信号屏蔽字即决定哪些信号可以被进程接收和处理。它允许进程更改当前的内核的阻塞信号集以阻止屏蔽或允许取消屏蔽特定的信号传递到进程。
sigprocmask函数的原型如下
#include signal.h int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);sigprocmask函数接受三个参数
how指定如何修改内核阻塞信号集有以下几种方式 SIG_BLOCK将内核阻塞信号集与给定的自定义信号集合set进行逻辑或操作即将set中的信号添加到屏蔽字中。 SIG_UNBLOCK将内核阻塞信号集与给定的自定义信号集合set的补集进行逻辑与操作即从屏蔽字中移除set中的信号。 SIG_SETMASK将内核的阻塞信号集设置为给定的信号集合set。 set指向一个信号集合的指针表示需要修改的信号集合。
oldset指向一个信号集合的指针用于存储修改前的内核的阻塞信号集。如果不关心旧的内核的阻塞信号集可以传递NULL。
sigprocmask函数返回成功时返回0出错时返回-1并设置errno。在调用sigprocmask之后内核的阻塞信号集将根据how参数的值进行相应的修改。被屏蔽的信号不会被进程接收而会被暂时挂起。当信号再次变得可接收时这些信号将被传递给进程。
我们来写一个小例子来感受这个事情这个程序是建立在之前的打印*号的程序基础上改的 编译运行就有了如下效果 从输出的第一行和第二行可以看到我们先用ctrlc发出了一个sigint信号但是直到第二行开始的时候该信号才被响应这符合我们的预期。第七行和第八行我们连续发送多个sigint信号可以看见该信号只会被响应一次这是因为就算发送多次因为该信号的mask位已经被置为0了所以收到多少次其实都不会被响应而最后mask位重新被置为1时也就只能响应到一次了具体可以看上面的信号的响应过程那一章节。
而sigprocmask的第三个参数是用来恢复信号集状态的依然是改上面的程序 运行效果相同这里不再赘述。
上面说了关于mask位图的操作系统调用对于pending位图也是有的叫sigpending(): sigpending函数是Linux中的一个系统调用用于查询当前进程的未决信号集合。未决信号集合是指那些已经发送给进程但尚未被处理的信号。这些信号可能因为被屏蔽而无法立即传递给进程。
sigpending函数的原型如下
#includesignal.h
int sigpending(sigset_t *set);使用此函数需导入signal.h头文件。
该函数将进程的未决信号集合存储在参数set指向的位置。如果成功函数返回0否则返回-1并设置errno以指示错误。
在Linux中可以通过修改内核头文件signal.h中的函数实现自定义的信号处理。例如可以在自定义的信号处理函数中调用sigpending函数来获取当前进程的未决信号集合。
需要注意的是在多线程或多进程环境下由于多个线程或进程可能同时处理相同的信号因此使用sigpending函数时需要考虑线程安全和进程间同步的问题。同时在调用sigpending函数时也需要保证进程处于正确的状态例如不能在信号处理函数中调用sigpending函数因为这可能会导致竞争条件和不可预测的行为。
但是这个系统调用基本上用不到就了解一下即可。
更好的信号相关的系统调用sigsuspend()、sigaction()、setitimer()
sigsuspend()
这个系统调用可以用来帮我们写信号驱动程序是一个非常强大的系统调用。 sigsuspend函数是Linux中的系统调用用于挂起进程并等待特定的信号。它可以看作是sigprocmask函数和pause函数的组合。
sigsuspend函数的原型如下
#includesignal.h
int sigsuspend(const sigset_t *mask);使用此函数需导入signal.h头文件。
sigsuspend函数会暂停进程的执行直到收到指定的信号。它接受一个参数mask该参数是一个信号集用于指定需要屏蔽的信号。当收到指定的信号时进程会恢复执行。
与pause函数相比sigsuspend函数更加灵活。它可以指定需要屏蔽的信号而不是简单地等待任何信号。同时sigsuspend函数也解决了竞态条件的问题。当调用sigsuspend函数时进程的信号屏蔽字由mask参数指定可以通过指定mask来临时解除对某个信号的屏蔽然后挂起等待。当sigsuspend返回时进程的信号屏蔽字恢复为原来的值如果原来对该信号是屏蔽的从sigsuspend返回后仍然是屏蔽的。
需要注意的是在多线程或多进程环境下使用sigsuspend函数时需要考虑线程安全和进程间同步的问题。同时在调用sigsuspend函数时也需要保证进程处于正确的状态例如不能在信号处理函数中调用sigsuspend函数因为这可能会导致竞争条件和不可预测的行为。
接下来我们使用刚刚的程序来实现一个例子之前是阻塞信号处理函数到下一行开始的时候才执行现在改成阻塞住之后通过信号驱动的方式再执行信号处理函数进行打印否则就一直阻塞住其实这个使用我们之前说过的pause系统调用就能实现但是它有缺陷; 从第一行和第二行可以看到确实是达到了我们一开始想要的效果但有个问题就是我们连续发送sigint信号时信号是会打断阻塞的系统调用的也就是上图中的sleep系统调用都没有被阻塞一秒就直接打印*号了。
我们想要的效果是在每一行*打印期间就算收到了信号但是也不做出响应此时就得使用我们这里说的sigsuspend系统调用了 这个例子举的有点乱…看不懂就算了以后经验丰富起来了应该就懂了。
sigaction() sigaction函数是一个Linux系统调用用于设置信号处理函数以指定在进程收到信号时应采取的操作。
函数原型
#include signal.h
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);参数
signum指定要捕获的信号类型。
act指向一个sigaction结构体的指针该结构体包含新的信号处理方式。
oldact指向一个sigaction结构体的指针用于输出先前信号的处理方式如果不为NULL的话。
sigaction函数允许您同时检查或修改与指定信号相关联的处理动作。如果您希望使用自定义的信号处理函数可以将act参数设置为指向您自定义函数的结构体指针。如果oldact参数不为NULL则该参数用于输出先前信号的处理方式。
使用sigaction函数可以设置信号处理函数以捕获各种类型的信号例如中断如SIGINT、异常如SIGSEGV等。在信号处理函数中您可以执行特定的操作例如清理和终止程序、跳转到备用代码等。
请注意在使用sigaction函数时需要确保信号处理函数的可重入性并避免在信号处理函数中调用其他可能导致不可预测行为的系统调用。
对于这个系统调用的第二个结构体参数再进行一个详细的说明
在Linux的sigaction系统调用中第二个参数是一个指向struct sigaction结构体的指针。struct sigaction结构体是一个用于描述信号处理函数和相关属性的数据结构。
以下是 struct sigaction 结构体的主要成员 __sighandler_t sa_handler这是一个函数指针它指向在进程接收到信号时被调用的信号处理函数。这个函数通常接受一个整数参数即信号的值。当信号被捕获时内核将调用这个函数来处理信号。 sigset_t sa_mask这是一个信号集用于指定在信号处理函数执行期间需要被屏蔽的信号。在信号处理函数的执行期间这些信号将被忽略。这样可以防止在处理一个信号时被其他信号中断。 unsigned long sa_flags这是一组标志位用于指定信号处理函数的属性。这些标志位可以控制信号处理函数的执行方式例如是否在处理函数返回后自动重新注册处理函数等。 void (*sa_restorer)(void)这是一个可选的函数指针用于恢复信号处理函数之前的状态。在信号处理函数的执行期间内核将使用这个函数来恢复处理函数的先前状态以便在处理函数返回后可以继续执行。 使用sigaction系统调用时可以将struct sigaction结构体中的sa_handler成员设置为自定义的信号处理函数以指定在接收到特定信号时应该执行的操作。同时还可以根据需要设置sa_mask和sa_flags等成员来进一步控制信号处理的行为。这个结构体中的最后一个 sa_restorer 是已经不怎么用了的了解一下即可主要是用其它的几个。
我们可以使用这个系统调用来取代signal()系统调用这是一种更好的做法因为signal系统调用自身存在一定缺陷。
我们改写一个之前写的守护进程的那个例子来感受一下这个系统调用的用法 setitimer() setitimer系统调用是Linux系统中的一个功能用于设置一个间隔性定时器。它可以设置一个进程在特定时间间隔后接收一个信号signal通常用于限制程序或操作的执行时间、定期执行任务等。
setitimer系统调用的原型如下
#include sys/time.h int setitimer(int which, const struct itimerval *new_value, struct itimerval *old_value);setitimer函数接受三个参数
which指定设置哪种类型的定时器有以下三种类型 ITIMER_REAL以实时时间递减到期后发送SIGALRM信号。 ITIMER_VIRTUAL只有在进程执行时才递减到期后发送SIGVTALRM信号。 ITIMER_PROF在进程被执行和系统在代表该进程执行的时间都进行计数到期后发送SIGPROF信号。 new_value指向一个itimerval结构体的指针用于设置定时器的初始值和间隔时间。
old_value指向一个itimerval结构体的指针用于返回定时器的原有值。
itimerval结构体包含两个成员it_interval和it_value分别表示定时器的间隔时间和初始值。这两个成员都是以秒和微秒为单位进行计时的。
当定时器到期时系统会发送一个信号给进程。进程可以捕获这个信号并执行相应的操作。一般来说可以使用signal函数或sigaction函数来处理这些信号。
需要注意的是一个进程只能有一个setitimer定时器并且不支持在同一进程中同时使用多次以支持多个定时器。如果需要多个定时器可以通过编程实现多个定时器的逻辑。
getitimer系统调用是Linux系统中的一个功能用于获取一个进程的间隔性定时器的状态。它可以用于查询定时器的当前值、剩余时间以及定时器的类型。
getitimer系统调用的原型如下
#include sys/time.h int getitimer(int which, struct itimerval *value);getitimer函数接受两个参数
which指定要查询哪种类型的定时器和setitimer系统调用中的which参数相同。
value指向一个itimerval结构体的指针用于保存定时器的当前值和剩余时间。
当调用getitimer系统调用时会返回该定时器的当前值和剩余时间以及定时器的类型即ITIMER_REAL、ITIMER_VIRTUAL或ITIMER_PROF。如果定时器没有设置或者不存在则返回-1并设置errno为ENOENT。
使用getitimer系统调用可以方便地获取进程的定时器状态以便进行监控和管理。例如可以通过查询定时器的剩余时间来判断是否需要进行某些操作或者通过获取定时器的当前值来判断是否已经超过了某个时间阈值。
我们可以使用这个系统调用来取代 alarm 系统调用。
实时信号
Linux下的实时信号是一种改进的信号机制相对于标准信号而言它具有更高的可靠性和可控性。
标准信号是 Linux 系统中的一种异步通信方式用于通知进程发生了某种事件。标准信号的投递顺序未定义且信号不排队会丢失。当一个进程多次接收到相同的信号时它只会接收到一次信号而丢失的信号将无法被处理。此外标准信号中用于自定义的只有SIGUSER1和SIGUSER2而实时信号的信号范围有所扩大可供应用程序自定义的目的。
实时信号是一种改进的信号机制它解决了标准信号的缺陷。实时信号采取的是队列化管理如果某一个信号多次发送给一个进程该进程会多次收到这个信号。此外实时信号还具备以下优势
传递顺序有保障不同的实时信号之间有传递顺序的保障信号的编号越小优先级越高。
可伴随数据实时信号可以伴随数据供接收进程的信号处理器使用。
综上所述实时信号和标准信号之间的区别在于实时信号解决了标准信号的缺陷具备更高的可靠性和可控性同时具备传递顺序保障和伴随数据等优势。 标准信号是Unix系统早期定义的信号类型被称为传统信号或标准信号。传统信号的范围是1到31用整数方式表示例如SIGINT是2SIGALRM是14。传统信号的处理方式是异步的即信号发送后立即触发信号处理程序执行中断当前进程的正常执行流程。传统信号的处理程序是函数指针可以由进程注册自定义的信号处理函数用于在接收到信号时执行特定的操作。如果进程没有为某个信号注册处理函数操作系统将采用默认的处理方式例如终止进程、忽略信号或者产生核心转储文件。 实时信号Real-Time Signals是在POSIX.1b标准中引入的一种更高级的信号机制。实时信号的范围是从实时信号1SIGRTMIN到实时信号31SIGRTMAX共计64个信号。实时信号的处理方式可以是同步的或异步的具体取决于进程设置的信号发送和接收机制。实时信号的处理程序是函数指针可以由进程注册自定义的信号处理函数用于在接收到信号时执行特定的操作。实时信号的一个重要特点是可以传递一个整数值作为附加数据这使得实时信号在进程间通信中非常有用。实时信号相对于传统信号具有更高的灵活性和可靠性且能够提供更细粒度的信号处理。传统信号在某些情况下可能会发生竞争条件或丢失信号的问题而实时信号可以帮助解决这些问题。 实时信号并没有相关的可以使用的系统调用或者函数实时信号的使用与标准信号的使用相比其实就是值的不同用不同的值来区别系统调用使用的是标准信号还是实时信号比如下面要使用实时信号 SIGRTMIN6那么我们可以宏定义一个值来代替它 此时在程序中直接使用这个值就可以使用实时信号了