并发
并发是指两个或多个同时独立进行的活动。在计算机系统中,并发指的是同一个系统中多个独立活动同时进行,而非依次进行。
并发在计算机系统中的表现:
一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理机上运行,但任一个时刻点上只有一个程序在处理机上运行。
虽然每个任务的部分操作在时间上重叠,但从微观上看,这些任务是交替执行的。
任务切换对使用者和应用软件自身都制造出并发的表象。
总之,并发是一种同时进行的概念,可以存在于计算机系统的不同层面。
补充同步和异步的概念:
同步和异步是两种不同的处理方式,它们在计算机系统中有着不同的含义和应用。
1、同步(Synchronous)
同步是指发送方发出数据后,等接收方发回响应以后才发下一个数据包的通讯方式。在同步通信中,发送方和接收方之间的数据传输是按照一定的时间顺序进行的。发送方会等待接收方对每个请求进行响应,然后才能继续发送下一个请求。因此,同步通信具有可靠性和顺序性的特点。
在同步通信中,由于发送方需要等待接收方的响应,因此数据的传输速度相对较慢。此外,如果接收方在处理请求时出现错误,发送方可能需要重新发送请求。因此,同步通信适用于对可靠性要求较高的场景,如文件传输、邮件通信等。
2、异步(Asynchronous)
异步是指发送方发出数据后,不等接收方发回响应,接着发送下个数据包的通讯方式。在异步通信中,发送方和接收方之间的数据传输是独立进行的,不需要按照时间顺序进行。发送方在发送请求后可以继续执行其他操作,而不需要等待接收方的响应。当接收方处理完请求后,会通知发送方并返回结果。
异步通信具有高效性和灵活性的特点。由于发送方不需要等待接收方的响应,因此数据的传输速度相对较快。此外,异步通信适用于对实时性要求较高的场景,如网络通信、事件驱动的系统等。在异步通信中,需要注意处理并发访问和数据竞争等问题。
总之,同步和异步是两种不同的处理方式,它们在计算机系统中有着不同的含义和应用。根据不同的需求和场景,可以选择适合的处理方式来提高系统的性能和可靠性。
对于异步处理方式我们有两种常用的方法:查询法(轮询法) 和 通知法(条件成熟再来进行操作),后面会再详细展开。
对于并发主要有两种方式的并发,分别是信号实现并发和多线程实现并发。
信号
信号概念
信号是软件层面的中断,更确切的说是信号的响应依赖于中断(硬件层面的中断)。
在Linux中,信号是一种进程间通信的方式,用于通知某个进程发生了某个事件。信号是一种异步的通知机制,当一个事件发生时,操作系统会向进程发送一个信号,中断该进程的正常控制流程。
进程可以定义信号的处理函数,当接收到信号时,执行相应的处理操作。如果没有定义处理函数,系统会默认处理信号,如终止进程或执行特定的系统操作。
在Linux系统中,有多种类型的信号,例如:
SIGINT:当用户按下中断键(如CTRL+C)时,会向当前进程发送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 给中断了,其实这就是在发送信号,Ctrl+C是信号 SIGINT (INT是Interrupt的缩写)的快捷方式。
SIGINT是终端中断符,其默认功能是终止一个进程。
接下来我们来验证该程序确实是接收到了一个 SIGINT 才被结束的:
可以看到此时我们发送的SIGINT信号就不再起作用了。
继续进行验证:
可以看见我们的信号处理函数起了作用,它输出了感叹号。
我们来试一下按住ctrl+c不放:
但其实静态的图片显示不了这个操作的效果,因为这样按住不放的话,原本十秒才能打印完的 *,瞬间就会全部打印完,这意味着:
信号会打断阻塞的系统调用!
这意味着我们之前写的所有程序可能都是错误的,因为我们都是在没用涉及信号的前提下写的,如果涉及了信号那么就可能都会出错。
一个最简单的例子,比如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的程序吗,就是每一秒钟打印一个*号,连续打印十个,然后遇到ctrl+c就被打断打印一个!:
上图的左侧是我们的main函数,另外一个就是我们的信号处理函数。
mask位图是我们的信号屏蔽字,它用来表示当前信号的状态,而pending位图用来记录当前这个进程接收到了哪些信号。
而mask位图的值一般情况下全部都为1,我们可以改的:
而pending位图在进程一开始时则全部为0:
在每次进程被中断(不是信号中断,而是其它类型的中断比如时间片用完的中断)时,进程会携带着自己的现场扎进内核态中,然后等待着下一次被调度时回到用户态重新恢复现场继续运行,从内核切换到用户态这个时间点进程会执行一个表达式:mask 按位与上 pending,通过上图的初识状态我们得知按位与的结果为 0,说明没有收到任何信号,所以就正常运行即可。
注意,信号并非是一发送进程就能够收到的,信号从收到到响应有一个不可避免的延迟。
思考问题:如何忽略掉一个信号的?标准信号为什么要丢失?
刚刚说了没有信号的情况,那么再来看有信号的情况。
假如说某个时刻有一信号(比如interrupt)到来,该信号反应到pending位图上某一个位上,那么该位就被置为1:
此时进程收到了信号但却还不知道(因为尚未在内核态中,进程还在用户态呢),什么时候才会知道?一定要等到有中断来对进程执行中断之后,进程带着自己的上下文现场扎进内核态中的就绪队列中,等待下一次被调度时又带着自己的现场从内核态回到用户态的时候进行了之前说的表达式:mask 按位与上 pending,此时可以发现按位与后的结果值中的 interrupt 信号的那一位变成 1 了,此时进程才真正知道了自己收到的信号是什么,即信号从收到到响应有一个不可避免的延迟。
即收到的时候就pending位图对应的信号位值变为 1 的时候,而响应则是在进程从内核态回到用户态时通过mask与pending进行了按位与之后才能进行响应。
收到这个信号之后,进程会将这个mask位置成0,pending位也置成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是进程ID,sig是要发送的信号。
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):这是键盘中断信号,通常由用户按下Ctrl+C产生。
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参数的值进行相应的修改。被屏蔽的信号不会被进程接收,而会被暂时挂起。当信号再次变得可接收时,这些信号将被传递给进程。
我们来写一个小例子来感受这个事情,这个程序是建立在之前的打印*号的程序基础上改的:
编译运行就有了如下效果:
从输出的第一行和第二行可以看到,我们先用ctrl+c发出了一个sigint信号,但是直到第二行开始的时候该信号才被响应,这符合我们的预期。第七行和第八行,我们连续发送多个sigint信号,可以看见该信号只会被响应一次,这是因为就算发送多次,因为该信号的mask位已经被置为0了,所以收到多少次其实都不会被响应,而最后mask位重新被置为1时也就只能响应到一次了(具体可以看上面的信号的响应过程那一章节)。
而sigprocmask的第三个参数是用来恢复信号集状态的,依然是改上面的程序:
运行效果相同,这里不再赘述。
上面说了关于mask位图的操作系统调用,对于pending位图也是有的,叫sigpending():
sigpending函数是Linux中的一个系统调用,用于查询当前进程的未决信号集合。未决信号集合是指那些已经发送给进程但尚未被处理的信号。这些信号可能因为被屏蔽而无法立即传递给进程。
sigpending函数的原型如下:
#include<signal.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函数的原型如下:
#include<signal.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是2,SIGALRM是14。传统信号的处理方式是异步的,即信号发送后立即触发信号处理程序执行,中断当前进程的正常执行流程。传统信号的处理程序是函数指针,可以由进程注册自定义的信号处理函数,用于在接收到信号时执行特定的操作。如果进程没有为某个信号注册处理函数,操作系统将采用默认的处理方式,例如终止进程、忽略信号或者产生核心转储文件。
实时信号(Real-Time Signals)是在POSIX.1b标准中引入的一种更高级的信号机制。实时信号的范围是从实时信号1(SIGRTMIN)到实时信号31(SIGRTMAX),共计64个信号。实时信号的处理方式可以是同步的或异步的,具体取决于进程设置的信号发送和接收机制。实时信号的处理程序是函数指针,可以由进程注册自定义的信号处理函数,用于在接收到信号时执行特定的操作。实时信号的一个重要特点是可以传递一个整数值作为附加数据,这使得实时信号在进程间通信中非常有用。实时信号相对于传统信号具有更高的灵活性和可靠性,且能够提供更细粒度的信号处理。传统信号在某些情况下可能会发生竞争条件或丢失信号的问题,而实时信号可以帮助解决这些问题。
实时信号并没有相关的可以使用的系统调用或者函数,实时信号的使用与标准信号的使用相比其实就是值的不同,用不同的值来区别系统调用使用的是标准信号还是实时信号,比如下面要使用实时信号 SIGRTMIN+6,那么我们可以宏定义一个值来代替它:
此时在程序中直接使用这个值就可以使用实时信号了: