一.进程
1.什么是进程
一个进程创建,他会生成几块:
- 代码段:进程执行的程序代码
- 数据段:全局变量,静态变量,在进程生命周期中是动态可变的
- 堆:动态分配的内存区域,malloc、calloc、realloc等函数进行开辟
- 栈:用于存储局部变量、函数参数和返回地址
- PCB进程控制块:包含进程ID(PID),进程状态,程序计数器(吓一条指令的位置),进程优先级等等
进程是资源分配和调度的基本单位
进程是资源分配的最小单位
线程是最小的调度单位
2.进程中的三态模型
- 阻塞态:这个进程需要相应的资源,而现在需要等待相应的资源,那么这个进程就进入到阻塞态,CPU会将这个进程给拿下CPU,去执行就绪在就绪队列中的第一个进程。
- 就绪态:一个进程需要运行,并且他所有的需求都满足了,就等待CPU去执行,那么这个进程会在这个就绪队列中进行排队,等待CPU的调度执行
- 运行态:这个进程在CPU上执行
挂起
- 挂起:在挂起这个进程时,会将这些运行在内存上的内容给写入到磁盘中或交换区中,然后释放内存,在记录上下文(记录上下文的作用就是,为了让你回到在你挂起这个进程前那个状态,和玩游戏存档的意思差不多)
- 唤醒:将刚刚写入到磁盘的内容,又写入到内存中,到达就绪态,等待操作系统的调度
进程详细的五态
对于3态模型,只多了创建态和终止态。
3.进程的控制
如何实现的原子性?
在原语时,执行关中断指令,将中断关闭掉,这样CPU就不会中断这个原语的执行,在执行完成原语后,执行开中断指令,将中断打开。这样就实现了原语的原子性,在执行原语的过程中就不会被中断。
4.调度的基本概念
为什么需要了解如何进行调度?
当有一堆任务需要处理,但由于资源有限,这些事情没有同时处理。这就需要确定某种规则来决定处理这些任务的顺序,这就是”调度“研究”的问题
能否进行调度切换进程的情况:
进行切换是有代价的,如果过度频繁进行调度、切换,会使系统的效率降低。
调度算法:
1.时间片轮转算法
给每个进入到就绪队列中的进程分配一个时间片,这个时间片的大小需要写程序的我们来决定,不能太短,也不能太长。然后每个程序在CPU上执行的时间就是时间片的大小,如果执行完成可以先退出然后让出CPU,如果没有在时间片规定的时间完成,那么也要让出CPU让就绪队列中下一个进程进行执行,然后让出CPU后又到就绪队列中进行排队,等待下一次的CPU调度执行。
2.优先级调度算法
优先级调度算法分为:
- 静态:进程在创建时就确定了优先级,这个进程在生命周期中优先级不会改变
- 动态:在创建时确定了一个优先级,在生命周期的过程中,操作系统会通过规定进行对应的优先级改变
3.多级反馈队列调度算法
- 特点:进程可以在不同优先级队列之间移动。新进程进入高优先级队列,如果未能在规定时间完成则降级。
- 优点:兼顾响应时间和吞吐量,适应性强。
- 缺点:配置参数(如队列数量、时间片等)复杂。
4.短作业优先
- 特点:优先调度运行时间最短的进程。
- 优点:可以最小化平均等待时间。
- 缺点:难以准确预测每个进程的运行时间;可能导致长作业得不到及时处理(即“饥饿”)。
5.最短剩余时间优先
- 特点:是SJF的抢占式版本。每次调度时选择剩余执行时间最短的进程。
- 优点:可以进一步减少平均等待时间。
- 缺点:和SJF一样,难以预测执行时间,并且会导致频繁的上下文切换。
对比短作业优先就是,假如P1进程需要执行时间是10s,P2执行时间是20S,现在执行P1已近5s了,来了一个新进程P3执行是减是3S,那么CPU会将P1给拿下放入到就绪队列中,来执行P3。短作业优先就是P1执行完成了才执行P3
6.先来先服务
- 特点:按进程到达就绪队列的顺序进行调度,先到达的进程先调度。
- 优点:简单易实现。
- 缺点:可能导致“等待时间长”的问题(即“队头阻塞”)。
5.进程拷贝
进程拷贝时,他们会共享物理内存页面,但是这些内存页面现在是只读的。
其中一个进程需要堆这个内存页面进行写入时,那么就会创建该内存页的副本,然后给该进程附上可写的权限。
这样可以节省系统资源,防止创建一个子进程就立刻复制整个地址空间,减少不必要的空间。
6.创建进程
fork函数
#include <sys/types.h> #include <unistd.h> pid_t fork(void);
fork函数创建子进程的关键点:
- 共享资源:子进程继承了父进程的大部分资源,包括打开的文件描述符、环境变量、优先级、控制终端等。
- 独立地址空间:虽然子进程最初共享父进程的地址空间,但它们有独立的地址空间,修改内存不会影响对方。
- 执行路径:
fork()
返回值用于区分父子进程。子进程返回 0,父进程返回子进程的 PID。- 进程调度:子进程和父进程是并行执行的,具体谁先执行由操作系统调度决定。
例子:
#include <stdio.h> #include <unistd.h> #include <stdlib.h> int main() { int n; //创建子进程 pid_t child_pid = fork(); if (child_pid < 0) { perror("fork"); exit(1); } //当child_pid为04时说明是子进程 if (child_pid == 0) { n = 6; while (n > 0) { printf("child self = %d, parent = %d\n", getpid(), getppid()); n--; } } //不为0说明是父进程 else { n = 3; while (n > 0) { printf("parent self = %d, parent = %d\n", getpid(), getppid()); n--; } } return 0; }
你会发现每次输出的结果都是不一样的。
孤儿进程
那么像这种情况就是父进程先执行完成了,子进程还在执行,但是没有父进程为子进程收尸(回收资源),那么这个子进程就叫做孤儿进程,那么谁会为他收尸呢,也就是我们系统最初的那个进程0号进程。也叫做init进程(或者现代系统的systemd)接管,我们也可以称它为孤儿院。
僵尸进程
当一个进程终止后,其父进程还没有读取它的退出状态,这个进程就被称为僵尸进程。
如何处理我们调用wait()或者waitpid()来读取子进程的状态,来等待子进程的退出状态,这样就可以为子进程进行收尸。
wait和waitpid()
pid_t wait(int *status); pid_t waitpid(pid_t pid, int *status, int options);
wait:
- 功能:
wait
阻塞调用进程,直到其某个子进程终止。如果一个子进程已经终止,那么wait
会立即返回。- 参数:
status
是一个指向整数的指针,用于存储子进程的退出状态。如果status
是NULL
,则退出状态会被丢弃。- 返回值:返回终止的子进程的进程 ID。如果调用出错(例如,没有子进程),则返回 -1 并设置
errno
。waitpid:
- 功能:
waitpid
提供了比wait
更细粒度的控制,允许父进程等待特定的子进程终止,或者以非阻塞方式等待。- 参数:
pid
:指定要等待的子进程 ID。如果pid
为 -1,则等待任何子进程(等同于wait
)。status
:同wait
,用于存储子进程的退出状态。options
:提供额外的选项控制,如WNOHANG
(非阻塞方式)和WUNTRACED
(也会返回已停止的子进程)。- 返回值:成功时返回子进程的 PID,失败时返回 -1 并设置
errno
。如何获取退出状态,可以在Linux系统中的man手册查看。
接着上面那个例子来进行优化:
#include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <wait.h> int main() { int n; int state; //创建子进程 //在子进程中child_pid 为0 //父进程为fork的返回值 pid_t child_pid = fork(); if (child_pid < 0) { perror("fork"); exit(1); } //当child_pid为04时说明是子进程 if (child_pid == 0) { n = 6; while (n > 0) { printf("child self = %d, parent = %d\n", getpid(), getppid()); n--; } } //不为0说明是父进程 else { n = 3; while (n > 0) { printf("parent self = %d, parent = %d\n", getpid(), getppid()); n--; } int ret = wait(&state); if (ret < 0) { perror("wait error"); exit(1); } //看通过stata判断子进程是否正常退出 if (WIFEXITED(state)) { //正常退出输出子进程PID和退出状态 printf("child process %d is sucess exit %d\n", child_pid, WEXITSTATUS(state)); } else { //不正常输出子进程PID printf("child process %d is not sucess exit\n", child_pid); } } return 0; }