目录
一、操作系统引导编辑
二、虚拟机
三、进程之间的通信
四、信号
一、操作系统引导![]()
CPU通过ROM(主存特定地址)中的程序将磁盘中的MBR(磁盘第一块---主引导记录)加载到内存上,CPU执行磁盘引导程序,通过分区表,找到C盘(活动分区),并且将C盘中的”找到启动管理器程序“加载到内存中,CPU执行”找到启动管理器程序“,通过根目录,找到启动管理器程序,执行,完成操作系统的启动工作。
CPU通过ROM上的固有程序将磁盘上的特定程序加载到内存上,CPU执行程序找到C盘上的特定程序,找到启动管理器,执行程序,启动操作系统
二、虚拟机
第一类:直接运行在硬件上
单核CPU实现:将CPU的每个时间片进行划分,每个虚拟机器获得一定的时间片
磁盘,内存:空间划分出来给不同的虚拟机器
类似于操作系统对硬件资源的划分,让每个进程有独立的资源
虚拟管理程序是运行在内核态的,而操作系统运行在用户态,当它执行特权指令时,会被VMM截获,VMM会负责特权指令的执行。
第二类:老师让我们在电脑上安装Liunx虚拟机....
三、进程之间的通信
两个进程之间数据(比如一段文本、一个结构体、一个文件路径)的交互叫做通信
共享存储
- 共享区共享:在进程P用shm_open系统调用向操作系统申请共享内存区,两个进程使用mmap系统调用将共享区映射到各自虚拟地址空间 ,数据形式和存放位置由进程自主控制,属于高级通信方式,进程之间互斥访问共享区
- 数据结构共享:给共享空间限定特定数据结构(如固定长度数组)必须按预定格式读写数据,属于低级通信方式
消息传递
:“消息”
- 直接通信:P进程在自己的地址空间构造完整消息,用send原语发送消息(这里是系统调用),指明接收进程ID ,操作系统将消息复制到内核空间,并挂到该ID进程的消息队列,接收进程Q使用receive原语接收消息,指明发送进程P的ID,操作系统将消息从内核空间复制到进程Q地址空间。每个进程PCB中包含消息队列,存储其他进程发送给它的消息
- 间接通信:P进程通过系统调用申请信箱(可创建多个,如信箱a、b)P进程使用send原语发送消息到指定信箱(不指明接收进程),接收进程Q使用receive原语从指定信箱接收消息
管理特点:允许多个进程向同一个信箱发送消息,允许多个进程从同一个信箱接收消息,不需要指明具体接收/发送进程,只需指定信箱。
管道通信
P进程在循环队列的一端写入,Q进程从另一端读取,管道(pipe文件)本质是内存中的固定大小缓冲区。 进程之间互斥访问pipe文件。
写满就不能写,读取完就不能读取,数据一旦被读出就从管道中消失
pipe允许多个写进程,只允许一个读进程(与高教社2014年真题一致),实际系统(如Linux):允许多个读进程轮流读取
读进程条件:只要管道不空就可读 ,写进程条件:只要管道不满就可写 ,不需要等待完全写满或读空
四、信号
这就是Linux早期所有的信号
信号代表特定的事件的发生,用于通知一个进程,一个事件的发生
每个进程的PCB中都有
unsigned int pending [31]:记录哪些信号已经产生,但还在等待处理
unsigned blocked [31]:记录进程当前主动屏蔽了哪些信号
handler 表:这是一个函数指针数组(sighandler_t 数组)它记录了每个信号对应的处理动作(是默认处理、忽略,还是执行你写的自定义函数)
发送信号的本质动作:操作系统内核将目标进程 PCB 中 pending 位图里对应信号的比特位由 0 置为 1。
发送信号方式
- 用户进程通过系统调用发送:A 给B 发信号 / 自己给自己发信号
- 内核自己发(不走系统调用)比如你按了键盘,或者程序发生了“除以零”的致命错误。这些硬件中断或异常会直接触发内核,内核在内部直接调用原语给目标进程发信号,这属于内核的内部操作。
信号是怎么被处理的
内核不会在进程专心干活(用户态)时强行打断它,而是采用“软插队”:
- 进程从内核态返回用户态的那一刻,内核会检查 PCB 里的pending中有没有未处理的信号。如果有,就去 handler 表里查信号对应的函数指针,内核会修改进程的寄存器(把指令指针 EIP 改成信号处理函数的地址,把旧现场压入栈底)。然后让 进程 去执行。进程回到用户态后,以为自己还在正常跑,其实已经跳转去执行你提前写好的“信号处理函数”了。处理完后,通过一个特殊的系统调用(sigreturn)恢复旧现场,进程继续若无其事地干原来的活。
每一种信号都有默认处理程序,有的信号处理函数可以自定义,进程通过系统调用,自定义某些信号的处理程序
信号与异常的关系
异常是“因”,信号是“果”。 异常是硬件层面的底层事件,而信号是操作系统将这种底层事件“翻译”并传递给用户程序的软件机制。
异常是 CPU 在执行指令时,突然遇到无法继续正常执行的情况,从而被迫触发的内部中断。它完全发生在硬件和内核层面,用户程序根本感知不到异常的发生。
常见的异常包括:
- 除零异常:执行了除以 0 的操作。
- 缺页异常(Page Fault):访问了一个虚拟地址,但对应的物理内存还没分配。
- 非法指令异常:执行了 CPU 根本不认识的机器码。
- 段错误(Segmentation Fault):试图访问没有权限的内存地址(比如往只读内存里写数据)。
当 CPU 触发异常时,它会立刻暂停当前进程,陷入内核态,执行对应的异常处理程序。此时,内核会充当“翻译官”,根据异常的类型,决定下一步怎么做:
情况一:内核自己能搞定(默默处理)
比如缺页异常。内核发现你访问的内存地址是合法的,只是还没分配物理页。内核就会在内核态悄悄帮你分配内存、更新页表,然后让进程若无其事地继续运行。整个过程用户程序毫无察觉。情况二:内核搞不定,必须通知用户程序(转化为信号)
比如除零异常或段错误。内核在内核态无法修复这种逻辑错误,它必须把“锅”甩给用户程序。怎么甩?就是给该进程发送一个对应的信号,通知它出了什么事- 除零异常 ➡️ 内核发送
SIGFPE(算术异常信号) - 段错误 ➡️ 内核发送
SIGSEGV(段错误信号) - 非法指令 ➡️ 内核发送
SIGILL(非法指令信号)
- 除零异常 ➡️ 内核发送
当内核把异常翻译成信号发给进程后,进程在返回用户态时就会收到这个信号。此时命运就掌握在用户程序手里了:
- 默认行为(通常是自杀):如果用户程序没有对
SIGSEGV等致命信号做特殊处理,进程就会立刻终止,并可能在终端打印出Segmentation fault (core dumped)。这就是我们平时写 C 语言时经常遇到的“程序崩溃”。 - 自定义处理(起死回生):如果用户程序提前用
signal()注册了处理函数,进程就会去执行那段代码。比如,你可以写一个SIGSEGV的处理函数,在里面打印一句“哎呀,内存访问越界了,但我正在尝试恢复...”,然后优雅地退出或进行补救。
你可以把整个流程想象成一场“交通事故处理”:
- 异常:汽车撞上了护栏(硬件/底层发生了事故,CPU 被迫停下)。
- 内核:交警到达现场。如果是小刮擦(缺页异常),交警自己帮你修好,让你继续开;如果是严重车祸(段错误),交警无法现场修车。
- 信号:交警给你(用户程序)打了一个电话(发送
SIGSEGV信号),告诉你“车撞坏了,你看着办吧”。 - 信号处理函数:你接到电话后,决定是叫拖车(优雅退出),还是尝试自己把车推走(自定义恢复逻辑)。
以下为提问
所以进程A能给进程B发送信号,是因为B进程属于A?
这是一个非常关键的误解!进程 A 能给进程 B 发送信号,并不是因为 B 属于 A。
事实上,在操作系统中,进程之间是完全平等且相互隔离的,不存在“谁属于谁”的从属关系。
进程 A 之所以能给进程 B 发信号,完全取决于“权限(Permissions)”和“规则”。在 Linux 等现代操作系统中,内核判断 A 能不能给 B 发信号,主要看以下两条铁律:
1. 规则一:UID 相同(同属一个主人)
如果进程 A 和进程 B 的真实用户 ID(UID)相同,那么 A 就可以给 B 发信号。
- 通俗解释:这就好比你在一家公司(同一个操作系统用户)里,虽然你们在不同的部门(不同的进程),没有上下级关系,但既然都是同一个老板(UID)名下的员工,你就有权限去“拍他的肩膀”(发信号)。
2. 规则二:拥有超级用户权限(Root)
如果进程 A 是以超级用户(Root / UID=0)的身份运行的,那么它可以给系统里的任何进程发信号。
- 通俗解释:这就相当于你是公司的最高管理员或董事长,你可以直接去敲任何员工的门,甚至直接开除他(发送
SIGKILL信号)。
补充:你可能混淆了“父子进程”和“信号发送”
你之所以会有“B属于A”的错觉,可能是因为在实际开发中,我们最常遇到的场景是:父进程给子进程发信号(比如子进程跑飞了,父进程把它杀掉)。
- 父子关系:确实存在“谁创建了谁”的关系(A 调用
fork()创建了 B,A 是父,B 是子)。 - 发信号的关系:父子进程发信号,本质上依然是因为它们的 UID 相同(子进程继承了父进程的 UID),符合了上面的规则一。
反过来也完全成立:
子进程 B 同样可以给父进程 A 发信号!比如子进程执行完毕后,内核会自动给父进程 A 发送一个SIGCHLD信号,通知 A 来回收自己的尸体。
总结
操作系统里没有“谁属于谁”才能发信号的说法。只要你的权限够大(Root),或者你们归同一个用户管(UID 相同),哪怕这两个进程八竿子打不着,A 也能给 B 发信号。
UID怎么理解
UID 的全称是User ID(用户身份标识符)。
你可以把它理解为操作系统里的“数字身份证”。
在操作系统的眼里,它其实是个“脸盲”,它不认识“张三”、“李四”这些人类名字,也不认识“微信”、“QQ”这些软件。它只认数字。每一个在系统里登录的用户,或者每一个运行程序的“身份”,都会被分配一个唯一的数字,这就是 UID。
为了让你彻底弄懂,我们从以下三个维度来剖析:
1. 为什么要有 UID?(为了安全和隔离)
操作系统是一个多用户环境。比如一台服务器上,可能同时有“管理员”、“开发A”、“开发B”在用。
如果没有 UID,开发 A 就能随便看开发 B 的私密文件,甚至把 B 的程序给杀了,那系统就乱套了。
有了 UID,操作系统就能严格划分地盘:
- 文件归属:你创建一个文件,系统就会在文件的属性里打上你的 UID 标签。只有 UID 相同的人(或者 Root)才能修改它。
- 进程归属:你启动了一个程序,这个程序对应的进程也会继承你的 UID。
2. UID 是怎么分配和识别的?
在 Linux 系统中,UID 通常是一个整数。有几个非常经典的 UID:
- UID = 0:这是超级用户(Root)。拥有至高无上的权限,可以无视一切规则,为所欲为。
- UID = 1 ~ 999:通常是系统保留给系统服务用的。比如 Nginx 服务、MySQL 服务,它们为了安全,通常不会用 Root 运行,而是分配一个专门的低权限 UID。
- UID = 1000 及以上:这是分配给真实的人类用户的。比如你创建的第一个普通用户,UID 通常就是 1000。
3. 回到刚才的“发信号”话题
现在你明白为什么“UID 相同就能发信号”了吧?
当进程 A 想给进程 B 发信号时,内核会做一个极其简单的算术题:
“进程 A 的 UID == 进程 B 的 UID 吗?”
- 如果相等:内核认为“哦,这俩是同一个老板手下的,自己人,允许发信号!”
- 如果不相等:内核就会拦截:“对不起,你俩不是一个部门的,你没有权限去打扰他。”(除非你是 UID=0 的 Root)。
打个通俗的比方:
UID 就像是小区业主的“门禁卡”。
- 你(进程 A)想进 302 室(进程 B)敲门(发信号)。
- 保安(操作系统内核)会查你的门禁卡(UID)。
- 如果你们俩的卡号一样,说明你们是同一个业主,保安会放行。
- 如果卡号不一样,保安就会把你拦下。
这样解释,是不是就把 UID 这个概念彻底具象化了?