进程控制

1.fork函数

1.1如何理解fork函数有两个返回值问题

fork()函数是系统给用户的接口函数,那么fork()函数的实现在OS里,里边的大致内容是

pid_t fork(){ 

​	1.创建子进程PCB 

​	2.赋值,继承父进程的一部分内容 

​	3.创建并设置页表等 

​	4.将子进程放入调度队列 

​	5.return pid; 

}

在5 .return pid;即返回之前,此时其实子进程已经创建出来并调度了 ,所以  fork()之后父子进程共享代码不完全对在return 之前已经有两个执行流了

父子进程都要return ,所以父进程返回的是创建的子进程的pid,子进程返回给自己的是0

所以return要被调度两次,父子各自执行一次return,所以严谨的说,在fork内部已经有两个执行流生成了

1.2如何理解fork给父进程返回自己的pid,自己返回0

对于孩子和父亲,

一个父亲,有多个孩子,但没有一个孩子多个父亲这种,

所以对于父进程,肯定要记住孩子的信息等即(pid)从而方便管理

1.3如何理解同一个id值,有两个不同的值

pid_t id=fork();

在运行的时候,父子进程是共享内存的,即同时用一个物理空间,但是如果有写入,就会对于同一个值,在拷贝一份,让写入的哪个其实对应这个拷贝的

返回的本质其实就是写入,有写入,那么久会发生写时拷贝,所以会发生同一个id有两个不同的值,地址也相同,这个地址是虚拟地址

2.进程终止

2.1进程退出码

在我们写mian函数的时候,都要最后写一个return 0;

这个return 0; 是什么呢?

这个0是进程退出时候,对应的退出码,标志执行结果是否正确

可以判断退出码的结果来判断程序是否执行正确

查看退出码的办法:

echo $?

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

注意细节,echo也是一个程序,其实会执行一次,所以后边都变为0了

退出码的意义:用0表示成功,非0表示失败,不同的退出码标记不同的错误

2.2进程退出

2.2.1进程常见退出办法

1.代码跑完,执行结果正确

2.代码跑完,执行结果错误

3.代码异常终止

所以分为正常终止和异常终止两个情况

1.正常退出

①通过main函数return 返回码

②调用exit(返回码)直接退出

③_exit(返回码)退出

2.异常退出,ctrl+c,信号终止(除零或者野指针等异常信号)

2.2.3exit()函数和_exit()函数

在写代码时候,exit()函数可以直接终止当前进程,并且exit(num)返回里边的num即退出码,exit()是库函数

_exit()函数也可以终止并且返回退出码,但是不同的是,这个是系统调用

exit()是一个库函数,而_exit是一个系统调用

库函数是在系统调用的基础上来进行封装的

exit()在终止的时候,会刷新缓冲区,而_exit()函数终止则不会刷新缓冲区,所以可以引出,----》缓冲区是不在OS层的,而是在用户层的

3.进程等待

之前讲过僵尸进程的危害,如果僵尸进程不解决,那么则会造成内存泄漏

此时可以用进程等待的方式让父进程等待子进程,但是子进程要返回是否正常退出的信息,以及进程运行的结果对还是不对

父进程通过进程等待的方式进行对子进程的资源回收,以防止僵尸进程的产生

3.1进程等待的方法

3.1.1wait()函数

#include <sys/types>

#include <sys/wait.h>

pid_t wait(int*status);
  • 返回值:

成功返回被等待进程pid,失败返回-1。

  • 参数:

输出型参数,获取子进程退出状态,不关心则可以设置成为NULL


wait()函数:

父进程一旦调用了wait()函数,就会立即阻塞自己

wait()函数自动分析当前进程的某个子进程是否退出,

如果退出,那么wait()就会收集这个子进程的相关信息,把他彻底销毁释放

如果没有找到这样的一个子进程那么就会一直阻塞


注:wait()一般与fork()配用,如果没有用fork()调用wait(),那么wait()函数返回-1

这个参数status用来收集子进程死掉时候的状态信息,一般都不会关心她,所以一般这个值都设为NULL

eg: pid_t ret= wait(NULL);

3.2 waitpid()方法

#include<sys/types.h>//需要的头文件
#include<sys/wait.h>
pid_ t waitpid(pid_t pid, int *status, int options);
  • 返回值: 当正常返回的时候waitpid返回收集到的子进程的进程ID;

  • 参数:

    ①pid:

​ pid=-1时候,代表返回结果是-1,代表错误,等待的是错误的pid的子进程。

​ Pid>0.等待其进程ID与pid相等的子进程。

​ ②options:

			通常是0,代表阻塞时等待

3.2.1参数status

status参数,该参数是一个输出型参数,由操作系统填充

​ 有两种情况

​ ①是否正常退出,以及退出码

​ ②异常终止的信号

如下图

前七位代表的是终止信号,次八位代表的是退出状态

​ ①前七位如果是0,那么代表进程正常退出,那么次八位则就是子进程(进程)的退出码

​ ②前七位如果是非0,那么代表异常终止,前7位对应的数字其实就是对应了异常的编号,次八位就没意义了,一般是0

异常情况下的信号一般就是代表了下图序号,可以用kill -l 查看,例如发生除0操作等异常

代码:正常情况下如图

4.进程的程序替换

创建子进程的目的?

a.让子进程执行父进程的部分代码

b.执行一个全新的程序

所以,如果要想执行一个全新的进程,就需要引入进程的程序替换的概念

4.1替换函数

#include<unistd.h>	//头文件
int execl(const char *path,const char *arg,....);
int execlp(const char *file,const char *agr,...);
int execle(const char *path,const char *arg,...,char *consst envp[]);
int execv(const char *path,char *const argv[]);
int execvp(const char *file,char *const argv[]);
int execve(const char *path,char * const argv[],char *const envp[]);

这里的path是指需要执行的程序的路径,char* arg是执行的命令以及命令行参数,.....(省略号其实也有含义),是指可以传多个参数,不管传多少个,最后要加一个NULL结尾

eg:

	execl("/usr/bin/ls","ls","-l",NULL);//第一个是路径下的程序位置,后边依次就是要执行的操作

​ 这里就是执行了ls程序

 int main(){
     printf("process runing....\n");
     sleep(4);
     execl("/usr/bin/ls","ls","-l","--color=auto",NULL);
     exit(10);
     printf("process exit....\n");//在这里就没有执行了,因为代码在进程替换的时候被覆盖了,所以就无法执行了
     return 0;
 }

这里运行的时候会注意到运行结果没有第6行的 printf("process exit....\n");

这是因为,在运行到execl的时候,发生了进程替换从这里开始进程的代码和数据都变成了路径里的程序,所以不能直接这样,需要用创建一个子进程来进行替换,这样不会影响父进程

所以应该让子进程来进行

4.2替换原理

在运行父进程的时候,创建完子进程但是没有调用execl()的时候,这时候,按正常父子的进程运行方式一样,两个都指向同一个物理地址,公用代码段和数据段

当子进程遇到进程替换时,因为要让路径下的程序的代码和数据段进行覆盖,所以此时发生写时拷贝,此时OS将创建一个新的区域,然后让全新程序的代码还有数据段放入创建的新的区域的内存,然后页表重新映射改位置

在此过程中没有创建新的进程,只不过只有子进程页表映射的位置变了,并且创建了一块新的内存空间,进行运行全新的程序,跟原来的代码就无关了

4.3函数解释

  • 如果函数如果调用成功,则加载新的程序从新程序起始开始执行,不再返回
  • 如果调用出错则返回-1,eg:没有找到路径或者命令行参数传错了原因
  • 只有出错的返回值,没有成功的返回值

4.4 命名理解

对于所有的这些函数,必须都带l或者v传以list或者vector的形式传命令函参数,所以有execl系列和execv系列

​ ①execl:execl,execlp,execle

​ ②exev:execv,execvp,execve

这里的execve()比较特殊,因为这一个是系统调用函数,其他的都是基于这个进行封装

l (list) : 表示参数采用列表

v (vector) : 表示参数采用数组

p (path) : 表示自动搜索环境变量中找有的程序名

e (env) : 表示自己维护环境变量,自己去手动传(可以自定义)

4.4.1 execle()函数

execle(const char* path, const char *argv , ..... ,  char* const envp[]);

问题:在一个程序被执行的时候,execve()函数 先被执行 还是 main()函数 先被执行?

答案:execve先被执行

一个程序被加载到内存中,怎么被加载到内存中?

​ ①const char* path :在Linux中,调用函数execle()来加载到内存中,所以./可执行程序就是一个加载器,传的第一个参数就是一个路径

​ ②const char *argv:后边跟的是命令行参数,最后边的环境变量一般都由系统填充,例如ls -l这两个都是命令行参数

​ ③ char* const envp[]:这个就是环境变量,其实在每个程序进行编译后,环境变量会保存在虚拟进程地址空间的最上边,这些部分保存的是命令行参数,环境变量等信息,所以子进程能拿到默认的环境变量,通过进程地址空间

​ 所以对于main()函数,也要被执行,也要被传参,所以传参是execle传给main()函数的

int main(int argc , const char* argv[] , char* const env[]){
  //int argc是由`const char *argv`的长度来进行传参,代表传了几个命令行参数
  //const char* argv[]是又execle()中的argv来传参,传的就是命令行参数列表
  //env[]由execle()中的env[]传来的
}
posted @ 2024-04-18 16:02  有志者事竟成1019  阅读(43)  评论(0)    收藏  举报