5.1 进程
进程的诞生与概念
最开始的计算机一次只能执行一个任务, 只有当前任务完成时, 监控器才会分配第二个任务给CPU执行. 但是这样效率太低了, 比如我要跑一个大模型训练, 但是我又想听歌, 我就只能等大模型跑完才能听了.
因此提出了分时(time-sharing)操作系统, 使用多任务并行. 为了更好地管理这些任务, 这些任务被抽象成进程.
进程的执行状态不断更新, 需要不断切换处理器上运行的进程, 操作系统需要对进程进行调度.
进程的状态
进程应该至少有这五种状态
新生状态(new) 进程刚被创建
就绪状态(ready) 进程可以运行, 但还没有被调度
运行状态(running) 进程正在在处理器上运行
终止状态(terminated) 进程完成了执行
阻塞状态(blocked) 进程进入等待状态, 短时间不再运行
一个进程被创建后, 进入新生状态. 经过初始化后就进入就绪状态, 等操作系统调度了. 操作系统调度后, 进入运行状态, 当需要等待外部事件(如IO)时, 就会进入阻塞状态, 外部事件完成后再次进入就绪状态; 处于运行状态时, 如果自己的时间片用完了, 也会进入就绪状态, 等待下一次调度. 当进程退出后, 进入终止状态.
Linux中进程退出后不要完全释放资源, 而是等待父进程来查询原因等信息. 如果这个子进程是个孤儿, 那就会等待操作系统给它分配一个父进程, 然后再完全释放资源.
这里阻塞态在Linux中还可以进一步细分成轻度睡眠, 中度睡眠, 深度睡眠等.
数据结构
进程相关的数据结构: PCB
PCB, Process Control Block, 里面存放进程的各种相关信息, 如进程的标识符, 内存, 打开的文件等.
当进程通过中断, 系统调用进入内核态时, 上下文会保存在PCB中, 进入阻塞状态. 被调度时, 从PCB中取出上下文并恢复.
基本操作
进程创建: fork(spawn, vfork, clone)
进程执行: exec
进程间同步: wait
进程退出: exit/abort
fork
为调用进程创建一个一模一样的新进程, 调用进程为父进程, 新进程为子进程.
fork后, 在Linux中是写时拷贝机制, 父进程和子进程共享一块内存空间, 父进程和子进程的虚拟页都映射相同的物理页, 其中一个进程进行修改时再重映射.
一般情况下, 这两个进程并行执行, 互不干扰, 除非使用特定的接口.
特定的接口
#include <fcntl.h>
#include <stdio.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
char str[11] = {0};
int main(int argc, char* argv[]) {
int fd = open("test.txt", O_RDWR);
if (fork() == 0) {
ssize_t cnt = read(fd, str, 10);
printf("Child process: %s\n", str);
} else {
ssize_t cnt = read(fd, str, 10);
printf("Parent process: %s\n", str);
}
close(fd);
return 0;
}
这里的文件内容是abcdefghijklmnopqrst, 程序运行的结果是
Parent process: abcdefghij
Child process: klmnopqrst
进程会维护一张已打开文件的文件描述符(File Descriptor, fd).
struct task_struct {
unsigned int __state;
void *stack;
struct list_head tasks;
struct mm_struct *mm;
/* Filesystem information: */
struct fs_struct *fs;
/* Open file information: */
struct files_struct *files;
};
可以发现, 是files_struct指针, 因此两个进程都指向同一个文件描述符.
windows的进程创建CreateProcess
比fork的逻辑直观, 但是实操非常复杂.
进程树与进程组
fork为进程之间建立了父进程和子进程的关系, 进程之间建立了树形结构.
多个进程可以属于同一个进程组, 子进程默认与父进程属于同一个进程组. 可以向同一个进程组的所有进程发送信号, 主要用于shell程序中.
优缺点
- 优点
接口非常简洁.
将进程的创建与运行解耦, 提高了灵活度
刻画了进程之间的内在关系
- 缺点
完全拷贝过于粗暴(不如clone)
性能差, 可扩展性差(不如vfork, spawn)
不可组合性
在Linux中, vfork不被推荐使用. Linux实现fork时, 使用了写时拷贝的机制, fork的开销仅有为新进程创建一个唯一的标识符和建立虚拟页与物理页的映射, 与vfork的差别只在于vfork没有建立映射. 但是vfork存在共享地址空间的安全问题.
exec
exec需要为进程指定可执行文件路径和参数才能执行.
exec在fork之后调用, 在载入可执行文件后会重置地址空间.
waitpid
waitpid用于进程间监控及同步.