进程基础(命令的基石)

目录

函数 指令 /proc目录

/proc目录

关于进程的命令

关于进程的函数(C)

关于进程函数的头文件

拓展:

exec函数

wait函数

waitpid函数:

kill:(在信号栏目重点介绍)

进程入门:第一座大山

重新审视命令

PCB进程

PCB的访问

偏移量

怎么访问PCB成员

linux中进程的管理

进程的属性

计算机进行进程的步骤

运行队列->进程调度与执行

等待对列->进程阻塞与唤醒

等待对列->进程的挂起 换入换出

父子进程中...

进程的优先级 大O(1)调度算法

拓展:底层实现

2.大O(1)调度算法

其他概念 进程切换

进程地址空间 (一谈)

引子:

地址空间

区域划分

进程地址空间

虚拟地址和物理地址

为什么要有虚拟地址

虚拟内存是虚拟的不是实际开辟的空间

页表

页表中:页表项

页帧(Page Frame)和页框(Page Frame)

页结构体

虚拟页号和物理页号

思考(重要):

有没有可能虚拟地址映射的物理地址还没有被分配空间

子进程的页表和父进程相同吗:

子进程如何修改父进程复制过来的页表对应的数据,不是已经设置成只读了吗:

父子进程写时拷贝的实现**

如果虚拟地址映射的物理地址没有数据。操作系统是怎么从磁盘中加载数据到内存的呢

运行进程的 细节(进程控制)

进程创建

fork()

fork的实现原理(写实拷贝)

父子进程的运行顺序

进程终止

退出码

常见的退出方法

进程等待

是什么

为什么

wait()与waitpid()

status:

options:

实验

1.子进程是怎么变为僵尸进程的

2.僵尸程序的回收

3.wait函数接受的是任意一个子进程id,也就是说我们可以实现一个父进程有多个子进程的回收

4.wait()会阻塞父进程

5.waitpid的使用

6.waitpid中的status

7.waitpid中的options

思考:

进程替换

是什么

exec函数族

为什么

实验:

思考:


函数 指令 /proc目录

/proc目录

在Linux系统中,/proc目录是一个虚拟文件系统,它不是实际存储在磁盘上的文件系统,而是一个由内核提供的、以文件系统的方式组织的内存中的信息。/proc目录下的文件和目录反映了内核的当前运行状态,包括系统参数、进程信息、系统属性等。

软连接:cwd(current work dir) exe(可执行程序)

  1. 系统信息:例如/proc/version包含了Linux内核的版本信息,/proc/cpuinfo包含了CPU的详细信息。

  2. 进程信息:每个进程都有一个以其PID(进程ID)命名的目录,例如/proc/1234,其中1234是进程ID。在这个目录下,可以找到关于该进程的各种信息,如/proc/1234/status包含了进程的状态信息。

  3. 环境变量/proc/[pid]/environ文件包含了进程的环境变量。

  4. 文件描述符:进程的每个文件描述符都有一个对应的文件,例如/proc/[pid]/fd/[fdnum],其中[fdnum]是文件描述符的数字。

  5. 信号处理/proc/[pid]/status文件中包含了的信号处理信息。

  6. IO统计:例如/proc/[pid]/io文件提供了进程的IO统计信息。

  7. mount表/proc/mounts包含了系统的mount表,显示了所有已挂载的文件系统。

  8. 路由表/proc/net/route/proc/net/if_inet6包含了系统的路由表信息。

/proc目录是系统管理员和开发者获取系统信息和调试系统的重要工具。由于/proc是内存中的虚拟文件系统,因此它的内容在系统重启后会丢失

proc下的一些子目录:

  1. /proc/cpuinfo:包含有关CPU的信息,如型号、时钟速度、缓存大小等。

  2. /proc/diskinfo:包含磁盘设备的信息。

  3. /proc/meminfo:包含有关内存使用情况的信息,如可用内存、交换空间等。

  4. /proc/stat:包含有关系统活动的统计信息,如进程数、CPU使用情况等。

  5. /proc/kcore:提供物理内存映射,类似于一个虚拟的磁盘文件系统。

关于进程的命令

  1. psps命令用于显示当前系统上运行的进程的快照。常用的选项有:

    • ps aux:显示所有进程的详细信息,包括进程ID(PID)、CPU占用率、内存占用等。

    • ps ajx:显示进程大致信息,比aux简介。

  2. toptop命令可以实时显示当前系统上运行的进程的活动情况和资源占用情况,通过不断更新显示内容。按下q键退出。

  3. pgreppgrep命令用于根据进程的名称或其他属性查找并显示相关的进程ID。例如,pgrep firefox将显示与"firefox"相关的所有进程ID。

  4. pidofpidof命令根据进程的名称查找并显示与之关联的进程ID。例如,pidof firefox将显示与"firefox"关联的进程ID。

  5. pstreepstree命令以树状结构显示进程的层次关系,包括父进程和子进程。

  6. killkill命令用于发送信号给指定的进程,从而管理进程。常用的信号包括终止进程(SIGTERM),强制终止进程(SIGKILL)等。

  7. killallkillall命令可以根据进程的名称发送信号给一组进程,而不仅仅是单个进程。

  8. fg:将当前进程切换到前台执行。

  9. bg:将当前进程切换到后台执行。

  10. renice:调整进程的优先级。

  11. nice:以低优先级启动一个新进程。

  12. mpstat:显示系统中各个进程的性能信息。

  13. vmstat:显示系统中的虚拟内存状态和系统吞吐量等信息。

关于进程的函数(C)

  1. fork:创建一个子进程。这个函数会返回子进程ID或父进程ID,取决于你是从父进程还是子进程调用它。

  2. fork:与fork函数类似,但它是用来一个新线程的。

  3. getpid,getppid:得到进程pid与ppid。

  4. exec:加载并执行一个新的进程。这个函数可以用来替换当前正在运行的进程,它可以接受一个可执行文件路径作为参数。 execl:与exec函数类似,但它可以接受一个命令行参数列表作为参数。

  5. execlp:与上面那个execl函数类似,但它会自动将参数列表转换为适当的格式。

  6. wait:等待任意一个子进程完成。这个函数会阻塞当前进程,直到子进程完成或者超时。

  7. waitpid:等待一个子进程完成。这个函数与wait函数类似,可以接受一个进程ID作为参数,以便等待特定的子进程完成。

  8. exit:退出当前进程。这个函数可以接受一个退出码作为参数,表示当前进程的退出状态。

  9. _exit_exit() 函数的效果与在末尾写上 exit(0) 是相同的,都会导致程序立即终止。并传递一个整数退出码给操作系统,这个退出码通常用于指示进程终止的原因,可以被父进程通过 wait() 或者 waitpid() 函数获取。

exit函数与return 关键字的区别:

  1. 类型:exit是库函数,而return是关键字。

  2. 位置:当发生return时,控制权直接从所在函数返回调用处;而使用exit时,会立即终止程序,并且不会执行后续的代码。exit函数在结束时不会执行任何清理操作,但是会冲刷缓冲,关闭流

  3. 使用方式:return在任何时候都可以使用,而exit函数通常用于特定的错误代码。

_exit函数与exit函数的区别:

  1. 头文件exit() 头文件: _exit() 头文件

  2. 是否会调用用户定义的清理操作:两者都不会调用。

  3. 是否会冲刷缓冲,关闭流_exit() 不会执行任何清理操作,也不会执行任何未完成的系统调用。

    因此,使用 _exit() 会比使用 exit() 更快速地退出程序,但是它不会提供相同的资源释放和清理功能。

关于进程函数的头文件

  • unistd.h:提供了许多用于进程管理的函数,如fork()exec()等。

  • sys/wait.h:提供了与进程相关的系统调用和函数,如wait()waitpid()等。

  • sys/types.h:提供了数据类型定义,如pid_tint等,用于进程相关的函数参数。

  • sys/ipc.h:提供了进程间通信(IPC)的相关函数,如管道(pipe)、消息队列(msgqueue)等。

  • pthread.h:提供了线程相关的函数和数据类型,如线程创建、同步、互斥等。

拓展

exec函数

exec()函数族包含多个相关的函数,如execvp()execve()

这些函数的作用是将当前进程的内存空间替换为一个新的可执行文件的映像,并开始执行新的程序。通过exec()函数,可以在同一个进程中加载并运行不同的可执行文件或脚本。

一般而言,exec()函数通过指定新程序的路径或文件名来执行。它会将当前进程的代码段、数据段、堆栈等内容替换为新程序的内容,然后开始执行新程序。因此,从操作系统的角度来看,原进程的身份和资源都已经转变为新程序。

exec()函数在调用成功时将不会返回,因为当前进程已经被替换为新程序。如果返回了,通常代表执行失败。在这种情况下,可使用返回值来判断错误的原因

wait函数
  • 原型:int wait(int *status);

  • 作用:等待任意一个子进程结束,并将子进程的退出状态存储在status指针指向的变量中。函数返回子进程的进程ID,如果调用进程没有子进程,则返回-1。

waitpid函数:
  • 原型:int waitpid(pid_t pid, int *status, int options);

  • 作用:等待指定进程ID(pid)的子进程结束,并将子进程的退出状态存储在status指针指向的变量中。options参数可以用来设置等待子进程的方式,如等待特定的子进程(WNOHANG)、等待所有子进程(WUNTRACED)等。返回子进程的进程ID,如果调用进程没有对应的子进程或调用出错,则返回-1。

kill:(在信号栏目重点介绍)

kill命令在Linux中用于向进程发送信号,可以用于控制和管理进程。它的基本语法是:

kill [OPTIONS] 

以下是一些常用的kill命令的参数:

  • -l:列出可用的信号列表。

  • :指定要发送的信号编号或信号名称。

常用的信号包括:

  • SIGTERM(信号编号15):默认信号,用于终止进程。通常用于优雅地终止进程。

  • SIGKILL(信号编号9):用于立即强制终止进程,进程无法捕获或忽略该信号。

  • SIGHUP(信号编号1):终端挂起或断开连接时发送该信号。

  • SIGINT(信号编号2):由终端发送的中断信号,通常由Ctrl+C生成。

  • SIGSTOP(信号编号19):用于暂停进程的执行。

  • SIGCONT(信号编号18):用于恢复已暂停的进程执行。

例如,要终止进程ID为1234的进程,可以使用以下命令:

kill 1234

要强制终止进程,可以使用-9选项或-SIGKILL指定信号:

kill -9 1234
或
kill -SIGKILL 1234

要列出可用的信号列表,可以使用-l选项:

kill -l

进程入门:第一座大山

在计算机系统中,进程(Process)是指正在运行的程序的实例。它是计算机执行任务的基本单位,包含了程序代码、数据、执行环境和系统资源的集合。

每个进程都有自己独立的内存空间、寄存器、堆栈和文件描述符等系统资源。进程通过操作系统的调度器来分配处理器时间,以便在CPU上执行指令。多个进程可以在同一个系统中同时运行,各自独立地进行计算和执行任务。

进程的创建通常通过系统调用(如fork()exec())或程序的启动来完成。每个进程有一个唯一的进程标识符(Process ID,PID),用于识别和管理进程。进程间可以通过进程间通信(IPC)机制进行通信和共享数据。

一个进程可以包含多个线程(Thread),每个线程都在同一个进程的上下文中执行,并共享同一份内存地址空间。线程可以共享进程的资源,如内存、文件和打开的套接字,因此线程之间的通信和数据交换更为方便。

重新审视命令

在命令行参数以及环境变量中,我们知道了命令是怎么实现基本的运行的,

在进程的父子进程中,知道基本所有的用户执行的指令都是bash的子进程

但是,有一些命令比如 a=1 echo $a 能够输出a的值为1,但是a是本地变量,不会随着main函数中的env参数传递,这是怎么回事呢?

命令的分类:

  1. 常规命令(External Commands): 位于系统的/bin/sbin/usr/bin/usr/sbin等目录中。 通过创建子进程完成的。

  2. 内建命令(Built-in Commands): 内建命令是shell自身的一部分,不需要调用shell外的程序即可执行。这些命令直接嵌入在shell解释器中。内建命令可能包括cdpwdechohistoryclear等。内建命令通常用于执行简单的任务,如文件操作、环境管理、历史记录等。

PCB进程

PCB(Process Control Block)进程控制块是进程在操作系统中的数据结构,用于存储进程状态、程序计数器、寄存器值等信息。每个进程都有一个与之对应的PCB,操作系统利用PCB来管理和调度进程。PCB由操作系统形成,代码数据由用户来提供。

管理进程的本质就是管理PCB结构体

在PCB中存储的信息包括(进程属性的集合):

  1. 进程ID:唯一标识一个进程的ID,用于区分不同的进程。

  2. 进程状态:表示进程当前所处的状态,如运行、等待、阻塞等。

  3. 程序计数器:存储CPU下一条将要执行的指令的地址,用于实现进程的切换和恢复执行。

  4. 寄存器值(Registers):存储进程在切换时所用的寄存器值,用于在恢复执行时还原进程的状态。

  5. 进程优先级:表示进程在调度时的优先级,用于确定进程在CPU上的执行顺序。

  6. 进程映像(Process Image):包含了进程的代码、数据和栈等信息。

  7. PCB本身的信息:包括PC在内存中的地址、PCB的大小等。

  8. 下一个PCB的指针:操作系统管理进程就是对单链表的增删查改。注意可能是多个指针,PCB在系统中以多叉树的形式链接,内部会形成各种链表,队列,栈等等链式数据结构。

所以要创建进程时要做两件事(简化):创建PCB,把进程的代码和数据加载到内存中

PCB的访问

PCB是一个大型的结构体,

PCB的访问实现原理可以分为以下几个步骤:

  1. 获取PCB指针:当一个进程需要访问自己的PCB时,可以使用操作系统提供的系统调用来获取当前进程的PCB指针。例如,在Linux中,可以使用getpid()系统调用获取当前进程的进程ID(PID),然后使用/proc文件系统中的特定文件来访问该进程的PCB信息。

  2. 访问PCB:当获取到PCB指针后,进程可以访问PCB中的信息。例如,可以使用指针操作来读取或修改PCB中的CPU寄存器值、内存映射信息等。

  3. 同步和互斥:在多进程环境下,需要使用同步和互斥机制来保证进程之间的协作关系。例如,可以使用互斥锁来防止多个进程同时访问共享资源,使用信号量来控制进程的执行顺序等。

  4. 访问权限控制:直接访问进程的PCB通常是操作系统内核的特权操作,只有内核或具有特权的进程(如超级用户)才能够进行。这是为了确保系统安全和进程隔离性。普通进程只能通过操作系统提供的接口或系统调用来访问和操作自己的PCB信息,而不能直接访问其他进程的PCB。

偏移量

在一些操作系统中,为了访问PCB中的不同字段,可能需要获得PCB指针到头部的偏移量。这是因为PCB结构中的各个字段并不一定是连续存储的,可能存在填充字节或对齐需求等。

通过获得PCB指针到头部的偏移量,可以根据字段的偏移量直接计算字段的地址。这样,就可以访问PCB中的各个字段,而无需知道具体的内存地址。

对于某些操作系统,提供了一些宏和函数来简化获取PCB指针到头部的偏移量的操作。例如,在Linux的内核开发中,可以使用offsetof()宏来获取某个字段的偏移量。同时,也可以使用container_of()宏根据字段的指针和偏移量计算PCB的地址。

offsetof()宏的实现原理:

#define offsetof(type, member) ((size_t) &((type *)0)->member)

type是结构体类型,member是结构体中的字段名。

怎么访问PCB成员
  1. 通过指针访问:如果我们有一个指向结构体的指针,我们可以使用指针算术来访问成员。例如,如果我们有一个struct PCB类型的指针pcb,那么我们可以通过以下方式访问member成员:

struct PCB *pcb = /* 获取PCB的指针 */;
int value = (*pcb).member; // 或者 pcb->member;
  1. 通过指针加偏移量访问:我们也可以直接通过指加上偏移量的方式来访问成员。例如:

struct PCB *pcb = /* 获取PCB的指针 */;
int value = (*(char **)pcb + offsetof(struct PCB, member))[0];
int value = (pcb *)(*(char **)pcb + offsetof(struct PCB, member))->member;

在这个例子中,我们首先将pcb指针强制转换为char *类型,然后加上offsetof(struct PCB, member)偏移量,最后通过[0]来获取member成员的值。这种方法在某些情况下可能更高效,因为它避免了额外的 dereferencing(指针操作)。

  1. 通过联合体访问:如果结构体中的成员是通过联合体实现的,那么我们可以使用联合体的特性来访问成员。例如:

struct Union {
    int i;
    char s;
};
​
struct Union *u = /* 获取Union的指针 */;
int value = u->i; // 访问整数成员

在这种情况下,我们通过->操作符直接访问i成员,因为它是联合体Union中第一个定义的,所以它的偏移量为0。

linux中进程的管理

linux 中的PCB 叫做 task_struct,是PCB的一种

task_struct是内核的一部分,它包含了进程控制块(Process Control Block,PCB)和系统调用堆栈信息。

task_struct中的各个字段和成员变量用于表示进程的不同方面,如进程id(pid,ppid)、进程状态(状态码)、优先级、程序计数器(pc)、内存管理信息(内存指针)、上下文数据、I/O状态信息、记账信息(使用时钟数总和、时间限制等)、资源使用情况、文件描述符、网络连接等。

通过task_struct,内核可以跟踪进程的状态和执行路径,并根据需要执行各种操作,如创建、调度、终止和回收进程。

task_struct提供了与进程相关的系统调用接口,允许用户空间程序与内核进行交互和通信。

进程管理

在linux内核中,task_struct采用双向链表的方式组织,同时各个节点由队列,进程树等数据结构来组织,以实现等待队列等等

进程的属性

可以在proc目录中查看每一个进程的信息

一些常见的属性:ps ps aux ps ajx查看

  1. 进程ID(PID):每个进程都有一个唯一的标识符,用于标识进程。

  2. 父进程ID(PPID):父进程ID是该进程的父进程的进程ID。一般是bash

  3. 进程组ID(PGID):进程组ID是一个整数,它标识了该进程所属的进程组。

  4. 进程优先级Priority(PRI):进程优先级是一个数值,它决定了进程在系统中的优先级。优先级数值越低,进程获得CPU的时间片越多。

  5. 进程的修正优先级Nice value(NI):NI值表示进程的"好人度"或"友好度",用于影响进程在系统中占用CPU时间的优先级。通过nicerenice来设置和调整,也可以通过编程接口如setpriority()系统调用在程序中设置。

  6. 进程状态(STAT):进程状态表示进程当前所处的状态,如运行、等待、阻塞等。

  7. 进程资源使用情况:进程资源使用情况包括进程占用的内存(MEM)、CPU时间(CPU)、磁盘IO等资源的使用情况。PRI

  8. 进程打开的文件描述符(File Descriptors):进程打开的文件描述符是一个整数,用于标识进程打开的文件或套接字。

  9. 进程创建时间(Creation Time):进程创建时间表示进程被创建的时间。

  10. 进程执行时间(Exit Time):进程执行时间表示进程执行完毕的时间。

  11. 进程命令行(Command Line):进程命令行表示启动该进程时传入的命令行参数。

进程状态(STAT)

  1. 新建(New):当一个进程被创建但还未被系统调度执行时,它处于新建状态。在这个状态下,操作系统正在为该进程分配所需的资源。

  2. 就绪(Ready):一旦新建进程获得了所有必需的资源,它就会进入就绪状态。在就绪状态下,进程已准备好被调度执行,但还未被分配R到CPU运行。

  3. 运行(Running):当一个就绪进程获得了CPU资源并被调度执行时,它进入运行状态。在运行状态下,进程正在执行其指令和代码。(R)

  4. 等待(Waiting)/阻塞(Blocked):一个正在运行的进程可能会因某些原因而无法继续执行,例如等待某个事件的发生、等待输入/输出操作的完成等。在这种情况下,进程会进入等待状态,直到满足特定的条件或事件发生。(S+,S)

  5. 挂起:进程因为某些原因暂时不被执行,数据被放回外存,但它的状态(也就是PCB)被保存在操作系统中,可以在将来某个时刻被唤醒并继续执行。

  6. 结束(Terminated):一个进程完成其任务或被提前终止后,进程进入终止状态。在终止状态下,进程的执行已经完全结束,但系统仍会保留其进程控制块等相关信息。

Z:僵尸状态:如果父子进程中一个程序提前结束 会进入僵尸状态 导致占时的内存泄漏 在后边解决

D:disk sleep 深度睡眠(不可被系统杀死的状态) 例如磁盘写入的进程 S 是随时可以唤醒的普通睡眠

X : dead

T: stopped

t: tracing stop

计算机进行进程的步骤

  1. 进程创建:操作系统根据用户或系统的请求,使用fork()或其他相关的系统调用创建新的进程。新进程会从父进程继承一些属性,如文件描述符、环境变量等。

  2. 进程调度:操作系统根据一定的调度算法(如轮转调度、优先级调度等)从就绪状态的进程队列中选择一个进程执行。被选中的进程会被调度器分配给可用的处理器或CPU核心。

  3. 进程执行:被调度的进程开始执行其指令和代码。进程的代码会从磁盘加载到内存,CPU会按照指令的顺序执行代码,并操作内存和其他资源。

  4. 进程等待:在父子进程关系中,父进程通常会等待子进程完成,以便获取子进程的退出状态或处理子进程的结果。这个等待的过程可以使用系统调用(如waitwaitpid等)来实现。在等待期间,父进程会阻塞,直到子进程结束或满足特定的条件。当子进程结束后,父进程会从等待状态恢复并继续执行。

  5. 进程替换:在进程替换中,新的进程取代当前进程的执行,原进程的代码和数据被新进程的代码和数据所替代。这个过程通常是通过调用exec系列函数来实现的,例如execvpexeclp等。进程替换可以用于加载并执行新的程序,替换当前进程的执行环境、代码和数据。

  6. 进程阻塞:在执行过程中,进程可能需要等待某个事件的发生,如等待用户输入、等待磁盘读取等。在这种情况下,被调度的进程暂时挂起,进入阻塞状态,直到等待的事件发生。

  7. 进程唤醒:当一个进程等待的事件发生时,操作系统会将该进程从阻塞状态唤醒,并重新放入就绪状态的队列中,以便可以被再次调度执行。

  8. 进程终止:当一个进程完成其任务或不再需要时,它会被终止。操作系统会回收该进程使用的资源,并释放其占用的内存空间。

这些步骤是基本的进程执行过程,当多个进程同时存在时,操作系统会根据调度策略和资源管理来决定进程的执行顺序和时间片分配。同时,进程间可能会进行通信和同步操作,以实现协作和共享数据等功能。操作系统提供了在进程间传递消息、共享内存、管道、套接字等机制,用于实现进程间的交互和数据共享。

运行队列->进程调度与执行

运行队列是操作系统中管理进程调度和资源分配的一种数据结构。它存储了当前可运行的进程或任务,并根据调度算法从中选择一个进程分配给CPU执行。

  1. 队列形式:运行队列通常是一个队列数据结构,按照进入队列的顺序排列。当进程处于就绪状态时,会被添加到队列的末尾。

  2. 调度策略:运行队列的调度策略决定了从队列中选择进程的顺序。常见的调度算法包括先来先服务(FCFS)、轮转调度(Round-Robin)、最短作业优先(SJF)、最高优先级优先(HPF)等。

  3. 优先级:进程或任务可以具有不同的优先级,优先级高的进程在选择时会先于优先级低的进程执行。调度器根据进程的优先级从运行队列中选择进程。

  4. 时间片:一些调度算法,如轮转调度,将一个时间片分配给每个进程,一个时间片表示进程能够连续运行的时间。当时间片用完后,进程会被放回运行队列,等待下一次调度。

  5. 调度器:调度器是操作系统的一部分,负责管理进程的调度和资源分配。调度器会选择运行队列中适当的进程,并将其分配给CPU执行。

等待对列->进程阻塞与唤醒

等待队列是在操作系统中用于存储等待某个事件发生的进程或线程的队列。当一个进程或线程需要等待某个条件满足时(如等待输入、等待资源可用等),它会被放入等待队列中,直到条件满足时被唤醒并重新加入可运行队列,以便继续执行。链表链接在要操作的事件(如键盘)后。

  1. 条件:等待队列的每个元素都与一个特定的条件相关联。这个条件通常是指特定的事件或资源状态,当这个条件满足时,等待队列中的元素可以被唤醒。

  2. 队列:等待队列是一个数据结构,用于存储等待某个条件满足的进程或线程。队列可以是先进先出(FIFO)的结构,也可以是优先级队列。

  3. 阻塞和唤醒机制:当一个进程或线程处于等待状态时,它会被阻塞,暂停执行。当条件满足时,操作系统会将其从等待队列中唤醒,并放入可运行队列,使之能够继续执行。

等待队列的使用可以有效地实现进程或线程间的同步与通信,避免了资源竞争和浪费。常见的应用场景包括等待用户输入、等待网络连接、等待I/O完成等。

等待对列->进程的挂起 换入换出

挂起状态是进程生命周期中的一个正常状态,它表示进程因为某些原因暂时不被执行(把PCB加载到内存的数据和代码放回外存),但它的状态(PCB)被保存在操作系统中,可以在将来某个时刻被唤醒并继续执行。

进程挂起的原因可能包括:

  1. 等待资源:进程需要等待某些资源(如I/O设备、共享内存等)变得可用。

  2. 等待事件:进程需要等待某个特定事件的发生,例如输入、信号量变化等。

  3. 系统调用:进程可能因为执行了一个系统调用(如信号处理)而被挂起。

  4. 调度决策:操作系统可能会根据调度策略起某些进程,以便为其他进程提供CPU时间。

  5. 错误或异常:进程可能因为执行了非法操作或发生了异常而被操作系统挂起。

挂起状态不同于终止状态。挂起状态的进程可能会在将来被唤醒,而终止状态的进程则会从系统中移除,其资源将被回收。操作系统通常提供机制来管理挂起进程,例如使用信号量、互斥锁或专门的进程控制块(PCB)来跟踪挂起进程的状态和条件。

挂起的标志就是换入换出

进程的换入换出(也称为进程的调度和交换)是操作系统中的一种机制,用于有效地管理内存中活动进程的数量。

当可用内存不足时,操作系统需要将一些进程从内存中换出(置换出)到外存或磁盘上,以便给新的进程腾出内存空间。当需要执行一个先前从内存中换出的进程时,该进程需要再次换入(置换入)到内存中才可以继续执行。

换入换出的过程包括以下几个步骤:

  1. 选择换出的进程:操作系统根据一定的策略和算法,选择一个需要换出的进程。常见的选择策略包括最近最久未使用(LRU)、先进先出(FIFO)等。

  2. 保存进程的状态:被选择换出的进程的当前状态,如寄存器内容、页表等,需要被保存到外存或磁盘上。这样,在将来需要重新换入时,进程可以从之前保存的状态恢复。

  3. 释放内存空间:被换出的进程占用的内存空间被释放,以便为其他进程腾出空间。

  4. 选择换入的进程:根据前面换出的进程空出的内存空间,操作系统选择一个需要执行的进程,并将其换入内存。

  5. 恢复进程状态:被换入的进程的之前保存的状态被加载到内存中,以便该进程可以继续执行。

父子进程中...
  1. 继承性:在子进程先终止时,它的资源会被父进程继承。如果父进程不主动回收,子进程会一直处于Z状态,相关资源如打开的文件描述符、内存空间不会被释放。发生内存泄漏.

  2. 孤儿进程:在父进程先终止时,子进程不会随之终止。子进程的父进程会变为 init 进程(通常是进程ID为1的进程)。 init 进程是所有其他进程的祖先进程,它会接管孤儿进程(即父进程终止时没有被其他进程接管的子进程)。子进程会继续执行,直到它自己终止或被其他进程终止。init 进程会负责回收孤儿进程的资源。

  3. 消息传递:在某些情况下,父子进程之间可能存在消息传递机制。例如,使用管道、共享内存或套接字等机制,子进程可以向父进程发送消息,反之亦然。

进程的优先级 大O(1)调度算法

优先级高的进程可以优先获得资源,而优先级低的进程则可能会被推迟或暂停执行。

通过这种方式,操作系统可以确保系统资源得到高效利用,同时保持公平性。

  1. 静态优先级:一些操作系统使用静态优先级来表示进程的优先级。这种优先级在进程创建时就被分配,并且在进程的整个生命周期中保持不变。静态优先级通常是一个预定义的数值或标记。

  2. 动态优先级:另一些操作系统使用动态优先级来表示进程的优先级。动态优先级是根据进程在运行过程中的行为和状态动态调整的。例如,进程的优先级可能随着等待时间、CPU使用情况、资源需求等因素的变化而调整。

  3. 多级队列:一种常见的进程调度策略是多级队列调度算法,其中进程被划分为不同的优先级队列,每个队列具有不同的优先级。在这种情况下,进程根据它们所在的队列和队列中的优先级来进行调度。

  4. 时间片轮转:时间片轮转调度算法是一种动态的优先级调度策略,其中每个进程被分配一个相同大小的时间片来执行。一旦一个进程的时间片用尽,它就会被放回队列的尾部,并且下一个进程被选中执行。

一般不要求调整优先级

拓展:底层实现

Linux中每个进程都有一个动态优先级,称为"nice"值或"niceness"值。Nice值范围从-20到+19,其中-20表示最高优先级,+19表示最低优先级。较低的Nice值表示更高的优先级。

动态优先级是通过公式 dynamic_priority = base_priority + bonus 计算得到的。其中,base_priority是进程的原始优先级,通常由静态优先级或默认值(一般是80)确定,bonus是根据进程历史调度行为和CPU使用情况进行动态调整的值。

操作系统的主体也是结构体,包含

task_struct **run;

task_struct **wait;

task_struct *running[140];

task_struct *waiting[140];

swap(run,wait);

[0,99]是其他种类的进程使用 , [100,139]是我们用户的程序的优先级,也是我们nice可以修改的范围

run指针在正在运行的数组中按照优先级遍历每一个队列,优先级相同的进程在同一个队列,此时新加的进程会放在另一个数组(哈希桶)中。遍历完成后交换两个指针的地址,run指针开始继续遍历工作,wait指针开始遍历运行过的队列进行对运行过一次的进程的清理或者处理。

run指针如何知道所有队列是否遍历完成呢?

1.遍历数组 如果数组指针为空就便利下一个,效率低其时间复杂度为O(n),n是进程的数量

2.大O(1)调度算法

大O(1)调度算法的核心思想是使用多级反馈队列(Multilevel Feedback Queue)的概念。这个算法将所有进程划分为多个的就绪队列,每个队列都有不同的优先级。当进程从高优先级队列中出列后,它通常会进入一个低优先级的队列,并且只有在所有更高优先级的队列为空时,才会从低优先级队列中选择进程执行。

为了支持O(1)时间复杂度的调度,大O(1)调度算法会维护一个调度队列和一些辅助数据结构,如红黑树(Red-Black Tree)或者位图(Bitmap)。这些数据结构用于快速确定当前最高优先级队列中是否有进程可供执行。(后边会再次介绍)

例如采用位图标志位指示哪一位是空的

其他概念 进程切换

竞争性:系统进程数目众多,而CPU资源只是少量,所以进程之间还具有竞争属性的,为此提出的优先级的概念

独立性:多进程进行,需要独立享有各种资源,多进程之间不会互相干扰。

并行:并行(Parallel)是指同时执行多个任务的能力。在多核处理器系统中,可以将不同的任务分配给不同的核心同时执行。每个核心都有自己的CPU资源,它们相互之间独立进行计算,互不干扰。并行执行可以显著提高系统的计算能力和处理效率。

并发:并发(Concurrency)是指在一个时间段内同时处理多个任务的能力。在单核处理器系统中,通过进程切换技术,使得多个任务在时间片内交替执行。尽管每个任务在瞬间只能执行一个指令,但在整个时间段内它们并发地进行,给人一种同时执行的感觉。并发处理可以提高系统的响应速度和用户体验。

注意:在进程切换中,进程要将自己的上下文保存好(在寄存器中)或者随身携带,当进程从用户空间切换到内核空间时,它的上下文也会随之发生变化。在用户空间中,进程执行用户程序代码,而在内核空间中,进程执行内核代码。因此,进程上下文在进程切换时需要保存和恢复,以便在切换回用户空间时恢复进程的状态,使其能够继续执行用户程序代码。(进程的上下文是指进程在执行过程中的状态和环境。进程上下文包括了许多与进程执行相关的信息,例如进程的寄存器值、程序计数器、进程状态字等)

进程切换涉及以下几个主要步骤:

  1. 保存当前进程的上下文:操作系统会保存当前进程的寄存器、程序计数器、堆栈指针等相关信息,将这些信息保存在进程控制块(PCB)中。

  2. 选择下一个要执行的进程:根据进程调度算法(如轮转调度、优先级调度等),操作系统从就绪队列中选择下一个要执行的进程。

  3. 恢复下一个进程的上下文:操作系统将下一个要执行的进程的上下文从其PCB中恢复,并将控制权转移给该进程。

  4. 切换到下一个进程执行:操作系统将控制权转移到下一个进程,该进程从之前被保存的程序计数器位置开始执行,继续执行其用户程序代码。

进程地址空间 (一谈)

进程=内和数据结构(task_struct&& mm_struct&&页表)+程序的代码和数据

引子:

#include 
#include 
pid_t g_val = 100;
​
int main() {
    pid_t pid = fork(); // 创建子进程
    if (pid == 0) {
          int cnt = 5;
        while (1) {
            printf("I am child process, pid = %d, ppid = %d, g_val = %d, &g_val = %p\n", getpid(), getppid(), g_val, &g_val);
            sleep(1);
            if (cnt--);
            else {
                g_val = 200;
                printf("子进程change:100->200\n");
            }
        }// 子进程
    }
    else if (pid > 0) { // 父进程
        while (1) {
            printf("I am parent process, pid = %d, ppid = %d, g_val = %d, &g_val = %p\n", getpid(), getppid(), g_val, &g_val);
            sleep(1);
        }
    }
    else { // fork()调用失败
        printf("fork() error\n");
         return 1;
    }
    return 0;
}

运行结果:

I am child process, pid = 4258, ppid = 4257, g_val = 100, &g_val = 0x60105c
I am parent process, pid = 4257, ppid = 2956, g_val = 100, &g_val = 0x60105c
子进程change:100->200
I am child process, pid = 4258, ppid = 4257, g_val = 200, &g_val = 0x60105c
I am parent process, pid = 4257, ppid = 2956, g_val = 100, &g_val = 0x60105c

思考:为什么g_val值不一样,但是地址却是相同的?:变量取地址 得到的是线性地址(或者虚拟地址)并非物理地址。

地址空间

计算机的内存被划分为多个不同的区域,每个区域都有唯一的地址,这些地址的总和被称为地址空间。地址空间的大小通常是固定的,取决于计算机系统中可用的内存大小和操作系统的限制。在32位系统中,地址空间大小通常为4GB,而在64位系统中,地址空间大小则可以超过16EB。

地址空间是一个逻辑概念,它将进程的内存区域划分为不同的地址范围,并提供内存保护和隔离机制,以确保每个进程能够安全地运行。

区域划分

区域划分是指将计算机的内存划分为不同的区域或段,每个区域具有特定的用途和属性。区域划分的目的是为了更好地组织和管理内存,以提供给不同的程序或系统组件使用。

这些区域通常包括以下几种:

  1. 代码段(Code segment):也称为文本段,用于存储程序的机器代码。这个区域通常是只读的,因为程序的代码在运行时不应该被修改。

  2. 数据段(Data segment):用于存储静态数据,包括全局变量、静态变量和常量。这个区域通常是可读写的,但也可以设置为只读,这取决于编译器的设置。

  3. 堆(Heap):堆是动态内存分配区域,通常由程序员使用特定的系统调用或API来分配和释放内存。堆的大小可以根据需要进行扩展或缩小,以适应程序在运行时动态分配内存的需求。

  4. 栈(Stack):栈用于存储局部变量、函数调用和返回地址等。栈是一种后进先出(LIFO)的数据结构,它通过堆栈指针执行操作。

  5. bss段(Uninitialized data segment):bss段用于存储未初始化的全局变量和静态变量。这些变量在程序启动时会被自动初始化为0,其位置通常在数据段的开始处。

  6. data段(Initialized data segment):data段用于存储已初始化的全局变量和静态变量。这些变量的值在程序启动时就已经确定,其位置通常在数据段的中间或末尾处。

  7. 附加段(Additional segments):除了以上几个主要区域外,内存通常还包含其他一些区域,如常量段(constant segment)、只读段(read-only segment)等。这些区域通常用于存储特定的数据类型或格式,如字符串常量、常数或已编译的代码等。

这些区域的划分有助于优化内存的使用和管理,并提供内存保护和隔离机制,以确保每个进程能够安全地运行。

命令行参数和环境变量 (高地址

栈(向下生长)

——————(共享区域)

堆(向上生长)

未初始化全局变量

已初始化全局变量

字符常量区

代码区 (低地址

地址自下向上生长

实验:

#include
#include
int g_val_1;//未初始化全局变量
int g_val_2 =100;//已初始化全局变量
​
int main(int argc,char*argv[],char*env[])
{
    printf("argc addr: %p\n",&argc);
    printf("argv[0] addr: %p\n",argv[0]);
    printf("argv[1] addr: %p\n",argv[1]);
    printf("argv[2] addr: %p\n",argv[2]);
    printf("env[0] addr: %p\n",env[0]);
    printf("env[1] addr: %p\n",env[1]);
    printf("env[2] addr: %p\n",env[2]);
    printf("code addr: %p\n", main);
    const char *str ="hello";
    printf("read only string addr :%p\n",str);
    printf("init global value addr :%p\n",&g_val_1);
    printf("uninit global value addr :%p\n",&g_val_2);
    char *mem1 = (char*)malloc(100);
    char *mem2 = (char*)malloc(100);
    char *mem3 = (char*)malloc(100);
    printf("heap addr :%p\n",mem1);//堆向地址增加的方向减少
    printf("heap addr :%p\n",mem2);
    printf("heap addr :%p\n",mem3);
    static int n =0;
    int a,b,c;
    printf("static member addr :%p\n",&n);
    printf("stack addr :%p\n",&str);//栈向地址减少的方向减少
    printf("stack addr :%p\n",&a);
    printf("stack addr :%p\n",&b);
    printf("stack addr :%p\n",&c);
    return 0;
}

运行结果:

argc addr: 0x7ffd58cfe14c
argv[0] addr: 0x7ffd58cff7c2
argv[1] addr: 0x7ffd58cff7ca
argv[2] addr: 0x7ffd58cff7cd
env[0] addr: 0x7ffd58cff7d0
env[1] addr: 0x7ffd58cff7e1
env[2] addr: 0x7ffd58cff7f0
code addr: 0x40057d
read only string addr :0x4008a7
init global value addr :0x601048
uninit global value addr :0x60103c
heap addr :0x1b80010
heap addr :0x1b80080
heap addr :0x1b800f0
static member addr :0x601044
stack addr :0x7ffd58cfe160
stack addr :0x7ffd58cfe15c
stack addr :0x7ffd58cfe158
stack addr :0x7ffd58cfe154

观察:

1.  栈空间 与堆空间之间有一个大空洞 
  1. static成员地址处于全局变量区

  2. 命令行参数和环境变量存储位置在栈的上方

进程地址空间

虚拟地址和物理地址

介绍:

在Linux系统中,每个进程都有自己独立的地址空间,用于存储进程的代码、数据、堆栈等信息。这个地址空间由虚拟地址和物理地址组成。进程地址空间本质上是描述进程可视范围的大小。

所谓的进程地址空间是内核的一个数据结构对象,类似PCB,地址空间的结构体会被操作系统来管理。

虚拟地址是进程看到的地址,是相对于进程自身的地址空间而言的。每个进程都有自己的虚拟地址空间,因此进程看到的虚拟地址是唯一的。

物理地址是实际的内存地址,是相对于计算机系统的内存而言的。物理地址是内存中实际的位置,由内存控制器管理。

关系:

进程的虚拟地址空间与物理内存之间的关系由内存管理单元(MMU)维护。MMU将进程的虚拟地址映射到物理内存地址,从而实现了地址空间的保护和共享。这种映射关系是由操作系统的页表来维护的。

页表是一种数据结构,用于将进程的虚拟地址空间映射到物理内存中的实际页框。它通过将虚拟地址划分为固定大小的页,并将每个页映射到一个物理页框,实现了虚拟地址到物理地址的转换。

具体而言,页表是由操作系统内核维护的,每个进程都有自己的页表。在进程运行时,MMU通过页表来将进程的虚拟地址转换为物理内存地址。当进程发出内存访问请求时,MMU会查找页表来确定虚拟地址对应的物理页框,然后将数据从物理内存读取或写入到指定的虚拟地址。

页表还用于实现虚拟内存管理机制,允许系统将进程的地址空间分为多个页面,并按需将页面从磁盘交换到物理内存中。这种分页机制提供了更大的地址空间和更高的内存管理灵活性。

模拟实现进程地址空间的结构体:

#include 
//uintptr_t 是 long的宏
// 进程地址空间结构体
typedef struct {
    uintptr_t start_code;      // 代码段的起始地址
    uintptr_t end_code;        // 代码段的结束地址
    uintptr_t start_data;      // 数据段的起始地址
    uintptr_t end_data;        // 数据段的结束地址
    uintptr_t start_heap;      // 堆的起始地址
    uintptr_t end_heap;        // 堆的结束地址
    uintptr_t start_stack;     // 栈的起始地址
    uintptr_t end_stack;       // 栈的结束地址
    // ... 其他内存区域的起始地址和结束地址
    uint32_t page_size;        // 页大小,例如4KB
    uint32_t num_pages_code;   // 代码段占用的页数
    uint32_t num_pages_data;   // 数据段占用的页数
    uint32_t num_pages_heap;   // 堆占用的页数
    uint32_t num_pages_stack;  // 栈占用的页数
    // ... 其他与内存管理相关的信息
} mm_struct;
​
// 初始化进程的地址空间
void init_mm_struct(mm_struct *mm) {
    // 假设使用4KB的页大小
    mm->page_size = 4096;
​
    // 初始化代码段、数据段、堆和栈的起始和结束地址
    mm->start_code = 0x1000; // 假设代码段从0x1000开始
    mm->end_code = mm->start_code + 0x10000; // 假设代码段大小为64KB
    mm->start_data = mm->end_code; // 数据段紧接在代码段之后
    mm->end_data mm->start_data + 0x10000; // 假设数据段大小也为64KB
    mm->start_heap = mm->end_data; // 堆紧接在数据段之后
    mm->end_heap = mm->start_heap + 0x20000; // 假设堆的大小为128KB
    mm->start_stack = mm->end_heap; // 栈紧接在堆之后
    mm->end_stack = mm->start_stack + 0x4000; // 假设栈的大小为16KB
    
    // 计算各段占用的页数
    mm->num_pages_code = (mm->end_code - mm->start_code) / mm->page_size;
    mm->num_pages_data = (mm->end_data - mm->start_data) / mm->page_size;
    mm->num_pages_heap = (mm->end_heap - mm->start_he) / mm->page_size;
    mm->num_pages_stack = (mm->end_stack - mm->start_stack) / mm->page_size;
    
    // ... 初始化其他与内存管理相关的信息
​
}
​
int main() {
    mm_struct mm;
    init_mm_struct(&mm);
    // ... 使用mm_结构体进行内存管理
    return 0;
}

为什么要有虚拟地址

虚拟地址在计算机系统中起着重要的作用,它为实现多个进程并发运行、内存保护和地址空间隔离等提供了关键的机制。下面是一些虚拟地址的重要用途:

  1. 进程隔离和保护:每个进程可以拥有自己的虚拟地址空间,这使得每个进程独立运行,互不干扰。虚拟地址提供了一种机制,让每个进程认为它独占了整个内存空间,而实际上这些地址可以被映射到物理内存的不同区域。这种隔离和保护机制可确保进程无法访问其他进程的地址空间,维护了数据的安全性和隐私。

  2. 内存管理和地址空间扩展:虚拟地址允许操作系统对内存进行灵活的管理和分配。操作系统可以根据实际需求动态地将虚拟地址映射到物理内存中的适当位置,并根据需要进行调整,以最优化内存使用。此外,虚拟地址还允许系统将存储在磁盘上的数据映射到内存中,以实现虚拟内存的概念,从而扩展可用的内存空间。

  3. 地址空间隔离和共享:虚拟地址提供了地址空间的隔离,使得不同的进程可以同时运行,而彼此之间不会干扰。每个进程可以有自己的独立地址空间,使得它们可以将相同的虚拟地址映射到不同的物理内存位置,从而实现了内存的共享和资源隔离。

  4. 硬件支持和快速地址转换:虚拟地址是由硬件和操作系统共同进行转换和管理的。主要是通过使用内存管理单元(MMU)来实现的。MMU负责将程序中的虚拟地址转换为物理地址,以便实际访问内存中的数据。这种硬件支持和快速地址转换使得系统能够快速高效地处理内存访问,并提供高性能的计算环境。

总结:

让所有进程以一个统一的角度去看待物理内存,PCB指针只用维护虚拟内存的指针,修改数据,换入换出只用通过页表中的物理地址去维护,虚拟内存是连续的几段空间

虚拟内存是虚拟的不是实际开辟的空间

虚拟内存是一种操作系统提供的抽象概念,它给进程提供了一种看似连续且非常大的内存空间,这个空间被称为虚拟地址空间。虚拟地址空间是一个逻辑上连续的地址范围,但实际上并不一定会在物理内存中连续存在。

虚拟内存的主要目的是为了实现内存的扩展和地址空间隔离。它通过使用逻辑地址(虚拟地址)来引用内存,而不需要关注物理内存的实际位置。在访问虚拟内存时,操作系统会将虚拟地址转换为物理地址,然后访问实际的物理内存位置。

虚拟内存的实现通常基于分页机制和页面置换算法:

  1. 分页机制:虚拟内存将虚拟地址空间划分为固定大小的页面(通常是4KB)。物理内存也被分为相同大小的页框。当程序访问虚拟内存时,操作系统将虚拟地址分割为页号和页偏移,并根据页号查找对应的页表项,从而确定对应的物理页框。

  2. 页面置换算法:当物理内存不足时,操作系统会使用页面置换算法将某些页从物理内存中替换出去,以腾出空间给新的页面。常用的页面置换算法包括最近最少使用(LRU)和先进先出(FIFO)等。

虚拟内存的好处是可以实现比物理内存更大的地址空间,允许多个进程同时运行,并为每个进程提供独立的地址空间。它还提供了内存保护和隔离,使进程不能直接访问其他进程的内存,增加了系统的安全性。

虚拟内存在应用程序编写时对程序员是透明的,程序员可以像访问物理内存一样访问虚拟地址空间。操作系统负责虚拟地址到物理地址的映射和页面的管理。

页表

  • 在cpu中有专门的寄存器(cr3)来存储页表的地址,当进程离开cpu时会将页表一起带走(页表也是进程上下文的一部分)。

  • 页表存储的位置是物理内存

  • 页表是进程管理和内存管理之间的纽带,将进程管理模块和内存管理模块进行解耦合

    • 这里的解耦合:

      解耦合进程管理和内存管理的方法主要有以下几种:

      1. 虚拟地址空间:虚拟地址空间是进程管理和内存管理解耦合的关键。每个进程都有自己的虚拟地址空间,可以独立地访问和管理自己的内存。这样可以实现进程之间的隔离和保护,避免进程之间的数据泄露和破坏。

      2. 独立的内存管理器:将进程管理和内存管理模块解耦可以通过为每个进程创建一个独立的内存管理器来实现。每个内存管理器负责管理该进程的虚拟地址空间和物理内存,这样可以实现进程之间的独立性和隔离性。

      3. 共享内存:在某些情况下,进程之间可能需要共享一些数据。为了实现进程之间的通信和协作,可以使用共享内存技术。通过将一些内存区域映射到多个进程的虚拟地址空间中,可以实现进程之间的数据共享和通信。

  • 页表将无序变为有序:通过连续的空间,把处于磁盘上的各种切片信息整理起来。

页表中:页表项
虚拟内存页 物理内存页 状态位 其他属性
虚拟地址 物理地址 W?R? 页面大小、指针
ox7777ffffff 0x123456

页表(Page Table)中存储的是页表项(Page Table Entry)

  • 页面的大小:这是在操作系统层面规定的,每个页面的大小会影响页表中的条目数量和存储需求。

  • 虚拟页号(Virtual Page Number,VPN):表示页面在虚拟地址空间中的位置。

  • 物理页号(Physical Page Number,PPN):表示页面在物理地址空间中的位置。

  • 标志位:用于描述页面的状态或属性。如可读、可写、可执行、脏页(内容已经被修改)、保护位(指示访问权限)、有效位(指示页面是否已分配)、状态位(0表示页面不在内存中,还有在磁盘中的数据没被读取,处于缺页状态)等。

  • 表头指针:指向下一级页表(如果有的话)或者指向数据或代码的物理地址。

  • 访问模式信息:包括读模式、写模式、日期、使用状态等。

  • 帧号:在分页系统中,每个物理内存帧都有一个唯一的帧号,页表条目中可能会包含这个帧号,以便于快速访问物理内存中的对应帧。

  • 页表项的日期和过期时间:这可以帮助操作系统判断页面是否仍然有效,以及何时可能需要替换或更新页面。

页帧(Page Frame)和页框(Page Frame)

页帧(Page Frame)和页框(Page Frame)在计算机系统中是指相同的概念,它们都是指内存中的固定大小的存储块。它们的主要作用是用来存储虚拟地址空间中的页面(Page)。

在内存管理中,虚拟内存被划分为固定大小的页面,通常为4KB或者8KB。而物理内存也被划分为与虚拟内存相同大小的页帧或页框。每个页面在内存中有一个相对应的页帧或页框。

当进程在运行过程中需要访问某个虚拟地址时,操作系统通过页表将虚拟地址转换为对应的物理地址。页表的一项记录了虚拟页面和物理页帧之间的映射关系。通过这种映射,可以实现虚拟内存的页面与物理内存的页帧之间的关联。

总之,页帧和页框是指物理内存中的固定大小的存储块,用来存放虚拟内存中的页面。它们的概念基本相同,只是在不同的文献或不同的背景下使用的术语可能会有所不同。

页结构体

页结构体通常是在操作系统的内核中使用的数据结构,用于描述和管理页面的详细属性和信息。在一些实现中,页表中的每个页表项可能会引用页结构体,以实现更多的信息存储和管理功能。

页结构体是虚拟内存管理中的一个概念,用于描述一个页面的属性和信息。它通常包含以下字段:

  1. 虚拟页号(Virtual Page Number,VPN):标识页面在虚拟地址空间中的位置。

  2. 物理页号(Physical Page Number,PPN):如果页面已经分配了物理内存,物理页号表示页面在物理地址空间中的位置;如果页面未被分配,则物理页号可以为空或为无效值。

  3. 标志位(Flags):用于描述页面的状态或属性,常见的标志位包括可读、可写、可执行、脏页(内容已经被修改)、保护位(指示访问权限)等。

  4. 计数器(Counter):用于统计页面的使用情况,如页面被访问的次数,可用于页面置换算法的决策。(引用计数)

  5. 其他信息:根据具体的需求,页结构体可能还包含其他与页面管理相关的信息,如页面所属进程、页面大小、页面访问权限等。

页结构体在内核中使用,用于管理虚拟内存和物理内存之间的映射关系,以及执行页面置换和页面分配等操作。

虚拟页号和物理页号

在分页系统中,有两种页号的映射关系:虚拟页号(Virtual Page Number,VPN)和物理页号(Physical Page Number,PPN)。虚拟页号是进程中使用的虚拟地址中的页号部分,而物理页号是物理内存中实际页面的页号。

页号的映射是通过页表(Page Table)来实现的。页表是一种数据结构,存储了虚拟页号到物理页号之间的映射关系。在分页系统中,每个进程都有自己独立的页表,用于将进程的虚拟地址转换为对应的物理地址。

当进程访问一个虚拟地址时,操作系统会根据该虚拟地址中的虚拟页号,查找进程的页表,获取对应的物理页号。这个物理页号指示了在物理内存中的实际页面。

虚拟页号的范围通常较大,可以超过物理内存的大小。为了解决这个问题,操作系统使用了页面置换算法(Page Replacement Algorithm),以及其他技术(如内存分页和分段结合)来管理虚拟页号到物理页号的映射,以最大程度地利用有限的物理内存,并允许进程的页面在磁盘和内存之间进行动态迁移。

总之,虚拟页号和物理页号的映射关系由进程的页表确定。通过页表的查询,虚拟页号可以被映射到对应的物理页号,从而确定物理内存中的实际页面位置。

思考(重要):
有没有可能虚拟地址映射的物理地址还没有被分配空间

是的,有可能在虚拟地址映射的物理地址没有被分配空间。这种情况通常发生在以下几种情况下:

  1. 延迟分配:操作系统采用延迟分配策略时,虚拟地址可能先于物理地址分配。在这种情况下,当进程首次访问虚拟地址所指向的数据时,操作系统才会分配对应的物理内存空间,并将虚拟地址与物理地址建立映射。

  2. 页面错误:如果进程访问的虚拟地址所映射的物理地址尚未分配,或者物理页未加载到内存(缺页中断)中,会触发页面错误(Page Fault)。页面错误通常由操作系统处理,操作系统会将虚拟地址所需的数据从磁盘上加载到内存中,并更新页表以建立映射关系。通过这种方式,虚拟地址最终映射到了物理地址空间。

需要注意的是,虚拟地址空间是比物理地址空间更为广阔的概念。在实际的内存中,可能仅有部分虚拟地址被分配了物理内存空间,而其他虚拟地址可能没有相应的物理内存映射。当进程访问这些尚未映射的虚拟地址时,就会发生页面错误,操作系统会根据需要将相应的数据加载到内存中。

子进程的页表和父进程相同吗:

fork()函数中,子进程会复制一份父进程的页表,但是子进程的页表和父进程的页表并不完全相同。

fork()函数被调用时,操作系统会为新进程分配一个新的进程标识符(PID)并创建PCB,在PCB中指向一个与父进程相同的虚拟地址空间。然后,新进程会复制一份父进程的页表,并将自己的权限信息(读/写/只读)添加到页表中。

由于新进程和父进程有不同的权限,因此它们对内存的访问方式也不同。父进程只能读取自己的页表,而子进程可以读取自己的页表和父进程的页表。子进程的页表中包含了父进程页表中所有页面的副本,但是这些页面的权限信息已经被修改为子进程自己的权限信息。

子进程如何修改父进程复制过来的页表对应的数据,不是已经设置成只读了吗:

如果要进行写操作,或者父进程要对自己原本的数据进行修改,操作系统会捕获这一事件,并为正在进行写操作的进程复制相应的内存页,创建一个新的物理页并让该进程的虚拟地址映射指向这个新页。这样,父进程和子进程之间的数据就分开了,不再相互共享。(在后续进程创建中的写实拷贝中重点介绍)

这个知识点没有那么简单,在后续的学习会逐渐理解。

我自己目前的理解,因为有标志位记录原本父进程数据是只读还是读写,子进程为要对原本可写的数据进行操作就能被操作系统知道,这时就会发生物理内存页的复制,父子进程页表的虚拟地址是自身的属性,映射的物理地址才是关键,所以也出现了一样的虚拟地址不同的物理地址

父子进程写时拷贝的实现**

(在进程创建中介绍)

如果虚拟地址映射的物理地址没有数据。操作系统是怎么从磁盘中加载数据到内存的呢

(在文件系统后的文件的写入与写回中介绍)

运行进程的 细节(进程控制)

进程创建

fork()

在Linux中,创建进程可以使用fork()系统调用。fork()系统调用的作用是在当前进程中创建一个新进程,新进程与当前进程具有相同的内存空间和文件描述符,但拥有独立的进程ID和资源。它是实现多进程并发执行的基础。

fork()系统调用语法如下:

#include  
int fork();

fork()系统调用返回值如下:

  • 如果返回值是0,表示新创建的子进程调用fork()系统调用成功,并返回0。

  • 如果返回值是子进程的进程ID,表示父进程(一般是所在程序的进程)调用fork()系统调用成功,并返回子进程的进程ID。

  • 如果返回值是-1,表示调用`fork()系统调用失败。

  • fork()函数在调用时会返回两次,一次在父进程中返回子进程的PID,一次在子进程中返回0。根据这两个返回值可以区分父进程和子进程,并在不同的进程中执行不同的逻辑。

getpid() getppid()

#include 
#include 
​
int main() {
    pid_t pid = fork(); // 创建子进程
    if (pid == 0) { // 子进程
        printf("I am child process, pid = %d\n", getpid());
    } else if (pid > 0) { // 父进程
        printf("I am parent process, ppid = %d, child pid = %d\n", getppid(), pid);
    } else { // fork()调用失败
        printf("fork() error\n");p
        return 1;
    }
    return 0;
}

下面是一个简单的示例,演示了如何使用fork()函数创建子进程:

#include 
#include 
​
int main() {
    pid_t pid = fork();
fork的实现原理(写实拷贝)

fork() 函数的实现原理是写实拷贝:

  1. 创建子进程: 当fork()函数被调用时,操作系统会为新进程分配一个新的进程标识符(PID),创建基本相同的PCB,PCB中的进程地址空间指向的是父进程PCB的进程地址空间结构体地址,创建新的页表,子进程共享父进程页表的一部分页表项。如果设置子进程的权限等等属性,会在页表创建新的页表项,修改其中的权限位。

  2. 更新权限位:父进程的物理地址空间被划分为多个页面(通常是4KB大小),这些页面被标记为“只读”。这意味着新进程(子进程和父进程都)不能直接修改这些页面的内容。而父子进程接下来新建的页面都被标记位“读-写”(RW)

  3. 内存的副本: 无论谁先修改先前的数据,都会遇到只读的问题。这里就会产生异常,会触发页面错误(Page Fault),操作系统会捕获页面错误,并为进程在物理内存中复制一份要修改的页帧,修改其权限和原本页的权限为(RW),然后更新进程的页表,使其指向新的页面。这样子进程和父进程都会有一个独立的、可读写的页面,可以直接修改它们的内容。

  4. 返回值: fork() 函数对于父进程返回新的子进程ID,对于子进程返回0。这种返回值的不同使得父进程和子进程可以根据返回值来执行不同的代码路径。

  5. 执行继续: 在子进程中,程序计数器通常设置为父进程代码的下一条指令,子进程将从这里开始执行。在父进程中,fork() 调用之后的代码将立即继续。(存储在代码段的代码是共享的

  6. 资源管理: 子进程虽然复制了父进程的文件描述符,但实际上它是独立的。子进程需要打开文件以读取或写入时,它会获得自己的文件描述符表项。(通过页表的标志位来进行管理)

  7. 父子关系: 子进程和父进程之间存在一种父子关系,操作系统会维护这种关系,使得父进程可以等待子进程结束,或者子进程可以发送信号给父进程。

在 Linux 操作系统中,fork() 系统调用的实现涉及低级的系统调用和内核操作。具体来说,它调用的是 do_fork() 函数,在内核中完成实际的新进程创建工作。此函数会遍历进程描述符数组,找到合适的位置来插入新进程的描述符,并初始化这个描述符。之后,会根据当前进程的状态创建子进程的内存空间,并设置好相关的寄存器和返回值。

父子进程的运行顺序

取决于操作系统的调度机制(调度器)

如果在编写代码时需要特定的运行顺序,可以使用进程间同步机制,如使用父子进程之间的管道进行通信、使用信号量进行同步等。这样可以确保父子进程按照所需的顺序进行交互和执行。

调度器

调度器是操作系统中负责进程或线程调度管理的组件。它的主要职责是根据一定的调度策略,选择一个进程或线程执行,并控制其执行时间和资源分配。调度器可以基于时间片、优先级、用户态还是内核态等因素进行调度。常见的操作系统调度器有轮转调度算法、优先级调度算法、时间片轮转调度算法等。

进程终止

退出码

函数终止无外乎就三种情况:

  1. 代码运行完毕,结果正确。

  2. 代码运行完毕,结果不正确。

  3. 代码异常终止

return 0 这里的0叫做进程的退出码,表征进程的运行结果是否正确,0表示success.return 其他非0数字就表示不同的结果错误的原因。

通过strerror(int)函数可以查看错误码对应的信息。

perror(char*)函数也可以输出错误信息

有一个在error.h头文件中的全局变量 errno ,保存了最近的错误码

$?保存最近一次程序的退出码

常见的退出方法

正常终止:return exit _exit (在关于进程的函数中有做介绍)

异常终止:ctrl + c ,信号终止

进程等待

这里的进程等待不是我们后边要讲的进程的等待队列,而是父子进程之间的等待对方结束或者发出信号的过程。

是什么

父进程(系统)可以通过调用waitwaitpid函数来进行对子进程的状态监测和回收

为什么
  1. 解决僵尸进程无法被杀死的问题。(父进程运行过程中子进程不会内存泄漏)

  2. 通过进程等待,获得子进程的退出码

wait()与waitpid()

wait函数,接受任何一个退出码,如果没能接受到,父进程会进入阻塞。叫做阻塞等待状态。

pid_t wait (int *status)

waitpid()函数的原型如下

#include 
pit_t waitpid(pid_t pid, int *status, int options);//status是输出型参数,
  • pid:要等待的子进程的进程ID。如果为 -1,则表示等待所有子进程结束,>0表示等待其进程ID与pid相同的子进程

  • status:这是一个指向一个整数的指针,当子进程结束时,该整数包含了子进程的状态信息。这个整数通常通过 WIFEXITED(status)WEXITSTATUS(status) 这样的宏来访问。

  • options:这是可选参数,通常设置为 0。如果设置为 WNOHANG,表示只检查是否有可等待的子进程,并不实际等待。(如果函数调用时pid指定的子进程没有结束返回0,否则返回改子进程的ID)

  • 返回值:等待成功返回pid,失败返回-1,没有等待到返回0(设置了options参数)

status:
  1. WIFEXITED(status)

这个宏用于检查子进程是否正常结束。如果子进程是通过exit()函数或者_exit()函数正常结束的,那么WIFEXITED(status)将返回1,否则返回0。这里的statuswaitpid()函数返回的子进程状态信息。

  1. WEXITSTATUS(status)

这个宏用于获取子进程的退出状态。如果子进程正常结束,WEXITSTATUS(status)将返回子进程结束时返回的值(即通过exit(status)_exit(status)传递的值)。如果子进程是非正常结束,这个宏将返回0

其他宏:

  • WIFSIGNALED(status): 如果子进程是由于信号而终止的,则返回真。

  • WTERMSIG(status): 如果使用 WIFSIGNALED 宏返回真,则该宏返回子进程的终止信号编号。

  • WCOREDUMP(status): 如果子进程在终止时生成了核心转储文件,则返回真。

使用用例:

int status;
int ret = waitpid(pid, &status, 0); // 等待子进程结束,并获取状态信息

注意:status的值

  1. tatus`参数中存储的是一个32位的整数,它包含了子进程的退出状态和其他信息。在这个32位整数中,高16位用于存储子进程的退出状态,低16位用于存储其他信息。

bits  31         24    23   22   21   20   19    15               8   7 6    0
     +----------------+----+----+----+----+----+----+----+----+----+----+----+
     |    |    |    |     |    |    |    |    |    | Exit Status  | Core Dump| 
     +----------------+----+----+----+----+----+----+----+----+----+----+----+
                                                          退出状态      被信号所杀   
  1. 一种:低7位(6-0)存储子进程的退出状态,(7)用于存储核心转储标志(Core Dump Flag),次低8位(15-8)存储退出状态。

bits  31         24    23   22   21   20   19   18   17   16   15    8    7     0
     +----------------+----+----+----+----+----+----+----+----+----+----+----+
     | Exit Status    |    |    |     |    |    |    |    |    |    | Core Dump |
     +----------------+----+----+----+----+----+----+----+----+----+----+----+
     正常退出                   被信号所杀
  1. (另一种):高8位(31-24)存储子进程的退出状态,接下来的1位(23)保留,在低7位(22-16)存储子进程的终止信号(Termination Signal),如果子进程被信号终止,则会存储相应的信号编号。最低位(0)用于存储核心转储标志(Core Dump Flag),如果子进程在终止时生成了核心转储文件,则该位将被设置为1。

  2. 终止信号如果为0,说明不是被信号终止

options:

以下是一些常用的waitpid选项:

  • WNOHANG:如果子进程还没有准备好,则不会阻塞调用进程。这个选项通常用于检查子进程是否已经结束,而不阻塞进程。

  • WUNTRACED:等待子进程,如果子进程处于跟踪状态(使用ptrace命令),则不会将子进程的跟踪状态解除。这个选项通常用于在跟踪子进程时等待子进程结束。

  • WEXITED:只返回已经结束的子进程的信息。这个选项通常用于只获取已经结束的子进程的信息,而不等待子进程结束。

  • WFORCED:强制等待子进程结束,即使子进程被阻塞。这个选项通常用于强制等待子进程结束,即使子进程被阻塞。

例如,要等待子进程结束,同时不阻塞调用进程,可以使用以下命令:

waitpid(pid,status, 0);

这里,0表示不使用任何标志。

要使用WNOHANG选项,可以使用以下命令:

waitpid(pid, &status, WNOHANG);

要使用多个选项,可以将它们组合在一起。例如,要等待子进程结束,同时不阻塞调用进程,可以使用以下命令:

waitpid(pid, &status, WEXITED | WNOHANG);

这里,WEXITED选项表示只返回已经结束的子进程的信息,WNOHANG选项表示如果子进程还没有准备好,则不会阻塞调用进程。

使用这个宏,主要是为了实现不同的场景:

例如,非阻塞轮询(父进程继续运行,隔一个周期查询子进程是否结束继续自己的代码)

实验
1.子进程是怎么变为僵尸进程的
#include 
#include  //fork
#include  //exit
#include  //pid_t
#include  //wait
int main() {
    pid_t id = fork(); // 创建子进程
    if (id == 0) { // 子进程
        int cnt = 5;
        while (cnt--) {
            printf("I am child process, pid = %d,"
                "ppid = % d\n", getpid(),getppid());
                sleep(1);
        }
        exit(0);
​
    }
    else if (id > 0) { // 父进程
        while (1) {
            printf("I am father process, pid = %d,"
                "ppid = % d\n", getpid(),getppid());
                sleep(1);
        }
    }
    else { // fork()调用失败
        perror("fork() error");
        return 1;
    }
    return 0;
}
​

通过这条命令我们观察现象:

 while :; do ps ajx | head -1 &&  ps ajx | grep mytest; sleep 1;echo "--------------------------";done
 #mytest是程序名

  PPID    PID   PGID    SID TTY       TPGID STAT   UID   TIME COMMAND
  4178   4269   4269   4135 pts/0      4269 S+    1000   0:00 ./mytest.exe
  4269   4270   4269   4135 pts/0      4269 S+    1000   0:00 ./mytest.exe
  3460   4289   4288   3460 pts/1      4288 S+       0   0:00 grep --color=auto mytest
--------------------------
  PPID    PID   PGID    SID TTY       TPGID STAT   UID   TIME COMMAND
  4178   4269   4269   4135 pts/0      4269 S+    1000   0:00 ./mytest.exe
  4269   4270   4269   4135 pts/0      4269 S+    1000   0:00 ./mytest.exe
  3460   4294   4293   3460 pts/1      4293 S+       0   0:00 grep --color=auto mytest
--------------------------
  PPID    PID   PGID    SID TTY       TPGID STAT   UID   TIME COMMAND
  4178   4269   4269   4135 pts/0      4269 S+    1000   0:00 ./mytest.exe
  4269   4270   4269   4135 pts/0      4269 Z+    1000   0:00 [mytest.exe] 
  3460   4299   4298   3460 pts/1      4298 S+       0   0:00 grep --color=auto mytest
--------------------------
  PPID    PID   PGID    SID TTY       TPGID STAT   UID   TIME COMMAND
  4178   4269   4269   4135 pts/0      4269 S+    1000   0:00 ./mytest.exe
  4269   4270   4269   4135 pts/0      4269 Z+    1000   0:00 [mytest.exe] 
  3460   4304   4303   3460 pts/1      4303 R+       0   0:00 grep --color=auto mytest
--------------------------
  PPID    PID   PGID    SID TTY       TPGID STAT   UID   TIME COMMAND
  4178   4269   4269   4135 pts/0      4178 T     1000   0:00 ./mytest.exe
  4269   4270   4269   4135 pts/0      4178 Z     1000   0:00 [mytest.exe] 
2.僵尸程序的回收
#include  //pid_t
#include  //wait
pid_t wait(int *status);
pid_t waitpid(pid_t pid,int *status,int options);
#include 
#include  //fork
#include  //exit
#include  //pid_t
#include  //wait
int main() {
    pid_t id = fork(); // 创建子进程
    if (id == 0) { // 子进程
        int cnt = 5;
        while (cnt--) {
            printf("I am child process, pid = %d,"
                "ppid = % d\n", getpid(),getppid());
                sleep(1);
        }
        exit(0);
​
    }
    else if (id > 0) { // 父进程 //这里的id是子进程的pid
        int cnt = 8;
        while (cnt--) {
            printf("I am father process, pid = %d,"
                "ppid = % d\n", getpid(),getppid());
                sleep(1);
        }
        pid_t ret = wait(NULL);//等待成功返回子进程的pid
        if(ret = id)
        {
            printf("wait success ,ret = %d\n",ret);
        }
        else{
            printf("wait false\n");
        }
    }
    else { // fork()调用失败
        perror("fork() error");
        return 1;
    }
    return 0;
}

  PPID    PID   PGID    SID TTY       TPGID STAT   UID   TIME COMMAND
  7107   7408   7408   7063 pts/1      7408 S+    1000   0:00 ./mytest.exe
  7408   7409   7408   7063 pts/1      7408 S+    1000   0:00 ./mytest.exe
  7189   7428   7427   7189 pts/0      7427 R+       0   0:00 grep --color=auto mytest
--------------------------
  PPID    PID   PGID    SID TTY       TPGID STAT   UID   TIME COMMAND
  7107   7408   7408   7063 pts/1      7408 S+    1000   0:00 ./mytest.exe
  7408   7409   7408   7063 pts/1      7408 S+    1000   0:00 ./mytest.exe
  7189   7433   7432   7189 pts/0      7432 R+       0   0:00 grep --color=auto mytest
--------------------------
  PPID    PID   PGID    SID TTY       TPGID STAT   UID   TIME COMMAND
  7107   7408   7408   7063 pts/1      7408 S+    1000   0:00 ./mytest.exe
  7408   7409   7408   7063 pts/1      7408 Z+    1000   0:00 [mytest.exe] 
  7189   7438   7437   7189 pts/0      7437 R+       0   0:00 grep --color=auto mytest
--------------------------
  PPID    PID   PGID    SID TTY       TPGID STAT   UID   TIME COMMAND
  7107   7408   7408   7063 pts/1      7408 S+    1000   0:00 ./mytest.exe
  7408   7409   7408   7063 pts/1      7408 Z+    1000   0:00 [mytest.exe] 
  7189   7443   7442   7189 pts/0      7442 R+       0   0:00 grep --color=auto mytest
--------------------------
  PPID    PID   PGID    SID TTY       TPGID STAT   UID   TIME COMMAND
  7107   7408   7408   7063 pts/1      7408 S+    1000   0:00 ./mytest.exe
  7408   7409   7408   7063 pts/1      7408 Z+    1000   0:00 [mytest.exe] 
  7189   7448   7447   7189 pts/0      7447 R+       0   0:00 grep --color=auto mytest
--------------------------
  PPID    PID   PGID    SID TTY       TPGID STAT   UID   TIME COMMAND
  7189   7463   7462   7189 pts/0      7462 S+       0   0:00 grep --color=auto mytest

在另一个终端我们看到:

I am child process, pid = 7409,ppid =  7408
I am father process, pid = 7408,ppid =  7107
I am father process, pid = 7408,ppid =  7107
I am father process, pid = 7408,ppid =  7107
wait success ,ret = 7409
3.wait函数接受的是任意一个子进程id,也就是说我们可以实现一个父进程有多个子进程的回收
#include 
#include  //fork
#include  //exit
#include  //pid_t
#include  //wait
#define N 10
​
void RunChild()
{
    int cnt = 3;
    while (cnt--) {
        printf("I am child process, pid = %d,"
            "ppid = % d\n", getpid(), getppid());
        sleep(1);
    }
}
int main() {
    for (int i = 0; i < N; i++)
    {
        pid_t id = fork(); // 创建子进程
        if (id == 0) { // 子进程
            RunChild();
            exit(0);
        }
        else if (id > 0) { // 父进程 //这里的id是子进程的pid
            printf("create child process : %d\n", id);
        }
        else { // fork()调用失败
            perror("fork() error");
            return 1;
        }
    }
    sleep(5);
    for (int i = 0; i < N; ++i)
    {
        pid_t ret = wait(NULL);//等待成功返回子进程的pid
        if (ret > 0)
        {
            printf("wait success ,ret = %d\n", ret);
        }
        else {
            printf("wait false\n");
        }
    }
    sleep(3);
​
    return 0;
}

要修改一下Makefile文件:

mytest.exe:test6.c
                gcc -o $@ $^ -std=c99
.PHONY:clean
clean:
                rm -f mytest.exe
 while :; do ps ajx | head -1 &&  ps ajx | grep mytest; sleep 1;echo "--------------------------";done
:::
--------------------------
  PPID    PID   PGID    SID TTY       TPGID STAT   UID   TIME COMMAND
  7107   8876   8876   7063 pts/1      8876 S+    1000   0:00 ./mytest.exe
  8876   8877   8876   7063 pts/1      8876 S+    1000   0:00 ./mytest.exe
  8876   8878   8876   7063 pts/1      8876 S+    1000   0:00 ./mytest.exe
  8876   8879   8876   7063 pts/1      8876 S+    1000   0:00 ./mytest.exe
  8876   8880   8876   7063 pts/1      8876 S+    1000   0:00 ./mytest.exe
  8876   8881   8876   7063 pts/1      8876 S+    1000   0:00 ./mytest.exe
  8876   8882   8876   7063 pts/1      8876 S+    1000   0:00 ./mytest.exe
  8876   8883   8876   7063 pts/1      8876 S+    1000   0:00 ./mytest.exe
  8876   8884   8876   7063 pts/1      8876 S+    1000   0:00 ./mytest.exe
  8876   8885   8876   7063 pts/1      8876 S+    1000   0:00 ./mytest.exe
  8876   8886   8876   7063 pts/1      8876 S+    1000   0:00 ./mytest.exe
  7189   8900   8899   7189 pts/0      8899 S+       0   0:00 grep --color=auto mytest
--------------------------
  PPID    PID   PGID    SID TTY       TPGID STAT   UID   TIME COMMAND
  7107   8876   8876   7063 pts/1      8876 S+    1000   0:00 ./mytest.exe
  8876   8877   8876   7063 pts/1      8876 Z+    1000   0:00 [mytest.exe] 
  8876   8878   8876   7063 pts/1      8876 Z+    1000   0:00 [mytest.exe] 
  8876   8879   8876   7063 pts/1      8876 Z+    1000   0:00 [mytest.exe] 
  8876   8880   8876   7063 pts/1      8876 Z+    1000   0:00 [mytest.exe] 
  8876   8881   8876   7063 pts/1      8876 Z+    1000   0:00 [mytest.exe] 
  8876   8882   8876   7063 pts/1      8876 Z+    1000   0:00 [mytest.exe] 
  8876   8883   8876   7063 pts/1      8876 Z+    1000   0:00 [mytest.exe] 
  8876   8884   8876   7063 pts/1      8876 Z+    1000   0:00 [mytest.exe] 
  8876   8885   8876   7063 pts/1      8876 Z+    1000   0:00 [mytest.exe] 
  8876   8886   8876   7063 pts/1      8876 Z+    1000   0:00 [mytest.exe] 
  7189   8905   8904   7189 pts/0      8904 S+       0   0:00 grep --color=auto mytest
--------------------------
  PPID    PID   PGID    SID TTY       TPGID STAT   UID   TIME COMMAND
  7107   8876   8876   7063 pts/1      8876 S+    1000   0:00 ./mytest.exe
  8876   8877   8876   7063 pts/1      8876 Z+    1000   0:00 [mytest.exe] 
  8876   8878   8876   7063 pts/1      8876 Z+    1000   0:00 [mytest.exe] 
  8876   8879   8876   7063 pts/1      8876 Z+    1000   0:00 [mytest.exe] 
  8876   8880   8876   7063 pts/1      8876 Z+    1000   0:00 [mytest.exe] 
  8876   8881   8876   7063 pts/1      8876 Z+    1000   0:00 [mytest.exe] 
  8876   8882   8876   7063 pts/1      8876 Z+    1000   0:00 [mytest.exe] 
  8876   8883   8876   7063 pts/1      8876 Z+    1000   0:00 [mytest.exe] 
  8876   8884   8876   7063 pts/1      8876 Z+    1000   0:00 [mytest.exe] 
  8876   8885   8876   7063 pts/1      8876 Z+    1000   0:00 [mytest.exe] 
  8876   8886   8876   7063 pts/1      8876 Z+    1000   0:00 [mytest.exe] 
  7189   8910   8909   7189 pts/0      8909 S+       0   0:00 grep --color=auto mytest
--------------------------
  PPID    PID   PGID    SID TTY       TPGID STAT   UID   TIME COMMAND
  7107   8876   8876   7063 pts/1      8876 S+    1000   0:00 ./mytest.exe
  7189   8915   8914   7189 pts/0      8914 S+       0   0:00 grep --color=auto mytest
  --------------------------
  PPID    PID   PGID    SID TTY       TPGID STAT   UID   TIME COMMAND
  7107   8876   8876   7063 pts/1      8876 S+    1000   0:00 ./mytest.exe
  7189   8915   8914   7189 pts/0      8914 S+       0   0:00 grep --color=auto mytest
  --------------------------
  PPID    PID   PGID    SID TTY       TPGID STAT   UID   TIME COMMAND
  7107   8876   8876   7063 pts/1      8876 S+    1000   0:00 ./mytest.exe
  7189   8915   8914   7189 pts/0      8914 S+       0   0:00 grep --color=auto mytest
  --------------------------
  PPID    PID   PGID    SID TTY       TPGID STAT   UID   TIME COMMAND
  7189   8915   8914   7189 pts/0      8914 S+       0   0:00 grep --color=auto mytest

./mytest.exe

create child process : 8978
I am child process, pid = 8978,ppid =  8977
create child process : 8979
create child process : 8980
create child process : 8981
I am child process, pid = 8980,ppid =  8977
create child process : 8982
I am child process, pid = 8982,ppid =  8977
create child process : 8983
I am child process, pid = 8981,ppid =  8977
I am child process, pid = 8979,ppid =  8977
create child process : 8984
create child process : 8985
I am child process, pid = 8984,ppid =  8977
I am child process, pid = 8986,ppid =  8977
create child process : 8986
create child process : 8987
I am child process, pid = 8983,ppid =  8977
I am child process, pid = 8987,ppid =  8977
I am child process, pid = 8985,ppid =  8977
I am child process, pid = 8981,ppid =  8977
I am child process, pid = 8986,ppid =  8977
I am child process, pid = 8984,ppid =  8977
I am child process, pid = 8983,ppid =  8977
I am child process, pid = 8982,ppid =  8977
I am child process, pid = 8978,ppid =  8977
I am child process, pid = 8987,ppid =  8977
I am child process, pid = 8980,ppid =  8977
I am child process, pid = 8979,ppid =  8977
I am child process, pid = 8985,ppid =  8977
I am child process, pid = 8981,ppid =  8977
I am child process, pid = 8986,ppid =  8977
I am child process, pid = 8984,ppid =  8977
I am child process, pid = 8987,ppid =  8977
I am child process, pid = 8983,ppid =  8977
I am child process, pid = 8982,ppid =  8977
I am child process, pid = 8979,ppid =  8977
I am child process, pid = 8978,ppid =  8977
I am child process, pid = 8985,ppid =  8977
I am child process, pid = 8980,ppid =  8977
wait success ,ret = 8978
wait success ,ret = 8979
wait success ,ret = 8980
wait success ,ret = 8981
wait success ,ret = 8982
wait success ,ret = 8983
wait success ,ret = 8984
wait success ,ret = 8985
wait success ,ret = 8986
wait success ,ret = 8987
4.wait()会阻塞父进程
#include 
#include  //fork
#include  //exit
#include  //pid_t
#include  //wait
#define N 10
​
void RunChild()
{
    while (1) {
        sleep(1);
    }
}
int main() {
    for (int i = 0; i < N; i++)
    {
        pid_t id = fork(); // 创建子进程
        if (id == 0) { // 子进程
            RunChild();
            exit(0);
        }
        else if (id > 0) { // 父进程 //这里的id是子进程的pid
            printf("create child process : %d\n", id);
        }
        else { // fork()调用失败
            perror("fork() error");
            return 1;
        }
    }
    for (int i = 0; i < N; ++i)
    {
        pid_t ret = wait(NULL);//等待成功返回子进程的pid
        if (ret > 0)
        {
            printf("wait success ,ret = %d\n", ret);
        }
        else {
            printf("wait false\n");
        }
    }
   return 0;
}
create child process : 9153
create child process : 9154
create child process : 9155
create child process : 9156
create child process : 9157
create child process : 9158
create child process : 9159
create child process : 9160
create child process : 9161
create child process : 9162

发现:在终端中,子进程与父进程一直不会消失,输出的界面卡住了。

5.waitpid的使用
  1. WIFEXITED(status)这个宏用于检查子进程是否正常结束。

  2. WEXITSTATUS(status)这个宏用于获取子进程的退出状态。

  3. WIFSIGNALED(status): 如果子进程是由于信号而终止的,则返回真。

  4. WTERMSIG(status): 如果使用 WIFSIGNALED 宏返回真,则该宏返回子进程的终止信号编号。

  5. WCOREDUMP(status): 如果子进程在终止时生成了核心转储文件,则返回真。

  6. options一般是0或者 WNOHANG

#include 
#include 
#include 
#include 
#include 
int main() {
    pid_t pid = fork(); // 创建子进程
    if (pid < 0) {
        perror("fork failed");
        return 1;
    } else if (pid == 0) { // 子进程
        printf("This is the child process. I am done!\n");
        exit(0); // 子进程正常结束,退出状态为 0
    } else { // 父进程(等待子进程结束)
        int status;
        int ret = waitpid(pid, &status, 0); // 等待子进程结束,并获取状态信息
        if (ret == -1) {
            perror("waitpid failed");
            return 2;
        }
        if (!WIFEXITED(status)) { // 如果子进程异常结束,则打印错误信息
            printf("Child process terminated abnormally!\n");
        } else { // 否则打印正常结束的消息和退出状态
            printf("Child process exited with status %d\n", WEXITSTATUS(status));
        }
    }
    return 0;
}
This is the child process. I am done!
Child process exited with status 0

当子进程结束时,waitpid() 会返回,并将子进程的状态信息存入 status 变量中。通过查看这个状态信息,我们可以判断子进程是正常结束还是异常结束。在正常情况下,我们还可以从 status 中获取子进程的退出状态。

如果我们在子进程中加入int a=0,a=a/0;(发生除0错误)输出:

This is the child process. I am done!
Child process terminated abnormally!

6.waitpid中的status

status 是32位整数,前8位是退出状态,后7位是信号终止,倒数第8位是用于存储核心转储标志(Core Dump Flag)

#include 
#include 
#include 
#include 
#include 
int main() {
    pid_t pid = fork(); // 创建子进程
    if (pid < 0) {
        perror("fork failed");
        return 1;
    } else if (pid == 0) { // 子进程
        printf("This is the child process. I am done!\n");
        sleep(1);
        exit(2); // 子进程正常结束,退出状态为 2
    } else { // 父进程(等待子进程结束)
        int status;
        int ret = waitpid(pid, &status, 0); // 等待子进程结束,并获取状态信息
        if (ret == -1) {//ret == pid也可以
            perror("waitpid failed");
            return 2;
        }
        else{
        //7f:0111 1111
        //ff:1111 1111
        printf("status : %d,exit sig : %d,exit code : %d\n"
        ,status,status&0x7f,status>>8&0xff);
        }
        sleep(5);
    }
    return 0;
}

输出:

This is the child process. I am done!
status : 512,exit sig : 0,exit code : 2

#include 
#include 
#include 
#include 
#include 
int main() {
    pid_t pid = fork(); // 创建子进程
    if (pid < 0) {
        perror("fork failed");
        return 1;
    } else if (pid == 0) { // 子进程
        printf("This is the child process. I am done!\n");
        int a=0;
        a= a/0;
        exit(2); // 子进程正常结束,退出状态为 2
    } else { // 父进程(等待子进程结束)
        int status;
        int ret = waitpid(pid, &status, 0); // 等待子进程结束,并获取状态信息
        if (ret == -1) {//ret == pid也可以
            perror("waitpid failed");
            return 2;
        }
        else{
        printf("pid: %d,status : %d,exit sig : %d,exit code : %d\n",
        ret,status,status&0x7f,status>>8&0xff);
        }
        sleep(5);
    }
    return 0;
}

输出:

This is the child process. I am done!
pid: 2933,status : 136,exit sig : 8,exit code : 0

查看kill中信号(kill -l),8代表浮点数错误。

7.waitpid中的options

options参数可以使用WNOHANGWUNTRACEDWEXITEDWFORCED等标志进行组合,以控制等待子进程的行为。

例如,要实现阻塞轮询,可以使用以下代码:

#include 
#include 
#include 
#include 
#include 
int main() {
    pid_t pid = fork(); // 创建子进程
    if (pid < 0) {
        perror("fork failed");
        return 1;
    }
    else if (pid == 0) { // 子进程
        printf("This is the child process. I am done!\n");
        sleep(5);
        exit(0); // 子进程正常结束,退出状态为 0
    }
    else { // 父进程(等待子进程结束)
        int status = 0;
        while (1) {
            int result = waitpid(pid, &status ,WNOHANG);
            if (result == -1) {
                printf("wait false\n");
            }
            else if (result == 0) {
                printf("父进程正在等待子进程结束\n");
            }
            else {
                if (WIFEXITED(status)) {
                    printf("子进程退出,退出状态为:%d\n", WEXITSTATUS(status));
                }
                break;
            }
            sleep(1);
        }
    }
    return 0;
}

这里,我们使用了WNOHANG标志来等待子进程结束,同时不阻塞进程。当子进程结束时,程序会输出子进程的退出状态。

输出:

父进程正在等待子进程结束
This is the child process. I am done!
父进程正在等待子进程结束
父进程正在等待子进程结束
父进程正在等待子进程结束
父进程正在等待子进程结束
子进程退出,退出状态为:0

思考:
  1. 子进程怎么会把信号传递给父进程的,不是具有独立性吗:

子进程退出不是立马消除PCB,而是变成僵尸进程,等待父进程对他的信息进行回收

这里回收有专门的函数,而且fork函数返回给父进程的参数是子进程的pid。

  1. waitpid

    • status中的信号段 本质是系统或者用户对子进程发送的信号,是子进程异常终止传递的异常信息

    • 如果等待的pid不是这个父进程创建的子进程,会检测出来并且返回-1,这里说明父进程内waitpid调用异常

    • 如果WIFEXITED(status)为假说明子进程异常退出(可能是/0等重大错误)

    • 如果WEXITSTATUS(status)表示自定义的退出码

    • options实现了非阻塞轮询,这里父进程在等待时候执行的应该是时间复杂度不大的任务,

  2. 僵尸进程必须立马回收吗?

不是这样的,我们可以接受一段时间,这里的进程等待是一个比较优秀的策略。

进程替换

是什么

进程替换(Process Replacement)是指一个正在执行的进程被另一个进程替代的过程。在进程替换过程中,新的进程会取代当前进程的执行,这意味着原进程的代码和数据将被新进程的代码和数据所替换。

进程替换通常用于加载并执行新的程序,替换当前进程的执行环境、代码和数据。替换后,新进程会继续在原进程的上下文中执行,也就是在原进程被替换的位置继续执行。进程替换可以发生在当前进程内部,也可以通过创建一个新的进程来实现替换。

进程替换是通过调用exec系列的函数来实现的。这些函数包括execlexecvexecleexecveexeclpexecvpexcvpe头文件:

进程替换的步骤通常为:

  1. 创建一个新的进程。

  2. 在子进程中,调用exec函数族中的某个函数来加载新程序。

  3. 执行新程序的代码和数据,替换当前进程的执行。

  4. 如果执行失败(如指定的程序文件不存在),则将会返回错误码,父进程可以根据需要进行处理。

exec函数族

l:list, v: vector , p: PATH , e :ENV

  1. int execl(const char *path, const char *arg0, ...)

    此函数类似于 execv,但是每个命令行参数都需以单独的形式传递。参数列表以 NULL 结束。

    例如

    execl("/usr/bin/ls", "ls","-l", "-a", NULL);    
    execl("/usr/bin/bash", "bash","test.sh", NULL); //运行其他语言的脚本
  2. int execlp(const char *file, const char *arg0, ...)

    这个函数类似于 execl,但是它会在系统的环境变量 PATH 中搜索可执行文件路径。

    例如

    execlp("ls", "ls", "-l", "-a", NULL);   
  3. int execle(const char *path, const char *arg0, ..., ( *)NULL)

    int execle(const char *path, const char *arg0, ..., char *const envp[])

    exterv char **environ;
    putenv("VALUE=MY_ENV");
    char*const myenv={
          "V1=1",
           "V2=2",
           "V3=3"
    };
    execle("./myexecvtest","myexecvtest", "-l", "-a" ,"-p", NULL,environ);  
    execle("./myexecvtest","myexecvtest", "-l", "-a" ,"-p", NULL,myenv);

  4. int execv(const char *path, char *const argv[])

    接受程序的路径 (path) 和命令行参数 (常指针数组)(argv),并在当前进程中执行新程序。

    例如

    char *const myargv[]={
    "ls","-l","-a",NULL
    };
    execv("/usr/bin/ls",myargv);

  5. int execvp(const char *file, char *const argv[])

    这个函数类似于 execv,但是它会在系统的环境变量 PATH 中搜索可执行文件路径。即它可以直接使用文件名 (file) 来执行程序,而不需要指定程序的完整路径。

  6. int execve(const char *filename, char *const argv[], char *const envp[])

    它的参数包括程序的文件路径 (filename)、传递给程序的命令行参数 (argv) 以及程序的环境变量 (envp)。(这个函数是最关键的他存放在man手册的第二级,是系统调用,所有其他的exec族函数都会调用这个接口)

    char *const myargv[]={
    "ls","-l","-a",NULL
    };
    //1.新增环境变量
    execve("/usr/bin/ls",myargv,);
    //2.只使用某些环境变量
    execve("/usr/bin/ls",myargv,);

返回值:

如果执行成功,它们不会返回;如果执行出错,则会返回 -1,并将错误信息存储在 errno 中。

注意点:

  1. 即使不传递环境变量,替换进程使用依然是子进程所有的环境变量,命令行参数也是如此

  2. 使用这些函数时,需要特别注意错误处理,因为如果在执行新程序时出现错误,当前的进程可能会被替换为错误程序,这可能会导致不可预知的行为。

  3. 在使用这些函数时,要注意参数的正确传递和数组的边界检查,以避免出现缓冲区溢出等问题。

为什么

进程替换的优点包括:

  • 能够动态地加载和执行新的程序,实现程序的热插拔。

  • 能够改变当前进程的执行环境和代码,以满足不同的运行需求。

  • 可以减少内存和资源的消耗,重新释放不再需要的资源。

需要注意的是,在进行进程替换时,原进程的当前状态会被丢失。新进程会从替换点开始执行,继承一些环境变量和打开的文件描述符,但不会继承其他的父进程状态。

实验:

1.(单进程的程序替换)

execl(path, arg0, ...)

  • path:程序的路径。

  • arg0:程序的第一个参数。

  • ...:程序的其他参数。

使用例子:

#include 
#include 
#include 
#include 
#include 
int main() {
    pid_t id = fork();
    if(id==0){
        printf("before process\n");
        execl("/usr/bin/ls","ls", "-l", "-a", NULL);    
        printf("after process\n");
        exit(0);
    }
    else
    {
        pid_t ret =waitpid(id,NULL,0);
        if(ret >0) printf("wait success\n");
        
    }
    return 0;
}

输出://没有输出after process

total 40
drwxrwxr-x 2 lzh lzh  192 Feb  1 05:13 .
drwxrwxr-x 5 lzh lzh   75 Jan 31 02:56 ..
-rw-rw-r-- 1 lzh lzh   83 Jan 31 04:51 Makefile
wait success

2.execv,传递给另一个命令行参数

#include 
#include 
#include 
#include 
#include 
int main() {
    pid_t id = fork();
    char* const argv[] = {
        "test_execv", "-l", "-a" ,"-p", NULL
    };
    //putenv("VALUE=MY_ENV");
    if(id==0){
        printf("before process\n");
        execv("./test_execv",argv); 
        
        printf("after process\n");
        exit(0);
    }
    else
    {
        pid_t ret =waitpid(id,NULL,0);
        if(ret >0) printf("wait success\n");
        
    }
    return 0;
}
​
//test_execv.cpp 
#include
using namespace std;
int main(int argc, char* argv[], char* env[])
{
    cout << argv[0] << " begin running" << endl;
    cout << "now show argvs\n"< 
  

运行结果:

before process
test_execv begin running
now show argvs
​
0 :test_execv
1 :-l
2 :-a
3 :-p
now show envs
​
0 :XDG_SESSION_ID=2
1 :HOSTNAME=MYCAT
2 :SHELL=/bin/bash
3 :TERM=xterm
4 :HISTSIZE=1000
5 :SSH_CLIENT=192.168.170.1 1506 22
6 :SSH_TTY=/dev/pts/0
7 :USER=lzh
8 :LS_COLORS=rs=0:di=01;34:ln=01;36:mh=00:pi=40;33:so=01;35:do=01;35:bd=40;33;01:cd=40;33;01:or=40;31;01:mi=01;05;37;41:su=37;41:sg=30;43:ca=30;41:tw=30;42:ow=34;42:st=37;44:ex=01;32:*.tar=01;31:*.tgz=01;31:*.arc=01;31:*.arj=01;31:*.taz=01;31:*.lha=01;31:*.lz4=01;31:*.lzh=01;31:*.lzma=01;31:*.tlz=01;31:*.txz=01;31:*.tzo=01;31:*.t7z=01;31:*.zip=01;31:*.z=01;31:*.Z=01;31:*.dz=01;31:*.gz=01;31:*.lrz=01;31:*.lz=01;31:*.lzo=01;31:*.xz=01;31:*.bz2=01;31:*.bz=01;31:*.tbz=01;31:*.tbz2=01;31:*.tz=01;31:*.deb=01;31:*.rpm=01;31:*.jar=01;31:*.war=01;31:*.ear=01;31:*.sar=01;31:*.rar=01;31:*.alz=01;31:*.ace=01;31:*.zoo=01;31:*.cpio=01;31:*.7z=01;31:*.rz=01;31:*.cab=01;31:*.jpg=01;35:*.jpeg=01;35:*.gif=01;35:*.bmp=01;35:*.pbm=01;35:*.pgm=01;35:*.ppm=01;35:*.tga=01;35:*.xbm=01;35:*.xpm=01;35:*.tif=01;35:*.tiff=01;35:*.png=01;35:*.svg=01;35:*.svgz=01;35:*.mng=01;35:*.pcx=01;35:*.mov=01;35:*.mpg=01;35:*.mpeg=01;35:*.m2v=01;35:*.mkv=01;35:*.webm=01;35:*.ogm=01;35:*.mp4=01;35:*.m4v=01;35:*.mp4v=01;35:*.vob=01;35:*.qt=01;35:*.nuv=01;35:*.wmv=01;35:*.asf=01;35:*.rm=01;35:*.rmvb=01;35:*.flc=01;35:*.avi=01;35:*.fli=01;35:*.flv=01;35:*.gl=01;35:*.dl=01;35:*.xcf=01;35:*.xwd=01;35:*.yuv=01;35:*.cgm=01;35:*.emf=01;35:*.axv=01;35:*.anx=01;35:*.ogv=01;35:*.ogx=01;35:*.aac=01;36:*.au=01;36:*.flac=01;36:*.mid=01;36:*.midi=01;36:*.mka=01;36:*.mp3=01;36:*.mpc=01;36:*.ogg=01;36:*.ra=01;36:*.wav=01;36:*.axa=01;36:*.oga=01;36:*.spx=01;36:*.xspf=01;36:
9 :PATH=/usr/local/java/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/root/bin
10 :MAIL=/var/spool/mail/root
11 :PWD=/home/lzh/tmp/processtest
12 :JAVA_HOME=/usr/local/java
13 :LANG=en_US.UTF-8
14 :HISTCONTROL=ignoredups
15 :HOME=/home/lzh
16 :SHLVL=2
17 :LOGNAME=lzh
18 :SSH_CONNECTION=192.168.170.1 1506 192.168.170.135 22
19 :XDG_DATA_DIRS=/root/.local/share/flatpak/exports/share:/var/lib/flatpak/exports/share:/usr/local/share:/usr/share
20 :LESSOPEN=||/usr/bin/lesspipe.sh %s
21 :DISPLAY=localhost:10.0
22 :XDG_RUNTIME_DIR=/run/user/0
23 :OLDPWD=/home/lzh/tmp
24 :_=./mytest.exe
test_execv stop running
wait success

3.execle传递环境变量

#include 
#include 
#include 
#include 
#include 
​
int main() {
    pid_t id = fork();
    if (id == 0) {
​
        printf("before process\n");
        char* myenv[] = {
            "V1=1",
            "V2=2",
            "V3=3",
            NULL
        };
        execle("./test_execv", "test_execv", "-l", "-a", "-k", NULL, myenv);
        //这里是覆盖原本的环境变量
        printf("after process\n");
        exit(1);
    }
    else {
        pid_t ret = waitpid(id, NULL, 0);
        if (ret > 0) {
            printf("wait success\n");
        }
    }
    return 0;
}
​
​
//test_execv.cpp 
#include
using namespace std;
int main(int argc, char* argv[], char* env[])
{
    cout << argv[0] << " begin running" << endl;
    cout << "now show argvs\n"< 
  

运行结果:

test_execv begin running
now show argvs
​
0 :test_execv
1 :-l
2 :-a
3 :-k
now show envs
​
0 :V1=1
1 :V2=2
2 :V3=3
test_execv stop running
wait success

思考:
  1. 代码段的写实拷贝,直接进行替换会修改原本父子进程的共享的代码,所以代码段也会发生写实拷贝。

  2. 没有创建新的进程,直接发生了进程的替换(数据和代码)PCB没有改变,exec后续的代码不会执行。

  3. 如果exec失败了,程序会继续在原进程执行。

  4. 能够通过进程替换,实现其他语言脚本的调用,exec族函数是一个调用器。所有的语言的脚本都是进程

你可能感兴趣的:(linux,学习)