目录
操作系统如何管理进程?
进程的结构体:
操作系统如何加载进程?
CPU如何调度进程?
进程如何在多个队列排队:
offsetof宏:
进程标识符:
程序打印自己的pid和ppid:
杀死进程:
———————————————
getpid()和getppid()如何得到当前进程的pid/ppid:
父进程和子进程的关系:
动态进程目录:
当前目录是什么?
———————————————
fork创建进程:
创建子进程的目的?
创建子进程后一般写法思路:
fork之后,干了什么事情?
fork之后父子进程谁先运行?
fork之后,为什么会return两个返回值?
代码共享,数据独立:
———————————————
进程状态通俗理解:
运行状态 / 运行队列:
阻塞状态 / 资源的等待队列:
阻塞挂起状态:
swap分区:
———————————————
Linux下,进程的运行状态:
运行状态:
前台进程 / 后台进程:
休眠状态:
暂停状态 / 18和19信号:
僵尸状态:
父进程不存在僵尸状态:
孤儿进程和进程领养:
———————————————
排队的本质——确认优先级:
进程优先级的本质:
通过NI值修正进程优先级值PRI:
限定优先级在一定范围的原因:
进程的并行和并发:
进程切换和上下文保存:
操作系统如何管理进程?
进程是由操作系统创建的->操作系统是由C语言写的->进程需要被C语言写的操作系统管理->C语言通过结构体管理数据->进程相当于一个个结构体->结构体中的成员是进程的一个个属性。
进程的结构体:
进程的结构体被称为“进程的PCB”,当进程被创建PCB就会被创建。Linux下PCB叫做task_struct。
操作系统如何加载进程?
-
操作系统先将可执行程序从磁盘加载到内存中。
-
操作系统为加载的进程创建PCB,用于管理进程信息。
-
将新创建的PCB链接到PCB链表上(Process Table)。
CPU如何调度进程?
-
CPU通过调度算法(轮转调度、优先级调度等),找到下一个需要运行的进程==找到进程的PCB。
-
操作系统保存当前运行中的进程的上下文(寄存器、程序计数器等状态信息) 。
-
操作系统加载新进程的上下文,将其寄存器、程序计数器等信息加载到 CPU 中。
-
CPU开始执行新进程的指令。
进程如何在多个队列排队:
-
进程排队排的是进程PCB。
-
PCB之间的链接属于双向链表。
-
一般情况下,双向链表存储两个指针,一个指向前前一个节点,一个指向后一个节点。
-
PCB中特定存储一个结构体dlist存放这两个指针。
-
当进程被排到某个队列中,就创建一个这种结构体,存放该PCB在当前队列的前后指针。
-
这样PCB每被排序到一个队列,就创建一个dlist,就能实现PCB多队列排队。
offsetof宏:
-
offsetof 宏的定义通常依赖于 __builtin_offsetof 或者通过指针运算来实现。
-
其原理是,通过计算某个结构体成员的地址减去结构体首地址,得到成员相对于结构体开头的偏移量。
进程标识符:
-
操作系统中进程标识符pid(process id)会唯一标识一个进程。
-
pid在进程创建时被分配,是唯一的。当进程被释放,pid会被回收给其他新进程使用。
ps ajx | grep my_program
a: 显示所有终端的进程,包括其他用户的进程。
j: 以作业格式显示,包含会话、进程组等信息。
x: 显示没有控制终端的进程,例如守护进程。
grep 命令过滤指定名称的进程
-
每条命令都可以当作是一个进程,当调用grep过滤指定名称的进程时。他也会作为一个进程运行。
ps ajx | grep my_program | grep -v grep
//加上grep -v grep来过滤grep自身的进程。
//grep过滤器,在过滤某个关键字时,如果关键字不包含特殊符号可以不加双引号,如果包含特殊符号就需要加上双引号确保能够正确解析字符串。比如mycode可以不加双引号,my code就需要加上双引号。
程序打印自己的pid和ppid:
#include <stdio.h>
#include <sys/types.h>//调用getpid要包含这两个库
#include <unistd.h>
int main()
{
pid_t pid=getpid();//调用失败返回0
pid_t ppid=getppid();
printf("%d",pid);
}
杀死进程:
kill -9 pid
//发送9信号,杀死pid进程
———————————————
getpid()和getppid()如何得到当前进程的pid/ppid:
-
pid和ppid存放在进程的PCB中,当要得到他时需要访问PCB。
-
操作系统不希望用户直接访问PCB。
-
于是提供了“系统调用接口”,getpid和getppid得到PCB中的pid和ppid后,返回给用户。
父进程和子进程的关系:
-
子进程是由父进程创建的,创建时:子进程会以父进程为模板,继承父进程的部分数据和状态。
动态进程目录:
-
在Linux的根目录下有一个proc目录,其中动态维护当前存货的进程信息。
ls /proc
-
其中的子目录是当前存在的进程,子目录以进程pid命名。
ls /proc/pid -l//查看子目录中的文件
-
其中每个进程的目录中,有一条:exe->path,他会指向进程在磁盘中可执行程序的路径。
-
有一条:cwd->path,指向当前目录。
当前目录是什么?
-
当进程被创建,PCB会记录可执行程序当前的路径,存放在cwd中。
-
之后对当前进程的路径操作,如果不写为绝对路径,默认使用cwd路径作为相对路径。
chdir("PATH");
//在可执行程序中更改当前目录,更改后进程的PCB中的cwd也会同时修改为PATH。
-
每个进程都有当前目录==工作目录。
———————————————
fork创建进程:
pid_t fork(void)//创建进程
-
使用fork创建一个子进程,fork之后的代码会被父进程和子进程同时执行。
-
进程A创建了一个子进程B,子进程的父进程是进程A。
-
fork失败返回-1,创建成功给父进程返回子进程的pid,给子进程返回0。
创建子进程的目的?
-
让子进程协助父进程完成单进程无法完成的工作。
-
让子进程和父进程分别完成一项工作。
-
基于fork的返回值对于父子进程有区别,通过分支让父子进程执行不同的代码段。
创建子进程后一般写法思路:
pid_t id=fork();
if(id<0) return 1;//说明fork失败
else if(id==0) {}//对子进程返回0,这里写子进程的执行逻辑
else {} //写父进程的执行逻辑
fork之后,干了什么事情?
-
fork创建子进程之后,系统中会多一个PCB,就是子进程的PCB。
-
由于fork创建的子进程是直接在系统创建,所以这个进程没有自己的可执行程序。
-
所以子进程需要共享父进程加载到内存的可执行程序==共享代码和数据。
fork之后父子进程谁先运行?
-
进程创建后,在调度序列中被排序。
-
由调度器算法和PCB中的调度信息共同决定当前调度的进程。
fork之后,为什么会return两个返回值?
-
fork先找到父进程PCB,根据父进程PCB初始化子进程的PCB。
-
让子进程的PCB指向父进程的可执行程序。
-
将子进程PCB放到调度队列。
-
fork之后的代码是父子进程之间共享的,包括return。
-
当return时,父进程调度完成return一次,子进程调度完成return一次。返回两次也就是两个值。
代码共享,数据独立:
-
父子进程,共用同一份代码。
-
数据相互独立,子进程在用到某个成员时,从父进程的数据写时拷贝一份到自己的数据中。
———————————————
进程状态通俗理解:
-
进程状态就是进程PCB中的一个变量的值,这个值来自于一个枚举类型。
-
当变量的值对应枚举类型不同的成员时,代表进程处在对应的状态。
-
状态的变化,就是修改变量的值为对应枚举类型的成员。
运行状态 / 运行队列:
-
每个CPU都会维护一个运行队列。
-
进程创建PCB,并且被排序到运行队列中,他所处在的状态就是运行状态。
-
具体实现就是:进程中控制进程状态的变量被赋值为特定值,进程被链接到运行队列,PCB添加一个dlist标记队列前后节点。
阻塞状态 / 资源的等待队列:
-
当一个进程需要访问某个资源时,如果资源尚未准备好,进程无法继续执行。
-
资源的状态结构体中会自己维护一个队列,这个队列用于存储那些:因资源当前不可用而被阻塞的进程。
-
当前正在运行的进程如果发现需要的资源不可用,那么它会从运行队列中被移出,并放入所需资源的等待队列。并且它的状态将被修改为“阻塞”。
-
操作系统会调度其他进程运行,直到阻塞的进程所需的资源变得可用。资源的状态结构体会通知在等待队列中的进程,并将其从阻塞状态恢复到就绪状态。这些进程会被重新放回运行队队列。
阻塞挂起状态:
-
当操作系统检测到内存严重不足时,它可能会将那些处于等待状态且短时间内不会被执行的进程 (PCB+可执行程序)从内存中转移到磁盘,从而节省出一部分内存。这一过程称为“挂起”。
-
处于资源等待队列的进程,由于处于阻塞状态,短时间也不会被执行,不如直接挪动到磁盘来节省出空间。
-
将进程从内存移至磁盘的操作会带来性能上的开销,因为磁盘I/O操作比内存访问要慢得多。但在内存极度紧张的情况下,这种代价是必要的,以防止操作系统因为内存耗尽而崩溃。
-
swap分区,不建议设置很大,可能导致频繁的内存和磁盘的交互,导致操作系统变慢。一般设置到和内存一样大或者两倍即可。
swap分区:
swap分区存储的就是被挂起的进程。
swap的读写速度远不及物理内存。如果系统频繁使用swap,整体性能会明显下降,可能导致系统响应变慢,甚至出现“卡死”的现象。
如果swap分区过大,可能会诱导系统频繁将数据交换到磁盘上,导致更多的I/O操作,反而降低了系统的整体性能。
一般建议swap分区的大小设置为物理内存的1倍或更小。
———————————————
Linux下,进程的运行状态:
运行状态:
R (Running or runnable):进程正在运行或在运行队列中等待运行。
前台进程 / 后台进程:
当stat显示的进程状态后面有+,比如R+,S+。代表这个进程为前台进程。前台进程通常是由用户直接控制的,比如通过终端启动的程序。比如有一个可执行程序mycode,./mycod直接运行就是以前台进程方式运行的。
如果stat显示的进程状态没有+,代表这个进程为后台进程。以后台进程的方式运行某个进程,需要在进程后面加取地址符号(&)。例如:./mycode &
休眠状态:
S (Sleeping):进程处于休眠状态,即它正在等待某个事件(如I/O操作完成)。这类进程可以被唤醒。
D (disk sleep):不可中断的休眠状态。进程通常在等待硬件设备时处于此状态,不会处理任何信号,不可被中断,直到进程调用完成。处于D状态的进程过多,系统资源被大量消耗,可能导致系统性能下降或者崩溃。
暂停状态 / 18和19信号:
T (Stopped):进程已经停止运行。可能是因为收到了SIGSTOP信号或者被调试器暂停。
kill -18 pid //暂停进程 -SIGCONT
kill -19 pid //继续进程 -SIGSTOP
僵尸状态:
Z (Zombie):僵尸进程,进程已终止但尚未被其父进程回收。此状态下的进程只剩下一个PCB,系统资源已经被释放。
-
当进程进入僵尸状态时,可执行程序和大部分资源已经被释放,但PCB仍然保留。
-
当程序终止时会return一个值,会由操作系统写入到进程的PCB中(exit_code)。
-
之后父进程在读取子进程的退出状态的同时适释放PCB == 将进程状态从 ‘Z’改为 ‘X’。
-
再判断是否需要做出下一步动作,例如重新启动子进程、记录日志或采取其他相应的措施。
-
僵尸进程本身并不消耗大量资源,但如果大量僵尸进程积累,可能会导致系统进程表耗尽,影响系统的进程管理。
父进程不存在僵尸状态:
-
僵尸状态是专门针对子进程的,每个父进程也有他的父进程。
-
当一个子进程的父进程终止,父进程的父进程就会处理他的退出状态并释放PCB。
孤儿进程和进程领养:
-
当子进程的父进程先被终止,那么子进程就会变为孤儿进程。
-
孤儿进程终止变为僵尸进程后,没有父进程读取其退出状态,并为其释放PCB,会导致内存泄漏。
-
孤儿进程会被init进程(Linux系统中通常是systemd)收养并处理。当这些被收养的子进程终止时,init进程会读取它们的退出状态,防止它们成为僵尸进程。
———————————————
排队的本质——确认优先级:
-
进程排队的本质源于资源的有限性与进程需求的无限性之间的矛盾。
-
操作系统通过调度和排队机制管理这个矛盾,以确保资源被有效利用,同时为所有进程提供合理的执行机会。
进程优先级的本质:
-
进程优先级的本质,是一个存在于PCB中int变量。
-
变量的值越小,优先级越大。
-
linux中,进程优先级的范围为:60~99,默认进程的初始优先级为80。
ps -l //可以查看进程优先级,PRI字段就是优先级数值
通过NI值修正进程优先级值PRI:
-
linux下,不允许直接修改进程优先级,需要通过nice值=>NI值,来修正优先级数值。
-
PRI(new) = PRI(old) + NI
top//启动任务管理器 -> r//重设nice值 -> 输入pid -> 新的nice值。
-
普通用户可以调低进程的优先级,但是不能调高,使用超级用户可以调高进程优先级
-
由于PRI范围为60~99,基于pri初始值为80,所以调整nice值的范围为-20~19。
限定优先级在一定范围的原因:
-
某些进程优先级过低,可能长时间无法被调度。导致进程饥饿。
-
通过将进程优先级限制在一定的范围内,可以确保即使是最低优先级的进程,也能在合理的时间内获得CPU资源。
-
还可以帮助操作系统在资源分配中实现公平性和有效性,避免极端优先级设置导致某进程一直被调用。
进程的并行和并发:
-
并行:多进程分别在多CPU下,同时运行。
-
并发:多进程在单CPU下,高速切换运行。
进程切换和上下文保存:
-
一个CPU通常只有一套寄存器硬件。所以这套寄存器对不同进程都是共享。
-
当CPU切换进程时,会将寄存器中的内容保存到进程的PCB中,也可以认为是保存在内存中(通常是堆栈)。
-
当切换要调度的新进程后,不会直接清空存储在寄存器中上一个进程的数据,而是直接将新进程的数据覆盖到寄存器上。