Linux-进程控制详解(进程创建+进程终止+进程等待+进程程序替换)

本文详细介绍了Linux系统中的进程控制,包括进程创建(fork和vfork)、进程终止、进程等待(wait和waitpid)以及进程程序替换(exec函数簇)。在进程创建中,讲解了fork函数的工作原理和写时拷贝技术,以及vfork的特殊之处。进程等待部分解释了如何避免僵尸进程和获取子进程退出状态。最后,阐述了exec函数簇在进程程序替换中的作用和不同成员之间的区别。

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

1. 进程创建

1.1 fork

在Linux中,我们通常使用fork函数来为一个已经存在的进程创建一个新进程。而这个新创建出来的进程被称为原进程的子进程,原进程被称为该进程的父进程。

该函数其实是一个系统调用接口,原型如下:

#include <unistd.h>
pid_t fork(void);

特性:子进程会复制父进程的PCB,二者之间代码共享,数据独有,拥有各自的进程虚拟地址空间

此时可能会有一个疑问,既然代码共享,并且子进程是拷贝了父进程的PCB,虽然他们各自拥有自己的进程虚拟地址空间,但其中的数据必然是相同的(拷贝而来),并且通过页表映射到同一块物理内存中,那么又如何做到数据独有呢?答案是:通过写时拷贝技术。

写时拷贝技术:子进程创建出来后,与父进程映射访问同一块物理内存,但当父子进程当中有任意一个进程更改了内存中的数据时,会给子进程重新在物理内存中开辟一块空间,并将数据拷贝过去。 这样避免了直接给子进程重新开辟内存空间,造成内存数据冗余。换句话说,如果父子进程都不更改内存中的值,那他们二者各自的进程虚拟地址空间通过页表映射,始终是指向同一块物理内存。

正是通过这样的写时拷贝技术,才保证了父子进程代码共享但数据独有的这一特性。 对于一些小萌新来说,可能上述文字描述并不是那么直观,此处有必要上图来进一步说明一下:

如父进程中有全局变量g_val初值为10,子进程创建之后通过复制父进程的PCB,并且二者的进程虚拟地址空间通过页表映射到同一块物理内存,但如果子进程更改了g_val的值,就会在物理内存中开辟新的空间并保存属于子进程的g_val:
在这里插入图片描述

在知道了以上特性后,下面我们来认识一下fork函数的返回值,相当重要!

通过以上函数原型我们可以看到起返回值是pid_t类型,其实就是int,在内核中是通过typedef重命名过的,我们把其当做int类型即可。

如果创建子进程失败,会返回-1,是小于0的,而如果创建子进程成功,该函数则会返回俩个值,这一点和普通的函数有很大区别。它会给子进程返回0值,而给父进程返回子进程的pid(一个大于0的数),也正是通过给父子进程返回值的不同,从而我们可以使用选择语句对齐进行分流,从而让父子进程执行不同的代码,而达到我们创建子进程的某种目的。

在了解到这一点之后,我们便可以通过代码来创建子进程并且进一步验证前面说到的一些特性。

#include <stdio.h>
#include <unistd.h>

//父子进程代码共享,但数据独有
int g_val = 100;
int main()
{
   
    pid_t pid = fork();//创建子进程
    if(pid < 0) {
   
        printf("fork error!\n");
        return -1; 
    }
    else if(pid == 0) {
   
        //子进程
        g_val = 200;
        printf("This is Child! g_val = %d p = %p\n",g_val,&g_val);
    }
    else {
   
        //父进程
        sleep(1);
        printf("This is Parent! g_val = %d p = %p\n",g_val,&g_val);
    }
    return 0;
}

运行程序,得到如下结果:
在这里插入图片描述
对于这一结果感到惊讶吗?其实只要你看懂了我上面所说的内容,相信这个结果并不难理解:子进程拷贝父进程的PCB,拥有和父进程一模一样的进程虚拟地空间以及数据,但子进程将自己的g_val更改后,会在物理内存中为其重新开辟空间来存储子进程更改后的数据,而结果中看到的地址完全相同,则是因为它们仅仅是虚拟的地址空间,真正的值是存储在物理内存中的。而这时通过页表的映射,这俩个看似相同的地址已经指向了不同的物理内存。

1.2 vfork

不止可以通过fork来创建子进程,vfork也同样是用来创建子进程的系统调用函数,那么它和fork有什么区别呢?

#include <sys/types.h>
#include <unistd.h>
pid_t vfork(void);

通过函数原型我们似乎并不能看出什么端倪,的确,vfork在使用时和fork几乎没有什么区别,返回值及其含义也和fork完全相同。其和fork的区别在于,用v_fork创建出来的子进程,也是拷贝父进程的PCB,但它和父进程共享同一个进程虚拟地址空间。也就是如下图所示的这样:
在这里插入图片描述
但是我们要思考一个问题,父子进程共享同一个进程虚拟地址空间不会有问题吗?会的!会造成调用栈混乱的问题! 举个例子,如果父进程中调用Test函数首先压栈,之后子进程则调用Fun函数,由于二者共享同一个栈空间,则Fun函数也会继续压栈,但如果此时父进程的Test函数调用完毕想要返回,却发现其并不在栈顶位置,无法出栈,这不就有问题了吗?
在这里插入图片描述

那怎么解决呢?vfork采用的方案是,在其创建出子进程之后,让子进程先执行,而父进程则会阻塞,直到子进程执行完毕,父进程才会开始执行,这样就避免了调用栈混乱的问题。

但是!这个问题是解决了,可是新的问题也随之而来了呀,我们创建子进程难道不是为了让其而父进程并发的跑或者说更高效的完成一些任务吗,而现在再子进程退出前父进程什么都不能做,这难道不会影响效率吗?或者说的再直白一些,不是浪费时间吗???

不得不说,确实。可能也正是因为这些种种的缺点,vfork这个函数已然逐渐的被时代淘汰了,fork它不香吗?为什么要用vfork呢? 博主也理解不了它存在的意义…不过也罢,我们只需稍作了解,然后还是把爱全都给fork吧!
在这里插入图片描述

2. 进程终止

含义:进程终止的含义就是一个进程的退出。

进程退出的场景

  1. 程序运行完毕,从main函数中退出
    1.1 运行完毕,结果正确
    1.2 运行完毕,结果不正确
  2. 程序没有运行完毕,中途奔溃了

进程常见退出方法:

1. 正常退出:

  1. 从main函数返回
  2. 调用exit函数
  3. 调用_exit函数

2. 异常退出: Ctrl+C,信号终止等

exit函数:

#include <stdlib.h>
void exit(int status);

其中,stauts定义了进程的终止状态,由用户自己传递&#

评论 16
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值