文章目录
1. 进程创建
1.1 fork函数
Linux中进程创建使用的是fork函数
使用fork函数,创建一个子进程。fork函数有返回值,它将子进程PID返回给父进程,而给子进程返回0。如果给父进程返回了-1,则说明函数调用失败。
另外,fork函数的返回值是pid_t类型,这个类型不在unistd.h头文件中定义,而是在sys/types.h头文件中,所以我们在使用fork函数时,还要包含sys/types.h头文件。
这样一串简单的代码可以由父进程创建子进程,并执行不同的任务。
在调用fork函数之后,内核做了以下几件事:
- 分配新的内存块和数据结构给子进程
- 将父进程的部分数据结构拷贝给子进程
- 添加子进程到系统进程列表当中
- fork返回,开始调度器调度
1.2 写时拷贝
在fork进程创建的时候,为了保证父进程和子进程的独立性,操作系统用到了写时拷贝的技术。
1.3 fork函数的使用场景
- 当需要父子进程执行不同的代码段的时候,我们要用到fork函数,比如在网络通信中,用父进程获取客户端的请求,然后让子进程处理请求
- 进行进程的程序替换,这个在文章的下文会说
2. 进程终止
进程终止我们往往是通过三种方法:
return num; exit(num); 或者是向进程发出终止信号
2.1 通过return终止
平常写的C语言代码,在任务执行完毕之后,总要return 0; 它就是一个进程终止的标志,0这个数字表示进程终止,且进程运行的结果正确,如果不是return 0;而是return其他数字,则说明进程虽然正常结束,但是结果是不正确的!
而每次return的那个数字,也就叫做进程退出码!
在系统中,我们可以使用echo $?命令查看上一次进程结束的退出码
系统规定了进程退出码表示的意义,也就是说,0表示成功,当return其他数字时,指定唯一的运行错误。通过strerror函数,我们可以打印进程退出码代表的信息。
错误信息是很多的,在Linux中总共有一百三十多个,这里我就截了十几个。
总结一下,return返回的数字可以表示不同的运行结果,如果是0表示结果没问题,如果是其他数字则表示出现了指定的错误。
2.2 通过exit函数终止
exit函数的作用和return一样,都是表示进程正常终止,然后返回不同的数字表示进程的运行结果是否正确,基本上和return的作用差别不大。
值得注意的是,return只能在main函数中终止进程,在其他函数中的作用也只是终止函数的调用。而exit函数可以在任何函数中终止进程。
_exit函数
exit函数是C语言的库函数,而_exit函数是系统的函数,这两个函数有不同的地方,下面我们写代码演示一下。
运行结果:
可以发现,是可以把printf中的信息打印出来的,也就是说exit函数是可以刷新缓冲区的。
下面看看_exit函数
运行结果:
可以看到,_exit函数是不会刷新缓冲区的。
结论:exit函数刷新缓冲区,_exit函数不刷新缓冲区。
exit函数是C语言级别的函数,_exit函数是系统系统级别的函数。其实exit函数内部封装了_exit函数。
在调用了exit函数时,exit函数干了两件事,刷新缓冲区和调用_exit函数。
这里的缓冲区是用户层的缓冲区,而不是系统层的,如果是系统层,那么_exit函数也能刷新缓冲区了。
3.3 发出信号,进程异常终止
使用kill信号来终止进程,如kill -9直接杀死进程等。
使用信号终止进程的话,说明进程不是正常终止的。这时候进程的退出码也就不重要了,我们只用关心进程的终止信号。
发出信号使进程异常终止由两种情况:
- 用户使用kill命令杀死进程
- 进程出现访问空指针,除0等错误,操作系统检测到之后向进程发出终止信号。
3. 进程等待
3.1 进程等待简介
父进程等待子进程的完成,从而回收子进程的资源,就叫做进程等待。在进程等待期间,或许父进程什么也没做,也或许父进程做了一些其他的事情,这取决了程序员是如何调用的。
我们进行进程等待,通常是为了解决子进程的僵尸进程问题。在子进程运行结束之后,父进程需要知道子进程运行的怎样并对子进程进行回收。如果父进程一直不管运行结束的子进程,子进程的资源也就会一直不被回收,这就会造成内存泄漏的问题。
3.2 进程等待的方式
3.2.1 wait函数
使用wait函数,父进程对子进程进行进程等待,回收子进程的资源,但是没法知道子进程运行的怎样了。
这个函数调用成功会返回子进程的PID,调用失败时会返回-1。
这个wait函数非常简单,但是它不是重点,因为它无法指定具体要等待哪一个子进程。下面我们介绍更一个等待函数
3.2.2 waitpid函数
它的第一个参数是要等待的子进程的pid,第二个参数status是输出型参数,保存子进程的运行情况(它是一个位图结构,用一个变量同时表示退出码和终止信号),第三个参数表示等待方式(是阻塞式等待还是非阻塞式等待)。
waitpid函数的返回值可能有以下几种情况:
- 如果调用成功,并且提供了一个有效的子进程ID,返回值是结束子进程的进程ID。
- 如果waitpid调用失败,返回值为-1。这通常表示出现了错误,可以通过检查errno变量来获取具体的错误信息。
- 如果提供的子进程ID不存在或不是当前进程的子进程,返回值为0。这种情况下,waitpid函数不会阻塞,也不会等待子进程的结束。
status变量
这个变量是一个位图结构,它的不同比特位保存不同的数据。它是32位比特位的int整数,但是我们只研究前16位,后16位不考虑。
如果进程正常终止,即return或者exit函数,那么status变量前八位为0,后八位保存进程的退出码。
如果进程异常终止,即被用户或者操作系统kill,那么status变量前七个位保存终止信号,第八位为core dump标志(这个暂时先不考虑),后八位未使用,默认为0。
通过下面的方式得到进程的退出码和退出信号
下面我们来试着用一下waitpid函数
上面的代码中,父进程等待子进程的方式是阻塞式等待,即在等待期间,父进程不能做任何事情,它只能阻塞地等待子进程完成并回收子进程的资源。
假如我们想让父进程在等待期间做点其他的事情呢?进行非阻塞式等待。
waitpid函数的第三个参数就决定了等待方式是阻塞式等待还是非阻塞式等待,上面的代码我们设置为了0,表示阻塞式等待。如果要进行非阻塞式等待,可以传入WNOHANG(W为wait,NO为不的意思,HANG为悬挂)。
下面进行演示。
设置一个while循环,每次循环时调用一次waitpid函数,如果返回值为0,说明子进程还没有退出,父进程就做下它自己的事情。如果返回值大于0,说明父进程回收子进程成功,获取退出信息后退出循环。如果返回值为-1,说明waitpid调用失败,退出循环。
4. 进程程序替换
4.1 进程程序替换是什么
进程程序替换是指在一个正在运行的进程中,将当前运行的程序完全替换为另一个程序的过程。当进程需要执行不同的程序或任务时,可以通过进程程序替换来实现。这个过程中,原来正在执行的程序的代码,数据和执行状态都会被新的程序所替代。
进程程序替换一般包括以下步骤:
- 加载新的程序和数据:操作系统会将新的程序和相关数据从磁盘加载到内存中
- 建立新程序的运行环境:操作系统会创建新程序的执行环境,包括初始化新的栈空间,堆空间和其他必要资源
- 替换当前运行的程序:操作系统将当前运行的程序的代码,数据和执行状态完全替换为新的程序的代码,数据和执行装填
- 开始执行新的程序:一旦替换完成,操作系统会将处理器的控制权转移到新的程序的入口点,使其开始执行
进程程序替换允许在一个进程的上下文中切换不同的程序,从而实现更灵活的应用场景和任务切换。它是任务操作系统中的重要机制之一,为进程提供了动态性和可扩展性。
4.2 进程替换函数
在C语言中,有六个进程替换的函数。
注意:这些函数在调用失败时返回-1,而没有调用成功的返回值。原因是如果调用成功,那么进程就已经被替换掉了,就不存在返回值了。
int execl(const char *path, const char *arg, …);
第一个参数是可执行程序的路径,第二个参数和后面的…表示可变参数列表,传入参数的个数是可变,且以NULL结尾。
假如我们要程序替换为某个命令,平时在命令行怎么写命令,从第二个参数开始也就怎么传参。
l是list的意思,表示将参数一个一个地传入函数中
可以加入各种命令行参数。
int execlp(const char file, const char arg, …);
p为path的意思,“execlp” 函数的特点是可以使用文件名来代替完整的文件路径。它会在系统的环境变量中查找可执行文件的路径,以便找到要执行的程序
int execle(const char *path, const char *arg, …, char *const envp[])
第一个参数是路径,第二个参数是可变参数列表,第三个参数是自己传入的环境变量。
这样,在mycmd程序内部就可以使用envp_中自己设置的环境变量了
当然,也可以传入系统自带的环境变量。
如
int execv(const char *path, char *const argv[]);
v表示vector的意思,即数组。可以将我们要在命令行中输入的内容打包到数组中,然后传给进程。
int execvp(const char *file, char *const argv[]);
从上面的函数就可以知道,第一个参数表示不用传入路径,根据文件名通过path去找文件路径,第二个参数传入执行的方法数组。
int execve(const char *path, char *const argv[], char *const envp[]);
这个也不需要我多说啥了,v表示传入命令行数组,e为环境变量。
上面六个函数,都是C语言层面的函数,其实它们都是封装了一个系统层面的函数。
C语言将这一个系统调用接口封装成了六个函数,本质上就是为了满足不同的应用需求。