目录
一、共享内存通信机制中的临界资源访问与同步控制
1、概念
2、生活角度理解信号机制
3、信号量的操作
二、信号
1、生活角度的信号
2、技术应用角度的信号
3、操作系统角度的信号
信号如何产生
理解组合键变为信号
理解信号如何被进程保存
时钟中断(了解)
4、信号定义、kill -l查看信号列表
5、信号处理的常见方式
一、共享内存通信机制中的临界资源访问与同步控制
1、概念
基于对共享内存通信机制的理解,我们认识到要实现不同进程间的数据共享,关键是确保这些进程能够访问到同一份物理内存资源。在此之前,我们探讨的所有进程间通信手段,其核心目标都是确保进程间可以共享和访问同一份数据资源。
-
当这份资源被多个进程同时访问时,我们称其为“临界资源”,因为它的访问必须受到严格的控制,以避免竞争条件和数据不一致性问题。
-
在每个进程中,涉及访问临界资源的那段代码区域被称为“临界区”。临界区内的操作要求互斥执行,即在任意时刻,至多只有一个进程能够进入并执行这段代码。
-
为了保证进程间对临界资源的访问满足互斥性,我们需要采用适当的同步机制,确保在任何时候只有一个进程可以进入临界区进行操作。
#include <iostream>// 假设这是我们的临界资源:一个全局变量,它模拟一个打印机
int shared_resource = 0; // 全局计数器,一次只能有一个线程访问// 这是线程函数,其中访问和修改临界资源的代码段就是临界区
void thread_function(const char* name) {while (true) {// 简化版的“临界区入口”,在这里我们假设某种机制能够确保一次只有一个线程进入// (注:真实环境中,这将由互斥锁等同步原语来实现)// 开始临界区std::cout << name << " entered the critical section.\n";// 访问和修改临界资源shared_resource++; // 假设这是打印一页纸的操作std::cout << "Page printed by " << name << ". Total pages: " << shared_resource << "\n";// 简化的“临界区出口”std::cout << name << " left the critical section.\n";// 模拟其他非临界区的工作// ...}
}int main() {// 假设有两个线程,分别模拟两个任务std::thread t1(thread_function, "Thread A");std::thread t2(thread_function, "Thread B");// 等待线程结束(在此处省略了join操作)return 0;
}
在这个例子中:
-
临界资源:
shared_resource
是一个全局变量,代表着只能由一个线程同时访问和修改的资源,比如打印机的物理打印操作。 -
临界区: 在
thread_function
函数中,打印输出"entered the critical section."
到"left the critical section."
之间的代码段就是临界区。在这个区域内,线程会访问和修改临界资源shared_resource
。
原子性则是对这类操作性质的一种描述,它强调的是一个操作要么全部完成,要么完全不执行,不存在中间状态。
- 在多执行流环境下,如果不加保护地同时访问临界资源,可能会导致数据竞争和不一致的情况发生。
- 而在非临界区的代码执行,则不受这种互斥性的约束,多个执行流可以并行执行且彼此互不影响。
- 简言之,正是由于对临界资源的不当访问,才引发了进程间通信中的同步问题,而通过实现互斥和保证原子性,我们可以有效地避免这些问题的发生。
2、生活角度理解信号机制
在生活中,我们可以把信号机制比作去电影院观影时的座位预订系统。当你打算看电影时,并非径直走进影院就能随意落座。首先需要经历购票环节,这张电影票就像是一份承诺,代表着你有权在特定时间段内使用一个座位,即便此刻你并未实际坐在那里,座位已经被视为你的预定资源。
对应到计算机,这一现象与进程试图进入临界区(即仅允许单个进程访问的资源区域)的过程十分相似。就如同电影院不会放任观众自行进入影厅抢占座位,操作系统也不会让进程未经许可就擅自访问临界资源。
在此情境下,进程必须先通过某种方式取得“许可”——这个许可就是所谓的“信号量”。信号量的核心作用犹如现实中的电影票,当进程获取到一个有效的信号量时,这就意味着操作系统已经为该进程安排好对临界资源的使用权。只有持有信号量的进程才能安全地进入并操作临界资源,从而避免多个进程同时访问导致的数据冲突或不一致问题。
3、信号量的操作
信号量的操作基本可概括为以下几个步骤:
- 申请信号量:这是一个尝试进入临界区的过程。进程通过减少信号量的计数(如果信号量大于零)来标记对资源的请求。这等同于购买电影票,确认座位的预订。
- 访问临界资源:一旦信号量成功申请,进程就可以进入临界区执行其操作。这相当于拿着电影票进入放映厅,坐在自己预订的座位上。
- 释放信号量:进程完成其在临界区的操作后,会释放信号量,通常是通过增加信号量的计数来实现。这表明资源现在可以被其他进程预订,就如同观众观看完电影离开座位,座位可供下一位观众使用。
为了确保对临界资源的安全访问,我们需要使用信号量计数器来实现互斥和同步。申请信号量会导致计数器递减,释放信号量则会使计数器递增。P操作(申请信号量)和V操作(释放信号量)必须是原子的,以防止中间状态问题。
二、信号
1、生活角度的信号
在生活中,接收快递的过程与操作系统中处理信号的概念有异曲同工之处。想象一下,你在网购平台上购买了多个商品,就如同在系统中启动了多项任务,等待各自的“信号”——快递送达的通知。当快递员抵达楼下并向你发送送达通知时,这就相当于操作系统接收到一个“信号”。
在这个场景中,你可以做如下类比:
-
默认动作:当接收到“快递已到”的信号时,如果不做特别处理,默认的动作就像是立刻停下手头的事(如玩游戏),然后下楼签收包裹,之后拆开包裹并开始使用商品(如同进程接收到信号后执行默认操作,如进程终止或继续执行)。
-
自定义动作:如果对某个特殊商品(比如一盒零食)设置了特殊的“信号处理函数”,那么在接到通知后,你可能会选择暂时不立即取件,而是等一会儿,准备把它当作礼物送给女朋友,这就类似编程中设置自定义信号处理器函数,以便在接收到特定信号时执行定制化操作。
-
忽略快递:在收到快递到达的消息时,若你选择继续沉浸在游戏中而不去理会,这就类似于进程选择忽略某个信号,暂时不对该事件做出反应。
从接收到快递送达通知到实际下楼取件的过程,构成了一个类似操作系统中的“信号处理延时窗口”。在这个窗口期内,尽管你知道快递已到,但并不急于立即采取行动,而是根据自己的优先级和安排来调整处理时机。
整个过程体现了生活中的“异步处理”理念,因为快递送达的具体时间点具有不确定性,正如操作系统中的信号也可能随时到来一样。而如何应对这些“信号”,则完全取决于个人或进程事先设定的策略及实时情境判断。
2、技术应用角度的信号
[@localhost code_test]$ cat sig.c
#include <stdio.h>
int main()
{while (1) {printf("I am a process, I am waiting signal!\n");sleep(1);}
}
[@localhost code_test]$ . / sig
I am a process, I am waiting signal!
I am a process, I am waiting signal!
I am a process, I am waiting signal!
^ C
[@localhost code_test]$
在技术应用场景中,假设你网购下单购买了多件商品,正处于热切等待各商品快递送达的状态。就像上述C语言代码中的进程在持续循环等待信号一样,你清楚自己应该如何应对每一份即将到来的包裹。在实际生活中,快递员抵达你所在的楼宇时,你会通过某种方式得知快递已到达的通知。
- 在计算机程序中,
sig.c
中的进程不断循环并输出“我正在等待信号!”,这象征着你在网购后持续关注物流动态的行为。sleep(1)
则代表你间隔一定时间查询一次订单状态。 - 当按下Ctrl-C时,就如同快递员送达包裹并触发了一次“中断通知”(硬件中断),操作系统接收到这个中断后将其解释为一个信号(如SIGINT),并将其发送给正在运行的前台进程(即等待信号的进程)。
- 在现实生活中,当你收到快递员到达的通知后,可以选择立刻下楼取件或安排稍后处理。同样地,在计算机程序中,进程在接收到SIGINT信号后,默认会立即停止执行(退出进程),这就好比你收到快递通知后立即停止手头的事去收取包裹。
- 不过,如果你希望在接收到信号后延时处理,就需要在程序中添加自定义的信号处理函数来模拟“稍后再取件”的逻辑。在示例代码中,按下Ctrl-C后进程直接结束,对应的就是立刻响应并结束等待信号的状态。
需要注意在计算机操作系统的环境中:
-
当你在终端中直接启动一个命令或程序时,默认情况下它将以前台进程的身份运行。这意味着,如果你按下了
Ctrl-C
,产生的 SIGINT 信号将直接发送给当前处于前台的进程。若要在启动命令后使其在后台运行,只需在命令末尾添加一个 '&' 符号,这样一来,Shell 不必等待此进程结束即可接收新命令并启动其他进程。 -
Shell 具有灵活的并发处理能力,它能同时管理一个前台进程及任意数量的后台进程。然而,值得注意的是,只有当前台进程才具备捕获由诸如
Ctrl-C
这类控制键触发产生的信号的能力。换句话说,后台进程无法直接响应来自终端的这类交互式信号。 -
对于前台进程中运行的用户空间代码而言,由于用户在进程执行的任何时刻都可能按下
Ctrl-C
发送 SIGINT 信号,因此进程在执行流程中的任何一个点都可能遭遇此信号并随之终止。这种不受特定执行阶段限制、随时可能出现的信号特性,使得信号对于进程的控制流程来说具有显著的异步性(Asynchronous)。这就意味着进程必须能够适时地响应这些非预期的外部事件,以保证其正常且稳定地运行。
3、操作系统角度的信号
信号如何产生
[@localhost code_test]$ cat sig.c
#include <stdio.h>
int main()
{while (1) {printf("I am a process, I am waiting signal!\n");sleep(1);}
}
[@localhost code_test]$ . / sig
I am a process, I am waiting signal!
I am a process, I am waiting signal!
I am a process, I am waiting signal!
^ C
[@localhost code_test]$
-
当用户在终端中按下
Ctrl+C
组合键时,实际上触发了一个硬件中断。操作系统(OS)通过键盘设备驱动程序识别到这个特定的中断请求,并将其解释为向目标前台进程发送一个特定的信号——通常是编号为2的 SIGINT 信号。 -
操作系统接收到组合键信号后,会遍历进程列表,定位到当前在终端前台运行的进程。一旦确定目标进程,OS就会在该进程的进程控制块(PCB)内部的数据结构中记录或更新相应的信号位图信息。
-
信号位图通常是一个二进制的表示形式,例如,一个二进制位对应一个信号类型,当某位置1时就表示该进程已经收到了相应类型的信号。
理解组合键变为信号
- 键盘通过中断机制与CPU通信,其中包含了对组合键(如Ctrl+C)的识别功能。当这样的组合键被按下时,硬件会触发一个中断请求,操作系统接收到这个中断后,经过一系列解析和处理步骤,将组合键的含义映射为特定的信号类型。
理解信号如何被进程保存
-
每个进程在内核中都有一个进程控制块(PCB,Process Control Block),这是一个核心数据结构,用于存储和维护进程的状态信息,其中包括信号位图字段。当一个信号被发送给进程时,本质上是操作系统将这个信号的标记写入到该进程的PCB中的信号位图中。
-
“信号发送”的实质在于:操作系统不是通过某种通信机制发送信号,而是直接修改目标进程的PCB结构,改变其中信号位的状态,以此告知进程它已收到一个新的信号。进程随后可以通过检查其自身的PCB来得知收到了何种信号,并根据预先设定的策略进行相应的处理,如执行默认操作(如终止进程)、忽略信号或执行自定义的动作(如注册的信号处理器函数)。
[hbr@VM-16-9-centos signal]$ kill -l1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1
11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM
16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP
21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ
26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR
31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3
38) SIGRTMIN+4 39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8
43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7
58) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2
63) SIGRTMAX-1 64) SIGRTMAX
时钟中断(了解)
时钟中断是计算机硬件体系结构中的一个重要特性,它为操作系统提供了一个周期性的触发点,使得操作系统能够在规定的时间间隔基础上进行一系列关键的系统管理操作。以下是对时钟中断的详细解释:
发生原理: 时钟中断通常由计算机系统内部的一个定时器硬件设备产生。这个定时器(有时也称为滴答定时器或心跳定时器)基于硬件计数器工作,每当计数器递增达到预设的阈值时,会产生一个电信号,向中央处理器(CPU)发出中断请求。操作系统通过配置定时器的计数速率,可以设定时钟中断的频率,这个频率通常以毫秒级别进行设置。
核心作用:
-
任务调度:时钟中断是操作系统进行进程调度的主要依据之一。每当发生时钟中断,操作系统会暂停当前正在执行的进程,并保存其现场(上下文),然后根据调度策略选择下一个高优先级或等待时间片耗尽的进程继续执行。这种机制实现了多任务的并发执行(尽管在单核CPU上实际上是轮流执行),使各进程能够共享CPU资源,提高了系统的并发性能和响应能力。
-
时间管理:时钟中断用于维护系统的全局时间,包括但不限于系统时钟、进程运行时间统计、定时器超时检测等。操作系统通过累计时钟中断次数来更新系统时间,确保了系统时间的准确性和实时性。
-
实时性支持:在实时操作系统(RTOS)中,时钟中断更加重要,因为它为系统提供了严格的时间基线,确保了关键任务能够按照预定的时间间隔得到及时执行,满足实时性要求。
-
硬件同步与I/O轮询:时钟中断也可能用于硬件同步操作,例如与网络设备的时间同步或周期性检查I/O设备的状态,确保CPU在设备准备就绪时迅速响应。
-
总结: 时钟中断是操作系统的心跳,它赋予了系统时间感知能力和任务切换机制。通过周期性地打断CPU的连续执行,操作系统得以执行后台管理和调度任务,确保了系统资源的合理分配和高效使用。
4、信号定义、kill -l查看信号列表
信号是一种操作系统机制,用于在进程间实现事件的异步通信和通知,其本质上属于软件层面的中断处理方式。当一个进程接收到信号时,它会暂停当前任务的执行,并转而去响应预先定义好的信号处理程序,从而实现进程间的非同步交互。简而言之,信号作为一种软中断手段,能够在进程之间高效、灵活地传播事件信息和触发相应动作。
信号列表:
kill -l
命令在Unix/Linux系统中用于列出系统内建的所有信号及其对应的数字编号。信号是操作系统用于进程间通信或控制进程行为的一种机制,例如通知进程结束运行、报告错误状态或者要求进程执行某种特定动作。
每个信号都有一个独一无二的整数编号,并且在C/C++编程中,通过包含<signal.h>
头文件可以访问到这些信号对应的宏定义名称。例如,宏定义SIGINT
通常代表的是中断信号(Ctrl+C产生的信号),其对应的信号编号是2。
[hbr@VM-16-9-centos signal]$ kill -l1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1
11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM
16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP
21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ
26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR
31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3
38) SIGRTMIN+4 39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8
43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7
58) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2
63) SIGRTMAX-1 64) SIGRTMAX
对于标准信号,即编号小于等于33的那些信号,它们是内建于大多数Unix-like系统的基本信号集(普通信号)。例如:
SIGHUP
(1):终端挂断或会话首进程退出时发出。SIGINT
(2):用户按下中断键(通常是Ctrl+C)时产生。SIGQUIT
(3):用户按下退出键(通常是Ctrl+\)时产生。SIGKILL
(9):无条件终止进程信号,不可被捕获或忽略。SIGTERM
(15):请求进程正常终止的信号,可被捕获并自定义处理。
而编号34及以上的信号称为实时信号,它们是为了满足特定需求而设计的额外信号,主要用于实现更高优先级或更精确的进程间同步与通信,且通常具有更高的优先级。
- 在Unix/Linux系统中,通过查阅
man 7 signal
手册页可以获得更加详细的信号列表,包括每种信号何时由操作系统产生(触发条件)、它们默认的行为(比如是否会导致进程终止、暂停或继续执行),以及如何更改这些信号的默认处理方式等内容。
5、信号处理的常见方式
在计算机系统中,对于接收到的信号,常见的处理方式包括但不限于以下三种策略:
-
忽略信号:这种处理方式允许进程选择不响应特定的信号,即当接收到该信号时不进行任何操作,任由信号自然消逝。例如,进程可以选择忽略某些不影响其正常运行的非关键性信号。
-
执行默认处理动作:每个信号都有一个系统预设的默认处理动作。当接收到信号时,进程可以选择按照系统的默认方式进行响应。比如,SIGINT(中断)信号的默认处理通常是终止进程,而SIGSEGV(段错误)信号的默认处理通常是结束进程并生成core dump文件。
-
捕捉信号(Signal Catching):这是一种更为精细的信号处理方式,允许进程为特定信号注册一个自定义的信号处理函数。当指定的信号发生时,操作系统会暂停当前进程,并切换到用户态执行预先设定好的处理函数。这样,进程可以根据自身需求实现更灵活、个性化的信号响应逻辑。通过这种方式,进程能够捕捉并针对性地处理各种可能出现的异常情况或进行特定的程序控制流转。