进程通信
一、为什么需要进行进程通信呢?
1.1进程具有独立性
每一个进程的数据都是存储在物理内存当中的,进程通过自己的虚拟地址空间去进行访问,访问的时候通过各自的页表的映射关系,访问到物理内存当中去
从进程的角度看,每个进程都认为首己拥有4G(32位下)的空间,至 于物理内存当中属于如何存储,页 表如何映射,进程是不清楚。 这 也造就了进程的独立行。
1.2进程独立性的优缺点
优点:让每个进程运行的时候都是独立进行的,他们只能访问自己的内存空间
缺点:如果两个进程想要进行数据交换时,由于进程的独立性,就不是那么的方便了
所以: 进程问通信本质上是进程和进程之间交换数据的手段。
1.3进程通信的手段
管道,共享内存,消息队列,信号量,网络
下面将一一介绍这几种方式
1、匿名管道(半双工)
文件描述符
文件描述符fd,是非负整数,有三个特殊的描述符0(stdin).1(stdout).2(stder
他们有专门的用途,stdin对应键盘输入,stdout和stderr对应屏幕,将信息/错误信息输出到屏幕上
我们打开一个新的文件会从3号开始
- 作用:这个表述符和文件是有一个一一对应的关系的,所以我们需要一张表来建立这个整数和具体文件的映射关系,这张表就是我们的文件描述符表。
文件描述符表:
每个进程都有一个文件描述符表,每个进程默认Max可打开的文件描述符为1024个
使用ulimit命令进行查看openfiles的值,发现确实是1024个
实际上文件描述符表是一个数组,我们这里的fd(0,1,2,3…)就被隐含在了数组的下表上了。
每一个描述符都对应一个struct file,stdin的file,stdout的file,stderr的file均比较特殊是由操作系统默认就打开的,主要是读取键盘输入,和输出东西到屏幕。
管道的实质
管道在内核里是一块缓冲区,供进程进行读写、数据交换
-
管道只适用于 存在血缘关系 的两个进程之间通信,因为只有存在血缘关系的两个进程之间才能共享文件描述符
-
管道分为两端,一端读,一端写,有两个文件描述符分别表示读端和写端
-
管道是单向的,数据从写端输入,从读端取出
-
管道的本质是一个伪文件(实为内核缓冲区)
-
管道有容量限制,2.6.11 版本之前的 Linux 内核管道容量为 page size(4 Kb),之后版本改为 65535 字节
-
当管道满时,写端将堵塞。当管道空时,读端将堵塞
父进程调用fork函数成功后,子进程不能同时保留读写文件描述符,需要关闭读或者写文件描述符。
防止父子进程同时读写引发数据错误。
实现全双工
如何理解这个管道缓冲区?
管道的缓冲区是一个有限大小的内存区域,用于临时存储从一个进程写入管道的数据,以及另一个进程从管道读取的数据。以下是管道缓冲区的工作原理:
- 写入数据: 当一个进程向管道写入数据时,数据会被复制到管道的缓冲区中。如果缓冲区未满,写入过程会成功,并且数据会被存储在缓冲区中。如果缓冲区已满,写入过程会被阻塞,直到缓冲区有足够的空间来存储数据。在这种情况下,写入进程会等待,直到另一个进程从管道中读取数据,从而释放出空间。
- 读取数据: 当一个进程从管道中读取数据时,数据会被从管道的缓冲区中复制到进程的内存空间中。如果缓冲区中没有数据可读(即为空),读取过程会被阻塞,直到有数据可供读取。在这种情况下,读取进程会等待,直到另一个进程向管道中写入数据,从而提供了可读的数据。
- 阻塞与非阻塞: 在默认情况下,管道的读写操作是阻塞的,即如果写入进程尝试向满的管道写入数据,或者读取进程尝试从空的管道读取数据,那么它们会被阻塞,直到条件满足。您也可以设置管道的文件描述符为非阻塞模式,这样写入或读取操作会立即返回,无论管道的状态如何。
- 有限大小: 管道的缓冲区是有限大小的,因此在某些情况下,写入进程可能会被阻塞,直到读取进程读取了数据。如果写入速度过快,而读取速度过慢,缓冲区可能会被填满,导致写入进程被阻塞。因此,使用管道进行进程间通信时,需要注意缓冲区的大小和数据传输速度的匹配。
综上所述,管道缓冲区充当了临时存储数据的中介,使得进程间可以通过管道进行通信。理解管道缓冲区的工作原理对于正确地使用管道和解决相关问题至关重要。
API 说明
1. 头文件
#include <unistd.h>
2. 创建并打开管道
int pipe(int pipefd[2]);//pipefd[0] :读端的文件描述符;pipefd[1] :写端的文件描述符
close(pipefd[0]);//代表关闭读端
close(pipefd[0]);//代表关闭写端
示例
注: Ctrl+S会触发终端的“流控制vim的使用,a编辑,esc-:wq保存并退出 u撤销
按下Ctrl+S会触发终端的“流控制”(Flow Control)功能,这个功能会暂停终端的输出,导致终端看起来像“冻结”了一样,不再响应键盘输入。这是因为在Unix和类Unix系统中,Ctrl+S被用作停止输出的控制字符,而Ctrl+Q则是用来恢复输出的。
要解决这个问题,您可以按下Ctrl+Q来恢复终端的输出,让终端重新响应键盘输入。按下Ctrl+Q之后,终端应该会恢复正常工作,您可以再次进行编辑操作。
验证1:fd[0]读的值是3,fd[1]写的值是4
#include<stdio.h>
#include <unistd.h>
int main()
{
int fd[2];
printf("fd[0]=%d,fd[1]=%d",fd[0],fd[1]);
int x=pipe(fd);//int pipe=pipe(fd);会报错
if(x<0)
{
perror("管道创建失败");
return 0;
}
printf("fd[0]=%d,fd[1]=%d",fd[0],fd[1]);
return 0;
}
通过ps aux命令, 可以查看系统上运行的所有进程的详细信息
注:Ctrl+space键可以切换Linux的输入法
要让程序一直保持运行而不退出,您可以在程序中创建一个无限循环或者一个长时间的等待。这样,即使程序执行完所有指令,它也会继续等待。
下面是一个简单的示例程序,它创建一个无限循环,直到接收到终止信号为止:
#include <stdio.h>
#include <signal.h>
volatile sig_atomic_t flag = 1;
void handle_sigint(int sig) {
flag = 0;
}
int main() {
signal(SIGINT, handle_sigint);
while (flag) {
// 程序逻辑
// 可以是一些操作、计算、打印等
}
printf("程序已退出\n");
return 0;
}
方式二:
sleep(60);
printf("一分钟已经过去了,程序即将退出。\n");
这个程序在main
函数中创建了一个无限循环,直到接收到SIGINT信号(Ctrl+C)后退出。在接收到SIGINT信号时,它将修改flag
变量的值,退出循环并打印一条消息。
cd /proc/15419/fd
命令
是用来进入Linux系统中进程ID为15419的进程的文件描述符目录。在Linux系统中,
/proc
目录下包含了系统中运行进程的相关信息,其中的每个子目录都对应一个进程的信息。在这个目录中,
fd
是一个特殊的目录,它包含了进程的文件描述符的符号链接。
在/proc/15419/fd
目录中,每个文件描述符都被表示为一个符号链接,指向实际的文件或其他I/O资源。通过进入这个目录,您可以查看进程15419当前打开的所有文件描述符所指向的文件或资源。这对于查看进程的I/O活动、文件打开情况等非常有用。
pstack 命令
pstack
是一个用于显示进程的栈跟踪信息的命令。在类Unix系统中,特别是在Linux系统上,pstack
命令通常用于诊断运行中的进程的问题,特别是在进程因为死锁、卡死或者其他原因而停止响应时。
pstack
命令显示**指定进程的每个线程的栈跟踪信息**,包括线程的堆栈帧和函数调用链。这个信息可以帮助开发人员定位问题,例如找出导致死锁的原因、确定进程中的哪些函数或代码段导致了性能问题等。
要使用pstack
命令,您需要知道目标进程的PID(进程ID)。然后,在终端中运行以下命令:
pstack <PID>
其中,<PID>
是目标进程的PID。执行这个命令后,pstack
将会显示目标进程的每个线程的栈跟踪信息。
请注意,pstack
命令可能需要使用root权限或者其他足够的权限来查看目标进程的栈信息。
查看父进程的子进程的ID
-
使用
ps
命令: 使用ps
命令可以列出系统上运行的进程。您可以使用ps
命令结合grep
来查找特定父进程的子进程。例如,要查找父进程PID为12345的所有子进程,可以运行以下命令:ps --ppid 12345
这将列出所有父进程PID为12345的子进程的详细信息。
-
使用pstree命令:
pstree
命令以树状结构显示进程。您可以使用pstree
命令来查看特定父进程的所有子进程。例如:pstree -p 12345
这将以树状结构显示父进程PID为12345的所有子进程。
-
查看
/proc
目录: 在Linux系统中,每个进程都有一个对应的目录/proc/<PID>
,您可以在这个目录中查看有关进程的各种信息。特别地,您可以查看/proc/<PID>/task
目录,其中包含了与父进程相关的所有子进程的信息。例如,要查看父进程PID为12345的所有子进程的ID,可以运行以下命令:ls /proc/12345/task
这将列出父进程PID为12345的所有子进程的ID。
使用这些方法之一,您可以查看特定父进程的所有子进程的ID。
ps aux和ps的区别
ps aux
命令用于**显示系统上所有进程的详细信息**,而不仅限于当前终端。aux
代表"All Users",表示显示所有用户的进程信息。与ps
相比,ps aux
输出的信息更详细,包括进程的用户、CPU利用率、内存利用率等。
下面演示一个,父进程往管道写入数据,子进程从管道读出数据并输出到标准输出流。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
int main() {
int pipe_fd[2]; // pipe_fd[0] 表示读端,pipe_fd[1] 表示写端
pid_t child_pid;
char buf[] = "Hello World";
if (pipe(pipe_fd) == -1) { // 创建管道
perror("pipe failed");
exit(EXIT_FAILURE);
}
child_pid = fork(); // 创建一个子进程
//------------------------------------------------
//fork()以下部分,子进程和父进程交替上处理机执行
//但是由于fork()的特点,上面的child_pid在父进程中的值是父进程的pid,而子进程中这个值就是0
if (child_pid == -1) {
perror("fork failed");
exit(EXIT_FAILURE);
}
if (child_pid == 0) { // 根据 fork 的特点,child_pid 等于 0 表示子进程
close(pipe_fd[1]); // 子进程从管道读数据,所以关闭写端
while (read(pipe_fd[0], &buf, 1) > 0) { // 从读端读出数据
write(STDOUT_FILENO, &buf, 1); // 写入到标准输出流显示
}
close(pipe_fd[0]);
_exit(EXIT_SUCCESS);
} else { // 根据 fork 的特点,child_pid 大于 0 表示父进程
close(pipe_fd[0]); // 父进程从管道写数据,所以关闭读端
write(pipe_fd[1], &buf, strlen(buf)); // 往写端写入数据
close(pipe_fd[1]);
wait(NULL); // 等待并回收子进程
_exit(EXIT_SUCCESS);
}
return 0;
}
关于fork()
在调用fork()
函数后,fork()
函数会在当前进程的地址空间中创建一个新的子进程。fork()
函数的返回值不同,分别代表了在父进程和子进程中的不同情况:
- 在父进程中:
fork()
函数返回新创建的子进程的PID(进程ID),这个PID是一个大于0的整数,表示新创建的子进程的进程ID。因此,result
变量在父进程中的值为子进程的PID。 - 在子进程中:
fork()
函数返回**0**,表示当前正在运行的进程是子进程。因此,在子进程中,result
变量的值为0。
2.命名管道
命名管道的特性:
1.支持不同的进程进行通信,不依赖亲缘性了,因为不同的进程可以通过管道文件去找到管道(操作系统内核的缓冲区)
2.在读写有名管道之前需要用open函数打开该有名管道,打开有名管道操作与其他文件有一定的区别,如果希望打开管道的写端,则需要另一个进程打开该管道的读端,如果只打开有名管道的一端,则系统将暂时阻塞打开进程,知道另一个进程打开管道的另一端,当前进程才会继s续执行,因此,在使用有名管道时一定么使用两个进程分别打开其读端和写端!
mkfifo
命令用于创建一个命名管道(named pipe),也称为FIFO(First In First Out)。命名管道是一种特殊类型的文件,在文件系统中以文件的形式存在,可以用于进程间通信。命名管道允许一个或多个进程通过打开和读写文件来进行通信,就像使用普通文件一样。
ll
命令实际上是ls -l
命令的一个别名,用于列出目录中的文件和子目录,并显示它们的详细信息,例如权限、所有者、大小、创建时间等。ll
命令通常在一些Linux发行版中预先设置为ls -l
的别名,方便用户快速查看目录内容的详细信息。
结论:
1、数据还是存储在内核的缓冲区当中的
2、管道文件的作用是为了让不同的进程可以找到这块缓冲区
fifo1
和 fifo2
在打开时需要两个进程进行协作。如果一个进程只打开 fifo1
进行写操作,而另一个进程没有打开 fifo2
进行读操作,或者相反,程序可能会阻塞或失败。
为了确保这两个FIFO文件被正确打开,我们需要分别运行两个进程:一个进程负责写操作,另一个进程负责读操作。我们将这个程序分成两个部分,一个是发送者,另一个是接收者。
3.消息队列
1、消息队列的原理
消息队列是Linux的一种通信机制,这种通信机制传递的数据具有某种结构,而不是简单的字节流。
在Linux
内核我们可以创建一个队列结构,然后我们可以将我们需要发送和读取的数据插入这个队列里面,多个不同的进程可以通过相同的key
值找到相同的队列。
对于消息队列来说:无论发送消息的进程还是接收消息的进程,都需要在进程空间中用消息缓冲区来暂存消息,然后向消息队列写入或读取数据时也按照结构体的方式来进行写入和读取的!对于进程来说消息缓冲区的结构定义一般如下:
由于结构体中有一个mtype
类型,这个字段可以帮助我们区分是哪一个进程写入的,消息队列里面可以让多个不同的进程写入数据,多个不同的进程读取数据,因此消息队列是全双工通信,可读可写。
消息队列的内核结构
- 消息队列的本质其实是一个内核提供的链表,内核基于这个链表,实现了一个数据结构。
- 向消息队列中写数据,实际上是向这个数据结构中插入一个新结点;
- 从消息队列中读数据,实际上是从这个数据结构中删除一个结点。
- 和管道一样,每个消息的最大长度是有上限的(MSGMAX),每个消息队列的总字节数也是有上限的(MSGMNB),系统上的消息队列总数也是有上限的(MSGMNI)
- 消息队列是一个全双工通信,可读可写。
- 消息队列的生命周期是随内核的,即进程退出以后消息队列不会消失!
2、消息队列的使用
和共享内存一样,消息队列的使用也涉及很多的系统调用,而且它们的调用接口都是类似的。
1、msgget函数
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgget(key_t key, int msgflg);
此函数用于帮我们创建消息队列,创建完毕以后会给我们返回一个消息队列的标识符。
- 参数:
- 与共享内存一样,是一个
key
值,可以通过ftok
函数进行获取 - 是一个标志位,主要有三个标志:
IPC_CREAT
和IPC_EXCL
及mode_flags
其含义与共享内存一致。
- 与共享内存一样,是一个
- 返回值:调用成功,返回一个和参数
key
相关联的消息队列的标识符,调用失败就返回-1
,错误码被设置。
2、msgctl函数
int msgctl(int msqid, int cmd,struct msqid_ds *buf);
此函数主要用来控制消息队列。
- 参数
- 来自于
msgget
函数得到的消息队列标识符。 - 是标志位,里面有许多标志,我们经常使用的是这两个:
IPC_RMID
,IPC_STAT
- 一个
struct msqid_ds
类型的指针,在标志位中设置了IPC_STAT
,指针所指向的变量里面就能拿到相关的内核信息,如果不关心内核信息可以设置为nullptr
- 来自于
- 返回值:一般来说,成功返回是
0
,错误返回结果是-1
。
3、msgsnd函数
此函数可以将我们要通信的消息放入消息队列里面,类似与Linux
文件操作中的write
函数。
- 参数
- 来自于
msgget
函数得到的消息队列标识符。 - 要写入的消息的指针,由于消息队列支持多个进程进行写入,在向消息队列里面写数据时,用户自己要组织一个结构体,然后将这个结构体对象当成一条消息进行写入。实际中对于此参数的结构体常常这样定义:
- 发送的消息正文的字节数,注意这里的是指正文内容
mtext
里面数据的字节数。 - 标志位,
IPC_NOWAIT
消息没有发送完成函数也会立即返回,0
:直到发送完成函数才返回。
- 来自于
- 返回值:成功返回是
0
,错误返回结果是-1
。
4、msgrcv函数
这个函数的作用可以帮助我们从消息队列里面取出数据,类似与Linux
文件操作中的read
函数。
-
参数:
-
来自于
msgget
函数得到的消息队列标识符。 -
读取到的数据要放到哪里,这里还是填我们自定义的结构体对象。
-
要读取的正文字节数。
-
消息队列里面的消息的区分类型
mtype
,选择你想要读取的类型的数据 -
标准位,
IPC_NOWAIT
,非阻塞等待,若没有消息,进程会立即返回-1
。0
,阻塞等待。
-
-
返回值:
如果读取成功就返回读取到的字节数,如果读取失败就返回-1
,错误码被设置。
4.信号处理
一、什么是信号?
信号就是一条消息,它用来通知进程系统中发生了一个某种类型的事件。
信号是多种多样的,并且一个信号对应一个事件,这样才能知道收到一个信号后,到底是一个什么事件,应该如何处理这个信号。
1、信号的一些特性
- 进程在没有收到信号时就已经知道了一个信号应该怎么被处理了,这说明进程能够识别并处理信号。
- 进程记录的信号可能有很多个,因此进程需要用一种数据结构去管理所有的信号,在Linux下对于信号的管理采用的是位图结构,比特位的位置代表信号的编号。
- 所以所谓的发送信号本质就是:直接修改特定进程的信号位图中的特定的比特位。(由
0
->1
) - 进程信号的位图结构本质还是属于
task_struct
里面的数据,因此对于进程信号的位图结构里面的数据的修改,只能有操作系统来完成,即无论有多少种信号产生的方式,最终都必须让OS来完成最后的发送过程!
2、信号的处理方式
进程对于产生的信号不是立即去处理的,而是在"合适"的时候去处理信号,因为信号的产生的异步(信号产生时CPU在处理其它作业),时机:当进程从内核态切换回用户态的时候,进程会在操作系统的指导下,进行信号的检测与处理。
当CPU正在执行某条代码时,可能因为中断、异常或系统调用进入内核态,然后在内核态完成相应的任务,任务完成以后并不直接返回用户态,而是调用系统调用do_signal()去处理可以递达信号。
下图是我对下面过程的理解:
处理信号时会从1号到31号逐个检查block表(位图)和pending表,当block和pending表符合处理条件时才进行信号递达。(pending表为0代表该信号没有产生过,无需处理,block表为0,信号被阻塞,无需处理)即排查到某个信号block位图和pending位图的值为(01)时代表需要处理(需要递达)。
信号递达时就需要调用handler表里面对应位置的的函数进行执行:
SIG_IGN:忽略该信号,将该信号的pending表里面的1改为0,然后调用sys _sigreturn()系统调用进行返回原先中断的位置并恢复为用户态
SIG_DFL执行默认动作:1. 如果是暂停,就将该进程从运行队列里面取出放到等待队列里面,操作系统开始调度下一个进程。2. 如果是终止进程,就直接结束该进程,操作系统开始调度下一个进程。
用户自定义: 这里还处于内核态,执行用户自定义的代码应在在用户态中执行,需要先切回 用户态,把动作完成了,重新坠入 内核态,最后才能带着进程的上下文相关数据,返回用户态。
【注】
Q1:在 内核态 中,也可以直接执行 自定义动作,为什么还要切回 用户态 执行自定义动作?
我是这样理解的:如果直接在Ring0(内核态)执行用户代码,也就是意味着者段代码的级别由Ring3——>Ring0,直接就最高权限了,用户可以写危害系统的代码了,非常危险。
Q2:为什么不在执行完 自定义动作 直接后返回进程还要再回到内核态一次?
如果此次不回到内核态,那此次时间片的运行后的进程上下文更新的动作就没有完成,就是说“如果在用户态执行了一些操作后想要返回进程,就需要确保进程的上下文信息能够正确地保存和恢复,这通常需要在内核态中完成”
信号捕捉
信号捕捉主要是使用signal
函数,该函数内部使用了回调函数。
sighandler_t signal(int signum, sighandler_t handler);//1. 信号的编号。2. 回调函数的函数指针。
该函数的作用就是将指定的信号的默认行为更改为执行第二个参数对应的函数,这个函数要求必须是返回值为void
参数是int
的函数。
实例代码:
我们在键盘下按的 Ctrl + C 其实就是2号信号,下面我们尝试对2
号信号进行捕捉。
#include <iostream>
#include <signal.h>
#include <unistd.h>
void hander(int sig)
{
std::cout << "get a signal " << sig << std::endl;
}
int main()
{
signal(2, hander);
while (true)
{
std::cout << "我正在运行...,我的PID是: " << getpid() << std::endl;
sleep(1);
}
return 0;
}
3、Linux下的信号
在Linux下我们可以使用kill -l
命令列出所有的信号。
这里面是没有32 ,33
号信号的,其中从1~31
号信号是普通信号,34~64
是实时信号。
二、信号的产生
在Linux下进程信号的产生是有多种方式的,下面我们就来一起了解一下吧!
1、通过终端按键产生信号
在Linux下输入命令可以在Shell下启动一个前台进程,当我们想要终止一个前台进程时,我们可以按下 Ctrl + C 来进行终止这个前台进程,其实这个 Ctrl + C 也是一个信号,它对应的信号的2
号信号SIGINT
,这个信号对应的默认处理动作就是终止当前的前台进程。
- 用户按下 Ctrl-C ,这个键盘输入产生一个硬件中断 ,被OS获取,解释成信号,发送给目标前台进程,前台进程因为收到信号,进而引起进程退出
- Ctrl-C 产生的信号**只能发给前台进程。一个命令后面加个
&
可以放到后台运行,这样Shell**不必等待进程结束就可以接受新的命令,启动新的进程,同样这样的后台进程也无法使用Ctrl-C 来进行杀死。 - Shell可以同时运行一个前台进程和任意多个后台进程,只有前台进程才能接到像 Ctrl-C 这种控制键产生的信号。
关于硬件中断:
- 硬件中断是由硬件设备触发的中断,当硬件设备有数据或事件需要处理时,会向CPU发送一个中断请求,CPU在收到中断请求后,会立即暂停当前正在执行的任务,进入中断处理程序中处理中断请求。
关于软中断
- 信号是进程之间事件异步通知的一种方式,属于软中断。
2、调用系统函数向进程发信号
a、kill函数
#include <sys/types .h>
#include <signal.h>
int kill(pid_t pid, int sig);//1. 目标进程的`pid`。2. 要发送的信号`signal`。
kill
函数是操作系统给我们提供的一个系统调用,通过它我们能够**给指定的进程发送指定的信号**。
kill
命令其是就是调用kill
函数实现的,下面我们也来模拟实现一下kill
命令。
b、raise函数
int raise(int sig);//要发送的信号`sig`。
此函数会向当前进程发送指定的信号
我们用raise
函数给当前进程发送暂停信号19
SIGSTOP
,暂停以后我们可以在命令行中给进程发送继续运行18
号SIGCONT
信号
c、abort函数
void abort(void);
abort
函数使当前进程接收到信号而异常终止,abort
函数其实是向进程发送6
号信号SIGABRT
,就像exit
函数一样,abort
函数总是会成功的,所以没有返回值,值得注意的是就算6
号信号被捕捉了,调用abort
函数还是会退出进程。
实例代码:
**这三个函数只有kill
是系统调用,另外两个都是C库函数
3. 由软件条件产生信号
SIGPIPE
是一种由软件条件产生的信号,在“管道”中已经介绍过了。这里主要介绍alarm
函数和SIGALRM
信号。
unsigned int alarm(unsigned int seconds);
调用alarm
函数可以设定一个闹钟,也就是告诉内核在seconds
秒之后给当前进程发14
号信号SIGALRM
信号, 该信号的默认处理动作是终止当前进程。
- 参数:闹钟的秒数。
- 返回值:这个函数的返回值有一点特殊,它是是上一次设定的闹钟时间还余下的秒数或者是0(0代表上一次的闹钟没有收到干扰,正确的执行完了)
实例代码:
#include <iostream>
#include <signal.h>
#include <unistd.h>
int main()
{
alarm(1);
int count = 0;
while (true)
{
std::cout << count++ << std::endl;
}
return 0;
}
4、硬件异常产生信号
硬件异常产生信号是指硬件产生了错误并以某种方式被硬件检测到并通知内核,然后内核向当前进程发送适当的信号。
例如当前进程执行了除以0
的指令,CPU的运算单元会产生异常,内核将这个异常解释为SIGFPE
信号发送给进程。再比如当前进程访问了非法内存地址,MMU会产生异常,内核将这个异常解释为SIGSEGV
信号发送给进程。
大致原理:在计算机内部是有一个状态寄存器的,该寄存器内部是一个位图结构,如果对应的比特位为1
就表示本次计算有数据溢出的情况,说明本次计算结果不正确,CPU执行有误,而操作系统每次调度进程时都会去检查状态寄存器的状态,确保进程的执行的正确性。
当让CPU执行除0
操作就会引发数据溢出的问题,然后状态寄存器里面对应的比特位被置为1
,我们操作系统检测到了**状态寄存器中有比特位被置为1
**,就会向对应的进程发送SIGFPE
信号终止掉该进程,于是除0
就会导致程序崩溃。
补充
CR3
对于64位机,CR3寄存器也从32位变成了64位,它的主要功能还是用来存放页目录表物理内存基地址,每当进程切换时,Linux就会把下一个将要运行进程的页目录表物理内存基地址等信息存放到CR3寄存器中。
在CPU
中,存在一个 CR3
寄存器,这个寄存器的作用就是用来表是当前处于进程所处的状态。
当CR3
寄存器中的值为 3
时:表示处于用户态,可以执行用户的代码。
当CR3
寄存器中的值为 0
时:表示处于内核态,可以执行操作系统的代码。