一、并行和并发
并行 & 并发:有一个例子可以清晰地解释这二位的区别。如果将处理器(CPU)比作咖啡机,指令比作排队买咖啡的客人,则:
并发是两个队列交替使用同一台咖啡机;并行是两个队列同时使用两台咖啡机
二、进程控制块PCB
虚拟地址空间会通过CPU中的MMU(Memory Management Unit——内存管理单元),把虚拟地址空间中的数据映射到物理内存当中去。
在Linux内核中存在一个名为 PCB(Process Control Block——进程控制块)的东西,为了管理进程,操作系统必须对每个进程所做的事情进行清楚的描述,所以为每个进程分配一个PCB。一个PCB中的内容非常多,但只需要掌握以下几个:
- 进程id:系统中每个进程有唯一的 id,用pid_t类型表示,其实就是一个非负整数
- 进程的状态:有就绪、运行、挂起、停止等状态
- 进程切换时需要保存和恢复的一些CPU寄存器
- 描述虚拟地址空间的信息
- 描述控制终端的信息
- 当前工作目录(Current Working Directory)
- umask 掩码
- 文件描述符表,包含很多指向文件结构体的指针
- 和信号相关的信息
- 用户 id 和组 id
- 会话(session)和进程组
- 进程可以使用的资源上限(Resource Limit)
许多文件描述符以数组的形式形成了一个集合,即文件描述符表。一个文件描述符表中最多可以存放1024个文件描述符,文件描述符的作用就是定位磁盘上的某一个文件。
上图展示了一个完整的文件描述符表当中的内容,前3个是默认打开的文件描述符,它们都对应于了一个文件,即当前的终端(在Linux系统中有一句话“一切皆文件”,所以将终端也抽象为一个文件,而我们的显示器、网卡也都可以被抽象为文件)。
三、进程的状态
进程状态反映进程执行过程的变化,这些状态随着进程的执行和外界条件的变化而转换。在三态模型中,进程状态分为三个基本状态,即就绪态,运行态,阻塞态。在五态型中,进程分为新建态、就绪态,运行态,阻塞态,终止态。
- 运行态:进程占有处理器正在运行;
- 就绪态:进程具备运行条件,等待系统分配处理器以便运行。当进程已分配到除CPU以外的所有必要资源后,只要再获得CPU,便可立即执行。在一个系统中处于就绪状态的进程可能有多个,通常将它们排成一个队列,称为就绪队列;
- 阻塞态:又称为等待(wait)态或睡眠(sleep)态,指进程不具备运行条件,正在等待某个事件的完成
- 如何杀死进程呢?
首先使用指令 ps aux 来查看现阶段的全部进程
a:显示终端上的所有进程,包括其他用户的进程
u:显示进程的详细信息
x:显示没有控制终端的进程
kill 和 kill -9 这两个命令在Linux中都有杀死进程的效果,然而两命令的执行过程却大有不同,在程序中如果用错了,可能会造成莫名其妙的现象(*以下内容出自Kill区别)。
kill(不加 -* 默认kill -15)命令
系统会发送一个SIGTERM信号给对应的程序。当程序接收到该signal信号后,将会发生以下事情:
- 程序立刻停止
- 当程序释放相应资源后再停止
- 程序可能仍然继续运行
大部分程序接收到SIGTERM信号后,会先释放自己的资源,然后再停止。但是也有程序可能接收信号后,做一些其他的事情(如果程序正在等待IO,可能就不会立马做出响应),也就是说,SIGTERM有可能是会被阻塞的。
kill -9命令
系统给对应程序发送的信号是SIGKILL,即exit。exit信号不会被系统阻塞,所以kill -9能顺利杀掉进程。
总结:
在使用 kill -9 前,应该先使用 kill 指令,给目标进程一个清理善后工作的机会。如果没有,可能会留下一些不完整的文件或状态,从而影响服务的再次启动。
四、创建子进程
pid_t fork(void); 函数的作用:用于创建子进程。
- fork函数的返回值:
fork()的返回值会返回两次,一次是在父进程中,一次是在子进程中。
在父进程中返回创建的 子进程的ID
在子进程中返回 0
如何区分父进程和子进程:通过fork的返回值。
在父进程中返回-1,表示创建子进程失败,并且设置errno
- 父子进程之间的关系:
区别:
1.fork()函数的返回值不同
父进程中: >0 返回的子进程的ID
子进程中: =0
2.PCB中的一些数据
当前的进程的id pid不同
当前的进程的父进程的id ppid不同
信号集不同
共同点:
某些状态下:子进程刚被创建出来,还没有执行任何的写数据的操作
- 用户区的数据
- 文件描述符表
- 父子进程对变量是不是共享的?
实际上,更准确来说,Linux 的 fork() 使用是通过写时拷贝 (copy- on-write) 实现。写时拷贝是一种可以推迟甚至避免拷贝数据的技术。内核此时并不复制整个进程的地址空间,而是让父子进程共享同一个地址空间。只用在需要写入的时候才会复制地址空间,从而使各个进行拥有各自的地址空间。也就是说,资源的复制是在需要写入的时候才会进行,在此之前,只有以只读方式共享。
注意:fork之后父子进程共享文件,fork产生的子进程与父进程相同的文件文件描述符指向相同的文件表,引用计数增加,共享文件偏移指针。
父进程创建子进程代码如下,也可以从运行结果中看到CPU对于不同进程的分片操作。
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
int main() {
int num = 10;
// 创建子进程
pid_t pid = fork();
// 判断是父进程还是子进程
if(pid > 0) {
printf("pid : %d\n", pid);
// 如果大于0,返回的是创建的子进程的进程号,当前是父进程
printf("i am parent process, pid : %d, ppid : %d\n", getpid(), getppid());
printf("parent num : %d\n", num);
num += 10;
printf("parent num += 10 : %d\n", num);
} else if(pid == 0) {
// 当前是子进程
printf("i am child process, pid : %d, ppid : %d\n", getpid(),getppid());
printf("child num : %d\n", num);
num += 100;
printf("child num += 100 : %d\n", num);
}
// for循环
for(int i = 0; i < 3; i++) {
printf("i : %d , pid : %d\n", i , getpid());
sleep(1);
}
return 0;
}
五、GDB多进程调试
GDB默认只能跟踪一个进程,可以在 fork 函数调用之前,通过指令设置 GDB 调试工具跟踪父进程或者是子进程,默认跟踪父进程。
设置调试父进程或者子进程:set follow-fork-mode [parent(默认) | child]
设置调试模式:set detach-on-fork [on | off]
默认为 on,表示调试当前进程的时候,其它的进程继续运行;如果为off,调试当前进程的时候,其它进程被 GDB 挂起。