【Linux】——进程控制

目录

进程创建

fork函数

fork函数返回值 

 写时拷贝

fork的常规用法 

 fork调用失败的原因

进程终止 

进程退出场景 

进程退出码

 退出码的概念

退出码的意义

进程退出的方式 

正常退出 

三种正常退出方式的联系

异常退出 

进程等待 

进程等待的必要性 

进程等待的方法 

wait方法 

waitpid方法 

子进程的status 

进程程序替换

创建子进程的目的

理解进程程序替换的原理 

程序替换的本质 

 多进程的进程替换

进程替换函数

 execl

 execlp

execv

execvp

execle

execvpe

execve

 命名理解


进程创建

fork函数

在linux中fork函数时非常重要的函数,它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。

#include <unistd.h>
pid_t fork(void);
返回值:自进程中返回0,父进程返回子进程id,出错返回-1

进程调用fork,当控制转移到内核中的fork代码后,内核做:

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

当一个进程调用fork之后,就有两个二进制代码相同的进程。而且它们都运行到相同的地方。但每个进程都将可以开始它们自己的旅程。看下面这段代码。

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
    pid_t pid;
    printf("Before:pid is %d\n",getpid());
    pid = fork();

    if(pid == -1)//fork()失败
    {
        perror("fork()");
        exit(1);
    }
    
    if(pid == 0)//子进程
    {
        printf("I am child:pid is %d,fork return:%d\n",getpid(),pid);
    }


    if(pid > 0)//父进程
    {
        printf("I am parent:pid is %d,fork return:%d\n",getpid(),pid);
    }

    sleep(1);
    return 0;
}

运行结果如下:

 这里可以看到,Before只输出了一次,而After输出了两次。其中,Before是由父进程打印的,而调用fork函数之后打印的两个After,则分别由父进程和子进程两个进程执行。也就是说,fork之前父进程独立执行,而fork之后父子两个执行流分别执行。 

fork函数返回值 

从上面的运行结果中,我们不难发现fork有两个返回值: 

        子进程返回0,

        父进程返回的是子进程的pid。 

        一个父进程可以创建多个子进程,而一个子进程只能有一个父进程。因此,对于子进程来说,父进程是不需要被标识的;而对于父进程来说,子进程是需要被标识的,因为父进程创建子进程的目的是让其执行任务的,父进程只有知道了子进程的PID才能很好的对该子进程指派任务。

        父进程调用fork函数后,为了创建子进程,fork函数内部将会进行一系列操作,包括创建子进程的进程控制块、创建子进程的进程地址空间、创建子进程对应的页表等等。子进程创建完毕后,操作系统还需要将子进程的进程控制块添加到系统进程列表当中,此时子进程便创建完毕了。也就是说,在fork函数内部,子进程就已经创建完毕了,返回值时因为有两个进程所以能够返回两个不同的值。

 写时拷贝

通常,父子代码共享,父子再不写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方式各自一份副本。具体见下图:


 只有当父进程或子进程需要修改数据时,才将父进程的数据在内存当中拷贝一份,然后再进行修改。这种在需要进行数据修改时再进行拷贝的技术,称为写时拷贝技术。

fork的常规用法 

  • 一个父进程希望复制自己,使父子进程同时执行不同的代码段。例如,父进程等待客户端请求,生成子进程来处理请求。
  • 一个进程要执行一个不同的程序。例如子进程从fork返回后,调用exec函数。

 fork调用失败的原因

  •  系统中有太多的进程
  • 实际用户的进程数超过了限制

进程终止 

进程退出场景 

一个进程从运行到结束的过程只会有下面三种情况:

  • 代码跑完了,结果正确 ——return 0
  • 代码跑完了,结果不正确 ——return !0
  • 代码没跑完,程序异常了

进程退出码

 退出码的概念

 C/C++的代码总是从main函数开始,从main函数结束。当我们的代码运行起来就变成了进程,进程结束后main函数的返回值实际上就是该进程的进程退出码,我们可以用echo $?命令查看最近一个进程退出的退出码信息。

 0表示结果正确,非0表示结果错误

代码执行成功只有一种情况,成功了就是成功了,而代码执行错误却有多种原因,例如内存空间不足、非法访问以及栈溢出等等,我们就可以用这些非0的数字分别表示代码执行错误的原因。 

C语言当中的strerror函数可以通过错误码,获取该错误码在C语言当中对应的错误信息:

输出结果: 

 实际上Linux中的ls、pwd等命令都是可执行程序,使用这些命令后我们也可以查看其对应的退出码。这些命令成功执行后,其退出码也是0。但是命令执行错误后,其退出码就是非0的数字,该数字具体代表某一错误信息。

退出码的意义

0表示成功,!0表示失败,!0具体是几表示不同的错误,数字主要是对计算机友好,其实不方便人阅读 一般而言,退出码都有对应的退出码的文字描述:(1)可以自定义 (2)可以使用系统的映射关系(但使用不太频繁)

退出码都有对应的字符串含义,帮助用户确认执行失败的原因,而这些退出码具体代表什么含义是人为规定的,不同环境下相同的退出码的字符串含义可能不同。

进程退出的方式 

正常退出 

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

return退出

在main函数中使用return退出进程是我们常用的方法。执行return n等同于执行exit(n),因为调用main的运行时函数会将main的返回值当做 exit的参数。例如,在main函数最后使用return退出进程。

exit函数

使用exit函数退出进程也是我们常用的方法,exit函数可以在代码中的任何地方退出进程,并且exit函数在退出进程前会做一系列工作:

  1. 执行用户通过atexit或on_exit定义的清理函数。
  2. 关闭所有打开的流,所有的缓存数据均被写入。
  3. 调用_exit函数终止进程。

_exit系统调用

使用_exit函数退出进程的方法我们并不经常使用,_exit函数也可以在代码中的任何地方退出进程,但是_exit函数会直接终止进程,并不会在退出进程前会做任何收尾工作。

三种正常退出方式的联系

只有在main函数当中的return才能起到退出进程的作用,子函数当中return不能退出进程,而exit函数和_exit函数在代码中的任何地方使用都可以起到退出进程的作用。

使用exit函数退出进程前,exit函数会执行用户定义的清理函数、冲刷缓冲,关闭流等操作,然后再终止进程,而_exit函数会直接终止进程,不会做任何收尾工作。

异常退出 

  • 情况一:向进程发生信号导致进程异常退出
  • 情况二:代码错误导致进程运行时异常退出 

情况一 

情况二 :出现除0的错误

 


进程等待 

进程等待的必要性 

  1. 子进程退出,父进程如果不读取子进程的退出信息,子进程就会变成僵尸进程,进而造成内存泄漏。
  2. 进程一旦变成僵尸进程,那么就算是kill -9命令也无法将其杀死,因为谁也无法杀死一个已经死去的进程。
  3. 对于一个进程来说,最关心自己的就是其父进程,因为父进程需要知道自己派给子进程的任务完成的如何。
  4. 父进程需要通过进程等待的方式,回收子进程资源,获取子进程的退出信息。
     

进程等待的方法 

wait方法 

#include<sys/types.h>
#include<sys/wait.h>
pid_t wait(int*status)

返回值:
        成功返回被等待进程pid,失败返回-1。
参数:
        输出型参数,获取子进程退出状态,不关心则可以设置成为NULL

 测试wait

运行结果:

waitpid方法 

pid_ t waitpid(pid_t pid, int *status, int options);
返回值:

  • 当正常返回的时候waitpid返回收集到的子进程的进程ID;
  • 如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回 0;
  • 如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;

参数:
        pid:

  • Pid=-1,等待任一个子进程。与wait等效。
  •  Pid>0.等待其进程ID与pid相等的子进程。

        status:

  •  WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
  • WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)

        options:

  • WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的ID。

 测试waitpid

运行结果

子进程的status 

下面进程等待所使用的两个函数wait和waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统进行填充。如果对status参数传入NULL,表示不关心子进程的退出状态信息。否则,操作系统会通过该参数,将子进程的退出信息反馈给父进程。

status是一个整型变量,但status不能简单的当作整型来看待,status的不同比特位所代表的信息不同,具体细节如下(只研究status低16比特位):

 在status的低16比特位当中,高8位表示进程的退出状态,即退出码。进程若是被信号所杀,则低7位表示终止信号,而第8位比特位是core dump标志。

 我们通过一系列位操作,就可以根据status得到进程的退出码和退出信号

exitCode = (status >> 8) & 0xFF; //退出码
exitSignal = status & 0x7F;      //退出信号 

 系统中给我们提高了更便捷的使用方式,可以用两个宏来获取退出码和退出信号。

  • WIFEXITED(status):用于查看进程是否是正常退出,本质是检查是否收到信号。
  • WEXITSTATUS(status):用于获取进程的退出码。
exitNormal = WIFEXITED(status);  //是否正常退出
exitCode = WEXITSTATUS(status);  //获取退出码

进程程序替换

创建子进程的目的

  1. 让子进程执行父进程代码的一部分
  2. 让子进程执行一个全新的程序

想要完成目的1,我们可以通过添加if条件判断使子进程可以执行父进程对应的磁盘代码中的一部分。而如果我们想让子进程执行一个新程序的话,子进程就必须要加载到磁盘上指定的程序,从而执行新程序的代码和数据。这时候就需要用到进程程序替换了。

理解进程程序替换的原理 

程序替换的本质 

将指定程序的代码和数据加载到指定的位置,覆盖自己原有的代码和数据

 进程替换的时候不会创建新的进程

 下面看具体实例

替换成功的例子

 替换失败的例子

 对于进程替换函数,调用失败会返回-1,调用成功则没有返回值。进程替换函数不需要成功的返回值,因为替换成功后面的代码就无关了,判断返回值的逻辑也就毫无意义,所以execl只需要返回错误处理。

 多进程的进程替换

在使用进程替换的时候,我们大都会选择对子进程进程替换,因为进程具有独立性,子进程替换不会影响父进程。

 子进程进行程序替换时,虚拟地址空间+页表保证进程独立性:一旦有执行流想要替换代码或者数据,就会发生写时拷贝。子进程进行程序替换,将要执行的程序的代码和数据替换到物理内存上时为了避免影响父进程会发生写时拷贝,然后重新构建页表的映射,此时子进程就替换成功可以执行全新的程序而不影响父进程了。

进程替换函数

 execl

int execl(const char *path,const char *arg, ... ) 

  • ​参数1 path:找到要执行哪一个程序,需要传入程序的路径
  • 参数2 arg:如何执行,即在命令行中怎么使用就怎么传参
  • 可变参数列表:参数个数不固定 所有的exec函数的传参都要以null结尾,以证明传参完毕 
//使用样例
execl("/usr/bin/ls", "ls", "-a", "-l", "--color=auto", NULL);

 execlp

 int execlp(const char *file, const char *arg, ...)

  • p:path:如何找到程序的功能(不需要程序的路径,只需要程序名,会自动在环境变量$PATH,进行可执行程序的查找) 
//使用样例
execlp("ls", "ls", "-a", "-l", "--color=auto", NULL);

execv

 int execv(const char *path,char *const argv[])

  • v:vector:可以将所有的执行参数,放入数组中,统一传递,而不用进行使用可变参数方案(也不要忘记带null) 
//使用样例
char *const argv_[] = {
            "ls",
           "-a",
           "-l",
           "--color=auto",
           NULL
           };
execv("/usr/bin/ls", argv_);

execvp

int execvp(const char *file, char *const argv[]) 

 上面两种的组合使用

execle

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

  • e:自定义环境变量
  • 可以使用putenv()添加环境变量 
char *const envp_[] = {
           (char*)"MYENV=11112222233334444",
            NULL
        };
extern char **environ;
execle("./mybin", "mybin", NULL, envp_); //自定义环境变量
putenv((char*)"MYENV=4443332211"); //将指定环境变量导入到系统中 environ指向的环境变量表
execle("./mybin", "mybin", NULL, environ); //实际上,默认环境变量你不传,子进程也能获取

execvpe

 int execvpe(const char *file, char *const argv[],char *const envp[])

上面三种的组合 

execve

int execve(const char *filename, char *const argv[],char *const envp[])

 系统调用接口,上面都是为了方便使用基于此的封装,本质上调用的都是此函数。

 命名理解

这六个exec系列函数的函数名都以exec开头,其后缀的含义如下:

  • l(list):表示参数采用列表的形式,一一列出。
  • v(vector):表示参数采用数组的形式。
  • p(path):表示能自动搜索环境变量PATH,进行程序查找。
  • e(env):表示可以传入自己设置的环境变量。
函数名参数格式是否带路径是否使用当前环境变量
execl列表
execlp列表
execle列表否,需自己组装环境变量
execv数组
execvp数组
execve数组否,需自己组装环境变量

 下图为exec系列函数族之间的关系:

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

hrimkn

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值