Linux---系统编程阶段-------(进程控制)

本文详细解析了进程控制中的关键函数,包括fork用于创建进程,子进程与父进程之间的数据共享与写时拷贝技术,以及进程的终止方式如exit和_exit的区别。此外,还介绍了wait和waitpid函数在等待子进程退出和释放资源上的应用。最后,讨论了exec函数族在进程间程序替换中的作用,如何执行新的程序。通过实例展示了简单的shell制造过程。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

进程创建

1.进程创建函数:
①:pit_t fork(void);
其中pit_t实际上为int类型,返回的是子进程的id。
其头文件为:#include<unistd.h>
主要作用是在已经运行的进程里面创建一个进程。原来的进程称为父进程,新建的进程称为子进程。
并且,自身的返回值为0,给父进程返回值大于0,出错返回-1。
②:进程调用fork后,当控制转移到内核终的fork代码后,内核做以下事情:

  • 分配新的内存块和内存数据结构给子进程
  • 将父进程部分数据结构内容拷贝给子进程
  • 添加子进程到系统进程列表中
  • fork返回,开始调度器调度

③:当一个进程调用fork之后,就会有两个二进制代码相同的进程。而他们都运行到相同的地方,但是每个进程都可以干他们可以独自干的事情。
例如下面代码:
在这里插入图片描述

执行结果如下:
在这里插入图片描述
这里一共输出了三行,但是我们在程序中只看到了两行,所以我们可以看出来,在使用了fork函数之后我们才创建了一个新的进程,这两个进程分布执行了最后的两行。
其中getpid() //是返回正在调度进程的id号 pid //表示返回其父进程的id号
2.进程创建之后的数据如何保管:
①:首先我们看如下代码:
在这里插入图片描述

它的执行结果如下:
在这里插入图片描述
我们可以得到的是,父进程和子进程所保留的gval是同一个地址,这是没有问题的,但是当我们将子进程中的gval进行修改,那么结果如何呢?
请看如下代码:
在这里插入图片描述
其执行结果如下:
在这里插入图片描述
我们看到,子进程和父进程中的gval值不同,但是地址相同,造成这样的原因是因为父进程创建子进程的时候,直接将父进程中的数据进行复制的同时,将父进程中保存数据的虚拟地址也进行了相同的复制,所以出现了地址相同的原因,而值不同是因为在子进程中,对gval进行了修改,所以在子进程的虚拟地址所指向的物理地址的位置发生了改变(但是虚拟地址的位置还是没变),所以才造成了这样的结果。如下图:
在这里插入图片描述
情况就如图这样一样。
但是为什么会造成这样的结果呢?正是因为有写时拷贝技术的存在。
②:写时拷贝技术
由上面的情况可以看出来,父进程和子进程的数据应该是独有的,各有各的数据,否则会相互影响。
缺点:由于创建子进程后,系统会给子进程重新开辟空间,把父进程那些数据拷贝过去,这样才能保证每个进程自己有自己的数据,但是如果拷贝过来的子进程不适用这些数据,那么开辟的空间必然会得到浪费。
改善:由于上面的缺点,所以为了独立,又为了提高效率,避免资源的浪费,因此才有了写时拷贝技术。
写时拷贝技术:刚创建出来的子进程,让子进程和父进程同时指向同一个物理内存,但是如果子进程或者父进程中有数据即将改变的时候,这时,我们针对这个要改变的内存重新开辟空间,将数据拷贝过去,并更改其父子进程中虚拟地址的指向即可。
3.pid_t vfork(void)函数
①功能:通过复制父进程来创建一个新的进程,但是父子进程公用同一块物理内存。
②特性:vfork创建子进程后,父进程会发生堵塞,会一直等到子进程退出或者替换之后然后再进行运行。
原因如下:

  • 因为父子进程公用同一块物理内存,所以数据是共享的。
  • 任意一个进程对原始数据的修改都会影响到另一个进程。
  • 通用一个栈,因此函数调用都是压的都是同一个栈,同时运行会造成栈混乱,数据造成影响。

因此让一个进程先运行,其运行结束后再让另一个进程运行即可。

进程终止

1.如何终止一个程序,我们一共有三个方法:分别是如下三种:

  • 在main函数中的return (只有在main函数中才是退出程序进行)
  • 库函数:void exit(int retval)可以在任意位置调用,退出程序的运行,头文件是#include<stdlib.h>
  • 系统调用接口:void _exit(int status)可以在任意位置调用,退出程序的运行,头文件是:#include<unistd.h>

2.return 方法是我们最常用的一种方法,而上面的exit和_exit名字很相似,一个是库函数,一个是系统调用接口,但是他们干的事情是不一样的,我们可以通过如下代码来得出结论。
首先,我们必须知道的是:库函数于系统调用接口的关系是:库函数封装了系统的调用接口,证明库函数exit的使用会调用_exit函数,但是在调用之前,还是会干其他的事情,来看如下代码:
首先我们用exit进行演示:
在这里插入图片描述
执行结果如下:
在这里插入图片描述

我们看到结果很正常,和我们使用return的情况相同。
接下来我们使用_exit进行演示:
在这里插入图片描述

执行结果如下:
在这里插入图片描述

这时,我们发现出现问题了,我们想要打印的数据没有打印出来。
经过上述的比较,我们可以看出来exit和_exit的不同之处就是刷没刷新缓冲区,而缓冲区是什么呢?
缓冲区:作为中间数据缓冲,可以将多个小数据积累成一个大数据一次操作完成。
缓冲区出现的原因:如果每次打印都要操作一次显示器设备,如果有大量的小数据要进行处理打印或者写入文件,意味着每次写入都要操作设备,那么这样的效率很低。
所以:
①:对于exit函数和main函数中的return,他们在运行结束之后会刷新缓冲区,将还没有写入文件的数据写入到文件中。
②:_exit函数没有刷新缓冲区,而是直接退出,这样操作的结果会有可能造成资源泄漏。
并且:标准输出设备有一个特性:那就是换行会刷新缓冲区。
(但是这个特性只适用于标准输出设备中)
③:return和两个exit的参数status作用为:设置进程退出码
而退出码的设定最好设定在:0~255间,因为退出码只保存底8位,解释我们会在进程等待中进行。
echo $?:显示上个函数运行完后的退出码。
特例:获取上一次系统调用出错的原因,就是查看上一步调用接口失败的原因。

void perror(const  char* str);

进程等待

1.主要的原因:等待子进程的退出,获取子进程的退出码,释放子进程的资源。避免子进程成为僵尸进程。
2.操作:
系统给了两个函数去让我们进行进程等待去等待子进程的退出,分别如下:
①wait方法:pid_t wait(int* status)
头文件为:#include<sys/wait.h>

  • 功能:阻塞等待任意一个子进程退出。(意思是,只要有任何一个子进程还没有退出,他就会堵塞进程并且一直等着,真好,哈哈哈哈哈)
  • 参数:int *status:一个int整形空间的地址,用来存放子进程的退出码。(如果不关心的话可以设置为NULL)
  • 返回值:成功返回处理子进程的pid,失败则返回-1。
  • 阻塞等待:为了完成一个功能,我们发起一个调用,如果功能不能完成那么就一直等待。

使用方法如下:
在这里插入图片描述

程序结果如下:
在这里插入图片描述

②:waitpid方法:int waitpid(pid_t pid,int* status,int option)
头文件为:#include<sys/wait.h>

  • 功能:也是等待子进程退出,但是可以等待指定的子进程,也可以进程非阻塞等待。
  • 参数:①:pid_t pid:用于指定需要等待的子进程的pid。(如果设置为-1,那就指定任意子进程);②:int *status:整形空间地址用来存放子进程的退出码;③:int option:设置堵塞标志。(其中,设置为0表示阻塞等待,设置为WNOHANG表示非阻塞等待)
  • 返回值:①:大于0:表示处理进程的pid;②:等于0:表示当前没有子进程退出(这种情况只出现在非阻塞等待中);③:小于0:表示出现错误。
  • 非阻塞等待:为了完成一个功能,我们发起来了一个调用,如果不能立即完成任务,那么就直接报错返回。

使用方法如下:
在这里插入图片描述

程序运行的结果如下:
在这里插入图片描述

注意的是:wait和waitpid并不是处理刚好退出的子进程,而是只要有子进程退出,或者已经有子进程成为僵尸进程了都会去处理,意思是,如果已经有子进程退出了,它俩就会直接去处理然后返回。

3.退出码的表示及获取:
①:上面的程序我们发现我们对退出码的操作会有右移然后再进行操作,这是因为退出码的结构是这样的:
在这里插入图片描述
其中:

  • coredump:为核心转储,表示程序异常退出前,将自己的信息保存在此处,便于到时候调试。
  • 进程退出的场景:正常退出:遇到(return,exit,_exit);异常退出:没有运行到正常位置就崩溃了。
  • 进程的退出码在进程正常退出的时候才有意义,在异常退出的时候没有意义,所以我们要在获取退出码之前做出判断,程序是否退出正常。

②:程序崩溃的本质:程序在运行的过程中出错,都是内核检测到的,当运行出现异常的时候,系统检测到会给进程一个异常的信号,进程接收到这个信号就不会在继续运行,而是退出。(只有进程的信号值为0的时候,表示程序正常退出,否则就异常退出)
③:获取退出码:
由于上面的图示,所以:

  • status & 0X7F:表示获取异常退出的信号值。
  • (status >> 8) & 0XFF:获取退出码。

进程间程序替换

1.原理:用fork创建出来的子进程往往和父进程做的是同一件事,而子进程需要调用一个exec函数来执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新的程序替换,从新程序的启动例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。
2.exec函数:
系统一共给力exec函数有六种,分别如下:

  • int execl(const char *path,const char* argv,...)
  • int execlp(consr char* filename,const char* argv,...)
  • int esecle(const char* path,const char* argv,...,char* const env[])
  • int execv(const char* path,char* const argv[])
  • int exevp(const char* filename,char* const argv[])
  • int execve(const char* filename,char* const argv[],char* const env[])
  • 他们的头文件统一为:unistd.h

3.函数解释:

  • 这些函数只要调用成功,则就加载新的函数,从启动位置开始,不再返回。
  • 如果调用错误,返回-1。
  • 所以exec函数只有出错的返回值,没有成功的返回值。

4.函数的命名理解:

  • l :表示采用参数列表。
  • v:表示采用数组。
  • p:表示采用自动的路径。
  • e:表示自己设置环境变量。

但是使用这些函数的时候,参数的设置末尾必须是NULL。
5.关于声明与定义:
①:声明:格式为:extern +数据类型+变量。
用法:声明一个变量,前提是这个变量已经被定义出来了,不用给其开辟空间。(就是早已存在这个变量
②:定义:格式为:数据类型+变量。
用法:变量原来不存在,需要开辟空间。

制造一个简单的shell

通过上面的函数,我们设置一个简单的shell,如下:
在这里插入图片描述

结果如下:
在这里插入图片描述

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值