Linux之进程控制
Linux之进程控制
进程创建
fork()
1.函数介绍:pid_t fork();fork函数用于创建子进程,若是创建成功,在子进程中返回0,在父进程返回新创建的子进程的id,若是创建失败,返回-1
2.调用fork函数,内核会做什么?
<1>分配资源:
1)控制进程快:内核为新创建的子进程分配一个新的
task_struct
结构体,这是进程在内核中的核心数据结构,用于存储进程的所有信息,如进程ID、状态、内存映射、文件描述符等。
2)内存空间:为子进程分配必要的内存空间,包括用户态的内存(如栈、堆)和内核态的内存
<2>复制父进程的代码和数据:
1)复制代码段、数据段、堆和栈:内核将父进程的代码段、数据段、堆和栈的内容复制到子进程中。但为了提高效率,现代Linux内核采用了写时复制(Copy-on-Write, COW)技术。这意味着在
fork()
时,实际上并没有立即复制数据,而是让父子进程共享相同的物理内存页。只有当某个进程尝试修改这些共享的内存页时,才会触发缺页异常,内核才会为修改的进程创建这些内存页的副本。
2)复制文件描述符表:复制父进程的文件描述符表,使得父子进程可以共享打开的文件。但是,每个文件描述符的引用计数会增加,以反映它被两个进程共享。
<3>初始化子进程:
1)为新创建的子进程分配一个唯一的进程ID。
2)将子进程的状态设置为不可中断睡眠状态(TASK_UNINTERRUPTIBLE),以确保在子进程完成初始化之前不会被调度执行。
<4>设置父子关系:
1)在父进程的
task_struct
中添加对新创建的子进程的引用。
2)在子进程的
task_struct
中设置父进程的引用,以及指向父进程进程组的指针
<5>执行调度:
1)内核将子进程添加到系统的进程列表中,准备进行调度。
2)返回值
3.fork函数可以有两个返回值,这是为什么呢?
答案是在进行程序调度时发生写实拷贝,子进程和父进程共享大部分的数据和代码,当执行子进程时,在物理内存上对子进程的代码和数据进行拷贝,在另一块物理内存上执行,同理执行父进程时也是如此,故而父进程和子进程在执行时实际上是两个独立的进程,每个进程都会接收fork的返回值
补充:写实拷贝
<1>概念:写实拷贝是一种内存管理策略,其核心思想是:在多个进程或线程共享同一块内存区域时,如果不进行写操作,则这些进程或线程可以共享同一块物理内存;一旦某个进程或线程尝试对共享内存区域进行写操作,操作系统会为该进程或线程分配一块新的物理内存,并将原共享内存区域的内容复制到新的内存中,以确保数据的一致性和独立性。
<2>原理:
写实拷贝技术通常通过引用计数来实现。在分配共享内存区域时,系统会多分配一些空间来存储引用计数,用于记录有多少个进程或线程正在共享这块内存。当某个进程或线程尝试对共享内存进行写操作时,系统会先检查引用计数:
***如果引用计数大于1,说明有其他进程或线程也在共享这块内存,此时系统会为该进程或线程分配一块新的物理内存,并将原共享内存的内容复制到新内存中,然后将引用计数减1。
***如果引用计数等于1,说明没有其他进程或线程在共享这块内存,此时可以直接对该内存进行写操作,无需复制。
<3>好处:提高性能,简化内存管理
进程终止
1.进程退出的场景
<1>代码运行完毕,结果正确
<2>代码运行完毕,结果不正确
<3>代码异常终止
注:若代码运行完毕,结果正确与否是通过进程退出码判断的;若进程异常终止,此时进程退出码无意义,而进程出现异常,往往是进程接收了信号
2.查看进程退出码的命令
. echo $? :此命令用于打印最近一个程序/进程退出时的退出码(因为进程退出码会被写在进程的task_struct内部)
3.进程退出的方法
<1>在main函数内部写return
<2>使用exit(进程退出码)函数,在任何地方调用exit(),进程会结束,同时将子进程的进程退出码返回给父进程
<3>使用_exit(进程退出码)函数
exit | _exit | |
头文件 | <stdlib.h> | <unistd.h> |
是否进行缓冲区刷新 | 是 | 否 |
返回值 | 将代表退出状态的整数返回 | 将代表退出状态的整数返回 |
注:exit函数底层封装了_exit函数
进程等待
1.为什么要进行进程等待?
答案是通过等待,解决内存泄漏问题,回收子进程资源;获取子进程的退出信息
2.如何进行进程等待?
<1>pid_t wait(int* status);
1)功能:使父进程等待任意一个子进程结束,并获取子进程的终止状态
2)头文件:<sys/wait.h>
3)参数解释:
status:
一个指向整数的指针,用于存储子进程的终止状态。如果不需要这个信息,可以传递
NULL
。
4)返回值:成功返回子进程的id;失败返回-1,并设置errno指示错误信息
<2>pid_t waitpid(pid_t pid,int* status,int options);
1)功能:使父进程等待特定子进程结束,设置是否阻塞父进程模式,并获取子进程的终止状态
2)头文件:<sys/wait.h>
3)参数解释:
pid :指定要等待的子进程的进程 ID
>0 | 等待进程 ID 等于 pid 的子进程 |
=0 | 等待与调用进程属于同一个进程组的任意子进程 |
<-1 | 等待进程组 ID 等于 pid 绝对值的任意子进程 |
=-1 | 等待任意一个子进程,此时 waitpid 的行为与 wait 类似 |
status
:指向整数的指针,用于存储子进程的终止状态。如果不需要这个信息,可以传递
NULL
options :设置是否阻塞,默认为0
WNOHANG | 如果没有子进程结束,则立即返回 0 ,而不是阻塞等待。这允许父进程以非阻塞的方式检查子进程的状态 |
WUNTRACED | 除了等待终止的子进程外,还等待被暂停(stopped)的子进程 |
4)返回值:
0说明等待结束,返回子进程的id;
=0说明调用结束,但是子进程没有退出;
<0说明等待失败,并设置errno指示错误信息
进程替换
1.进程替换概念:
进程替换允许一个进程在运行过程中被另一个新的进程完全替换,在进程替换发生时,原有进程的代码、数据和资源会被新的进程所取代,新进程开始执行,原有进程的执行状态和上下文信息会被丢弃。
2.进程替换原理:
父进程通过fork函数创建子进程,子进程内部调用exec*系列函数来执行新进程。
exec
系列函数不会创建新进程,而是直接用新程序替换当前进程的内容,一旦替换完成,后续代码将不在执行。
调用
exec
函数后,当前进程的代码和数据完全被新程序替换,从新程序的启动例程(通常是
main
函数)开始执行。尽管进程的代码和数据被替换了,但进程的 ID(PID)、父进程 ID(PPID)、文件描述符等关键信息会被保留。在进程替换之后,父进程仍然可以通过原来的 PID 来等待子进程的结束
3.进程替换的接口–exec*函数系列
<1>int execl(const char* path,const char* arg,…);
直接接受参数列表和环境变量(或继承当前环境变量)
<2>int execlp(const char* file,const char* arg,…);
接受程序名而不是路径,并使用
PATH
环境变量来查找程序
<3>int execv(const char* path,const char* argv[]);
接受参数列表作为数组,并继承当前环境变量。
<4>int execvp(const char* file,const char* argv[]);
类似于
execv
,但接受程序名并使用
PATH
环境变量查找。
<5>int execvpe(const char* file,const char* argv[],char* const envp[]);
1)参数解释:
path:路径+程序名,用于解释我要执行什么
file:要执行的文件名(因为会自动在环境变量PATH中查找指定命令)
arg:可变参数列表,命令行怎么写,我就怎么写,最后要以NULL结尾
argv[]:参数列表数组,第一个元素是程序名,最后一个元素是
NULL
envp[]:存放环境变量的指针数组列表
2)返回值:成功不返回任何东西;失败返回-1,并设置errno指示错误信息
3)若要将新的环境变量添加到子进程内?
方法一:使用putenv(),在哪个进程调用它,就在哪个进程新增环境变量
方法二:使用extern char** environ;声明,再调用execvpe(file,argv[],envp[],environ)函数