Linux - 进程控制

1. 进程概述

从严格意义上来讲,程序和进程是两个不同的概念,他们的状态,占用的系统资源都是不同的。

  • 程序:就是磁盘上的可执行文件文件,并且只占用磁盘上的空间,是一个静态的概念。
  • 进程:被执行之后的程序叫做进程,不占用磁盘空间,需要消耗系统的内存,CPU资源,每个运行的进程的都对应一个属于自己的虚拟地址空间,这是一个动态的概念。

1.1 并行和并发

  • CPU 时间片

    CPU 在某个时间点只能处理一个任务,但是操作系统都支持多任务的,那么在计算机 CPU 只有一个和的情况下是怎么完成多任务处理的呢?原理和古时候救济灾民的思路是一样的,每个人分一点,但是又不叫吃饱。

    CPU 会给每个进程被分配一个时间段,进程得到这个时间片之后才可以运行,使各个程序从表面上看是同时进行的。如果在时间片结束时进程还在运行,CPU 的使用权将被收回,该进程将会被中断挂起等待下一个时间片。如果进程在时间片结束前阻塞或结束,则 CPU 当即进行切换,这样就可以避免 CPU 资源的浪费。

    因此可以得知,在我们使用的计算机中启动的多个程序,从宏观上看是同时运行的,从微观上看由于 CPU 一次只能处理一个进程,所有它们是轮流执行的,只不过切换速度太快,我们感觉不到罢了,因此 CPU 的核数越多计算机的处理效率越高。

     

  • 并发和并行

   这两个概念呢都可以笼统的解释为:多个进程同时运行,但是他们两个的同时并不是同一个概念。Erlang 之父 Joe Armstrong 用一张 5 岁小孩都能看懂的图解释了并发与并行的区别:


并发:第一幅图是并发。

  • 并发的同时运行是一个假象,咖啡机也好 CPU 也好在某一个时间点只能为某一个个体来服务,因此不可能同时处理多任务,这是通过上图的咖啡机 / 计算机的 CPU 快速的时间片切换实现的。
  • 并发是针对某一个硬件资源而言的,在某个时间段之内处理的任务的总量,量越大效率越高。
  • 并发也可以理解为是一个屌丝通过不断努力自我升华的结果。

 

并行:第二幅图是并行。

  • 并行的多进程同时运行是真实存在的,可以在同一时刻同时运行多个进程
  • 并行需要依赖多个硬件资源,单个是无法实现的(图中有两台咖啡机)。
  • 并行可以理解为是一个高富帅,出生就有天然的硬件优势,资源多自然办事效率就高。

 

1.2 PCB

PCB - 进程控制块(Processing Control Block),Linux 内核的进程控制块本质上是一个叫做 task_struct 的结构体。在这个结构体中记录了进程运行相关的一些信息,下面介绍一些常用的信息:

  • 进程 id:每一个进程都一个唯一的进程 ID,类型为 pid_t, 本质是一个整形数
  • 进程的状态:进程有不同的状态,状态是一直在变化的,有就绪、运行、挂起、停止等状态。
  • 进程对应的虚拟地址空间的信息。
  • 描述控制终端的信息,进程在哪个终端启动默认就和哪个终端绑定。
  • 当前工作目录:默认情况下,启动进程的目录就是当前的工作目录
  • umask 掩码:在创建新文件的时候,通过这个掩码屏蔽某些用于对文件的操作权限。
  • 文件描述符表:每个被分配的文件描述符都对应一个已经打开的磁盘文件
  • 和信号相关的信息:在 Linux 中 调用函数 , 键盘快捷键 , 执行shell命令等操作都会产生信号。
  • 阻塞信号集:记录当前进程中阻塞哪些已产生的信号,使其不能被处理
    未决信号集:记录在当前进程中产生的哪些信号还没有被处理掉。
  • 用户 id 和组 id:当前进程属于哪个用户,属于哪个用户组
  • 会话(Session)和进程组:多个进程的集合叫进程组,多个进程组的集合叫会话。
  • 进程可以使用的资源上限:可以使用 shell 命令 ulimit -a 查看详细信息。

1.4 进程状态

进程一共有五种状态分别为:创建态,就绪态,运行态,阻塞态(挂起态),退出态(终止态) 其中创建态和退出态维持的时间是非常短的,稍纵即逝。我们主要是需要将就绪态 , 运行态 , 挂起态,三者之间的状态切换搞明白。

就绪态:万事俱备,只欠东风(CPU资源)

  • 进程被创建出来了,有运行的资格但是还没有运行,需要抢 CPU 时间片
  • 得到 CPU 时间片,进程开始运行,从就绪态转换为运行态。
  • 进程的 CPU 时间片用完了,再次失去 CPU, 从运行态转换为就绪态。

运行态:获取到 CPU 资源的进程,进程只有在这种状态下才能运行

  • 运行态不会一直持续,进程的 CPU 时间片用完之后,再次失去 CPU,从运行态转换为就绪态
  • 只要进程还没有退出,就会在就绪态和运行态之间不停的切换。

阻塞态:进程被强制放弃 CPU,并且没有抢夺 CPU 时间片的资格

  • 比如:在程序中调用了某些函数(比如: sleep ()),进程又运行态转换为阻塞态(挂起态)
  • 当某些条件被满足了(比如:slee () 睡醒了),进程的阻塞状态也就被解除了,进程从阻塞态转换为就绪态。

退出态:进程被销毁,占用的系统资源被释放了

  • 任何状态的进程都可以直接转换为退出态。

1.5 进程命令

在研究如何创建进程之前,先来看一下如何在终端中通过命令完成进程相关的操作。

  • 查看进程
$ ps aux
	- a: 查看所有终端的信息
	- u: 查看用户相关的信息
	- x: 显示和终端无关的进程信息

如果特别想知道每个参数控制着哪些信息,可以通过 ps a, ps u, ps x 分别查看。

  • 杀死进程

kill 命令可以发送某个信号到对应的进程,进程收到某些信号之后默认的处理动作就是退出进程,如果要给进程发送信号,可以先查看一下 Linux 给我们提供了哪些标准信号。

查看 Linux 中的标准信号:

$ kill -l
 1) SIGHUP       2) SIGINT       3) SIGQUIT      4) SIGILL       5) SIGTRAP
 6) SIGABRT      7) SIGBUS       8) SIGFPE       9) SIGKILL     10) SIGUSR1
11) SIGSEGV     12) SIGUSR2     13) SIGPIPE     14) SIGALRM     15) SIGTERM
16) SIGSTKFLT   17) SIGCHLD     18) SIGCONT     19) SIGSTOP     20) SIGTSTP
21) SIGTTIN     22) SIGTTOU     23) SIGURG      24) SIGXCPU     25) SIGXFSZ
26) SIGVTALRM   27) SIGPROF     28) SIGWINCH    29) SIGIO       30) SIGPWR
31) SIGSYS      34) SIGRTMIN    35) SIGRTMIN+1  36) SIGRTMIN+2  37) SIGRTMIN+3
38) SIGRTMIN+4  39) SIGRTMIN+5  40) SIGRTMIN+6  41) SIGRTMIN+7  42) SIGRTMIN+8
43) SIGRTMIN+9  44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9  56) SIGRTMAX-8  57) SIGRTMAX-7
58) SIGRTMAX-6  59) SIGRTMAX-5  60) SIGRTMAX-4  61) SIGRTMAX-3  62) SIGRTMAX-2
63) SIGRTMAX-1  64) SIGRTMAX



9 号信号(SIGKILL)的行为是无条件杀死进程,想要杀死哪个进程就可以把这个信号发送给这个进程,操作如下:

# 无条件杀死进程, 进程ID通过 ps aux 可以查看
$ kill -9 进程ID
$ kill -SIGKILL 进程ID

 

2. 进程创建

2.1 fork函数

Linux 中进程 ID 为 pid_t 类型,其本质是一个正整数,通过上边的 ps aux 命令已经得到了验证。PID 为 1 的进程是 Linux 系统中创建的第一个进程。

  • 获取当前进程的进程 ID(PID)
pid_t getpid(void);
  • 获取当前进程的父进程 ID(PPID)
pid_t getppid(void);
  • 创建一个新的进程
pid_t fork(void);
pid_t vfork(void);
  • 基本代码框架
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
 
int main()
{
    pid_t pid;
    pid = fork();
    if(pid < 0)
    {
        perror("file to fork");
        return -1;
    }
    if(pid == 0)
    {
        //子进程的代码段
    }
    else
    {
        //父进程的代码段
    }
    return 0;
}

 

小贴士:

Linux 中看似创建一个新的进程非常简单,函数连参数都没有,实际上如果想要真正理解这个函数还是得死几个脑细胞。

 

2.2 fork剖析

 

pid_t fork(void);

 

启动磁盘上的应用程序,得到一个进程,如果在这个启动的进程中调用 fork() 函数,就会得到一个新的进程,我们习惯将其称之为子进程。前面说过每个进程都对应一个属于自己的虚拟地址空间,子进程的地址空间是基于父进程的地址空间拷贝出来的,虽然是拷贝但是两个地址空间中存储的信息不可能是完全相同的,下图是拷贝之后父子进程各自的虚拟地址空间:

相同点:

拷贝完成之后(注意这个时间点),两个地址空间中的用户区数据是相同的。用户区数据主要数据包括:

  • 代码区:默认情况下父子进程地址空间中的源代码始终相同。
  • 全局数据区:父进程中的全局变量和变量值全部被拷贝一份放到了子进程地址空间中
  • 堆区:父进程中的堆区变量和变量值全部被拷贝一份放到了子进程地址空间中
  • 动态库加载区(内存映射区):父进程中数据信息被拷贝一份放到了子进程地址空间中
  • 栈区:父进程中的栈区变量和变量值全部被拷贝一份放到了子进程地址空间中
  • 环境变量:默认情况下,父子进程地址空间中的环境变量始终相同。
  • 文件描述符表: 父进程中被分配的文件描述符都会拷贝到子进程中,在子进程中可以使用它们打开对应的文件。

区别:

  • 父子进程各自的虚拟地址空间是相互独立的,不会互相干扰和影响。
  • 父子进程地址空间中代码区代码虽然相同,但是父子进程执行的代码逻辑可能是不同的。
  • 由于父子进程可能执行不同的代码逻辑,因此地址空间拷贝完成之后,全局数据区 , 栈区 , 堆区 , 动态库加载区(内存映射区) 数据会各自发生变化,由于地址空间是相互独立的,因此不会互相覆盖数据。
  • 由于每个进都有自己的进程 ID,因此内核区存储的父子进程 ID 是不同的。
  • 进程启动之后进入就绪态,运行需要争抢 CPU 时间片而且可能执行不同的业务逻辑,所以父子进程的状态可能是不同的。
  • fork () 调用成功之后,会返回两个值,父子进程的返回值是不同的。
  • 该函数调用成功之后,从一个虚拟地址空间变成了两个虚拟地址空间,每个地址空间中都会将 fork() 的返回值记录下来,这就是为什么会得到两个返回值的原因。
  • 父进程的虚拟地址空间中将该返回值标记为一个大于 0 的数(其实记录的是子进程的进程 ID)
  • 子进程的虚拟地址空间中将该返回值标记 0
  • 在程序中需要通过 fork () 的返回值来判断当前进程是子进程还是父进程。
int main()
{
    // 在父进程中创建子进程
    pid_t pid = fork();
    printf("当前进程fork()的返回值: %d\n", pid);
    if(pid > 0)
    {
        // 父进程执行的逻辑
        printf("我是父进程, pid = %d\n", getpid());
    }
    else if(pid == 0)
    {
        // 子进程执行的逻辑
        printf("我是子进程, pid = %d, 我爹是: %d\n", getpid(), getppid());
    }
    else // pid == -1
    {
        // 创建子进程失败了
    }
    
    // 不加判断, 父子进程都会执行这个循环
    for(int i=0; i<5; ++i)
    {
        printf("%d\n", i);
    }
    
    return 0;
}

 

3. 父子进程

3.1 进程执行位置

在父进程中成功创建了子进程,子进程就拥有父进程代码区的所有代码,那么子进程中的代码是在什么位置开始运行的呢?父进程肯定是从 main () 函数开始运行的,子进程是在父进程中调用 fork () 函数之后被创建,子进程就从 fork () 之后开始向下执行代码。


上图中演示了父子进程中代码的执行流程,可以看到如果在程序中对 fork() 的返回值做了判断,就可以控制父子进程的行为,如果没有做任何判断这个代码块父子进程都可以执行。在编写多进程程序的时候,一定要将代码想象成多份进行分析,因为直观上看代码就一份,但实际上数据都是多份,并且多份数据中变量名都相同,但是他们的值却不一定相同。

3.2 循环创建子进程

掌握了进程创建函数之后,实现一个简单的功能,在一个父进程中循环创建 3 个子进程,也就是最后需要得到 4 个进程,1 个父进程,3 个子进程,为了方便验证程序的正确性,要求在程序中打印出每个进程的进程 ID。

下面是编写的代码:

// process_loop.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>

int main()
{
    for(int i=0; i<3; ++i)
    {
        pid_t pid = fork();
        printf("当前进程pid: %d\n", getpid());
    }

    return 0;
}


编译并执行上面的代码,得到了如下结果:

# 编译
$ gcc process_loop.c

# 执行
$ ./a.out
# 最终得到了 8个进程
当前进程pid: 18774     ------ 1
当前进程pid: 18774     ------ 1
当前进程pid: 18774     ------ 1
当前进程pid: 18777     ------ 2
当前进程pid: 18776     ------ 3
当前进程pid: 18776     ------ 3
当前进程pid: 18775     ------ 4
当前进程pid: 18775     ------ 4
当前进程pid: 18775     ------ 4
当前进程pid: 18778     ------ 5
当前进程pid: 18780     ------ 6
当前进程pid: 18779     ------ 7
当前进程pid: 18779     ------ 7
当前进程pid: 18781     ------ 8


通过程序打印的信息发现程序循环了三次,最终得到了 8 个进程,也就是创建出了 7 个子进程,还是上面跟大家讲的那句话,对应多进程的程序,一定要代码分成很多份去分析,并且如果没有在程序中加条件控制,所有的代码父子进程都是有资格执行的。接下来分析上边的编写的代码,通过画图的方式分析为什么得到了 7 个子进程:

上图中的树状结构,蓝色节点代表父进程:

  • 循环第一次 i = 0,创建出一个子进程,即红色节点,子进程变量值来自父进程拷贝,因此 i=0
  • 循环第二次 i = 1,蓝色父进程和红色子进程都去创建子进程,得到两个紫色进程,子进程变量值来自父进程拷贝,因此 i=1
  • 循环第三次 i = 2,蓝色父进程和红色、紫色子进程都去创建子进程,因此得到 4 个绿色子进程,子进程变量值来自父进程拷贝,因此 i=2
  • 循环第三次 i = 3,所有进程都不满足条件 for(int i=0; i<3; ++i) 因此不进入循环,退出了。

通过上面的分析,最终得到解决方案,我们可以只让父进程创建子进程,如果是子进程不让其继续创建子进程,因此只需要在程序中添加关于父子进程的判断即可。

修改之后的代码如下:

// 需要在上边的程序中控制不让子进程, 再创建子进程即可
// process_loop.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>

int main()
{
    pid_t pid;
    // 在循环中创建子进程
    for(int i=0; i<3; ++i)
    {
        pid = fork();
        if(pid == 0)
        {
            // 不让子进程执行循环, 直接跳出
            break;
        }
    }
    printf("当前进程pid: %d\n", getpid());

    return 0;
}


最后编译并执行程序,查看最终结果,可以看到最后确实得到了 4 个不同的进程,pid 最小的为父进程,其余为子进程:

# 编译
$ gcc process_loop.c

# 执行
$ ./a.out
当前进程pid: 2727
当前进程pid: 2730
当前进程pid: 2729
当前进程pid: 2728


在多进程序中,进程的执行顺序是没有规律的,因为所有的进程都需要在就绪态争抢CPU时间片,抢到了就执行,抢不到就不执行,但是不用担心,默认进程的优先级是相同的,操作系统不会让某一个进程一直抢不到CPU时间片。

3.3 终端显示问题

在执行多进程程序的时候,经常会遇到下图中的问题,看似进程还没有执行完成,貌似是因为什么原因被阻塞了,实际上终端是正常的,当我们通过键盘输入一些命令,终端也能接受输入并且输出相关信息,那么为什么终端会显示成这个样子呢?

  1. a.out 进程启动之后,共创建了 3 个子进程,其实 a.out 也是有父进程的就是当前的终端
  2. 终端只能检测到 a.out 进程的状态,a.out 执行期间终端切换到后台,a.out 执行完毕之后终端切换回前台
  3. 当终端切换到前之后,a.out 的子进程还没有执行完毕,当子进程输出的信息就显示到终端命令提示符的后边了,导致终端显示有问题,但是此时终端是可以接收键盘输入的,只是看起来不美观而已。
  4. 想要解决这个问题,需要让所有子进程退出之后再退出父进程,比如:在父进程代码中调用 sleep ()
pid_t pid = fork();
if(pid > 0)
{
    sleep(3);	// 让父进程睡一会儿
}
else if(pid == 0)
{
    // 子进程
}



3.4 进程数数

思考一个问题,当父进程创建一个子进程,那么父子进程之间可以通过全局变量互动,实现交替数数的功能吗?不过不确定可以写一段测试代码:

// number.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>

// 定义全局变量
int number = 10;

int main()
{
    printf("创建子进程之前 number = %d\n", number);

    pid_t pid = fork();
    // 父子进程都会执行这一行
    printf("当前进程fork()的返回值: %d\n", pid);

    //如果是父进程
    if(pid > 0)
    {
        printf("我是父进程, pid = %d, number = %d\n", getpid(), ++number);
        printf("父进程的父进程(终端进程), pid = %d\n", getppid());
        sleep(1);
    }
    else if(pid == 0)
    {
        // 子进程
        number += 100;
        printf("我是子进程, pid = %d, number = %d\n", getpid(), number);
        printf("子进程的父进程, pid = %d\n", getppid());
    }

    return 0;
}


编译程序并测试:

$ gcc number.c
$ ./a.out 
创建子进程之前 number = 10
当前进程fork()的返回值: 3513
当前进程fork()的返回值: 0
我是子进程, pid = 3513, number = 110
子进程的父进程, pid = 3512

我是父进程, pid = 3512, number = 11	# 没有接着子进程的110继续数,父子进程各玩各的,测试失败了
父进程的父进程(终端进程), pid = 2175

通过验证得到结论:两个进程中是不能通过全局变量实现数据交互的,因为每个进程都有自己的地址空间,两个同名全局变量存储在不同的虚拟地址空间中,二者没有任何关联性。如果要进行进程间通信需要使用:管道,共享内存,本地套接字,内存映射区,消息队列等方式。

4. exec函数蔟

在项目开发过程中,有时候有这种需求,需要通过现在运行的进程启动磁盘上的另一个可执行程序,也就是通过一个进程启动另一个进程,这种情况下我们可以使用 exec族函数。

函数原型如下:

#include <unistd.h>
 
extern char **environ;
 
int execl(const char *path, const char *arg, ...,/* (char  *) NULL */);
int execlp(const char *file, const char *arg, ...,/* (char  *) NULL */);
int execle(const char *path, const char *arg, ...,/*, (char *) NULL, char * const envp[] */);
 
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execve(const char *filename, char *const argv[],char *const envp[]);

返回值:
六个函数返回∶如果执行成功则函数不会返回,执行失败则直接返回-1,失败原因存于errno 中。

参数:
这些函数之间的第一个区别是前四个取路径名作为参数,后两个则取文件名作为参数。

 

1. execl()//用来执行参数path字符串所代表的文件路径,接下来的参数代表执行该文件时传递过去的argv(0)、argv[1]……,最后一个参数必须用空指针(NULL)作结束。

2. execle()//执行参数path字符串所代表的文件路径,接下来的参数代表执行该文件时传递过去的argv(0)、argv[1]……,最后一个参数必须用空指针(NULL)作结束。与execl不同的是,带e表示该函数取envp[]数组,而不使用当前环境。

3. execlp()//会从PATH 环境变量所指的目录中查找符合参数file的文件名,找到后便执行该文件,然后将第二个以后的参数当做该文件的argv[0]、argv[1]……,最后一个参数必须用空指针(NULL)作结束。

4. execv()//用来执行参数path字符串所代表的文件路径,与execl()不同的地方在于execve()只需两个参数,第二个参数利用数组指针来传递给执行文件。

5. execvp()//会从PATH 环境变量所指的目录中查找符合参数file 的文件名,找到后便执行该文件,然后将第二个参数argv传给该欲执行的文件。

6. execve()//用来执行参数filename字符串所代表的文件路径,第二个参数系利用数组指针来传递给执行文件,最后一个参数则为传递给执行文件的新环境变量数组。

/*错误代码:*/
EACCES
1. 欲执行的文件不具有用户可执行的权限。
2. 欲执行的文件所属的文件系统是以noexec 方式挂上。
3.欲执行的文件或script翻译器非一般文件。
EPERM
1.进程处于被追踪模式,执行者并不具有root权限,欲执行的文件具有SUID 或SGID 位。
2.欲执行的文件所属的文件系统是以nosuid方式挂上,欲执行的文件具有SUID 或SGID 位元,但执行者并不具有root权限。
E2BIG 参数数组过大
ENOEXEC 无法判断欲执行文件的执行文件格式,有可能是格式错误或无法在此平台执行。
EFAULT 参数filename所指的字符串地址超出可存取空间范围。
ENAMETOOLONG 参数filename所指的字符串太长。
ENOENT 参数filename字符串所指定的文件不存在。
ENOMEM 核心内存不足
ENOTDIR 参数filename字符串所包含的目录路径并非有效目录
EACCES 参数filename字符串所包含的目录路径无法存取,权限不足
ELOOP 过多的符号连接
ETXTBUSY 欲执行的文件已被其他进程打开而且正把数据写入该文件中
EIO I/O 存取错误
ENFILE 已达到系统所允许的打开文件总数。
EMFILE 已达到系统所允许单一进程所能打开的文件总数。
EINVAL 欲执行文件的ELF执行格式不只一个PT_INTERP节区
EISDIR ELF翻译器为一目录
ELIBBAD ELF翻译器有问题。

 

也就是说 exec族函数并没有创建新进程的能力,只是有大无畏的牺牲精神,让起启动的新进程寄生到自己虚拟地址空间之内,并挖空了自己的地址空间用户区,把新启动的进程数据填充进去。

exec族函数中最常用的有两个 execl() 和 execlp(),这两个函数是对其他 4 个函数做了进一步的封装,下面介绍一下。

 

4.1 execl()

该函数可用于执行任意一个可执行程序,函数需要通过指定的文件路径才能找到这个可执行程序。

#include <unistd.h>
// 变参函数
int execl(const char *path, const char *arg, ...);


参数:

path: 要启动的可执行程序的路径,推荐使用绝对路径

arg: ps aux 查看进程的时候,启动的进程的名字,可以随意指定,一般和要启动的可执行程序名相同

... : 要执行的命令需要的参数,可以写多个,最后以 NULL 结尾,表示参数指定完了。

返回值:如果这个函数执行成功,没有返回值,如果执行失败,返回 -1

4.2 execlp()

该函数常用于执行已经设置了环境变量的可执行程序,函数中的 p 就是 path,也是说这个函数会自动搜索系统的环境变量 PATH,因此使用这个函数执行可执行程序不需要指定路径,只需要指定出名字即可。

// p == path
int execlp(const char *file, const char *arg, ...);


参数:

file: 可执行程序的名字

在环境变量 PATH 中,可执行程序可以不加路径

没有在环境变量 PATH 中,可执行程序需要指定绝对路径

arg: ps aux 查看进程的时候,启动的进程的名字,可以随意指定,一般和要启动的可执行程序名相同

... : 要执行的命令需要的参数,可以写多个,最后以 NULL 结尾,表示参数指定完了。

返回值:如果这个函数执行成功,没有返回值,如执行失败,返回 -1

4.3 函数的使用

关于 exec 族函数,我们一般不会在进程中直接调用,如果直接调用这个进程的代码区代码被替换也就不能按照原来的流程工作了。我们一般在调用这些函数的时候都会先创建一个子进程,在子进程中调用 exec 族函数,子进程的用户

数据被替换掉开始执行新的程序中的代码逻辑,但是父进程不受任何影响仍然可以继续正常工作。

execl () 或者 execlp () 函数的使用方法如下:

示例1:

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


int main()
{
    // 创建子进程
    pid_t pid = fork();
    // 在子进程中执行磁盘上的可执行程序
    if(pid == 0)
    {
        // 磁盘上的可执行程序 /bin/ps
#if 1
        execl("/bin/ps", "title", "aux", NULL);
        // 也可以这么写
        // execl("/bin/ps", "title", "a", "u", "x", NULL);  
#else
        execlp("ps", "title", "aux", NULL);
        // 也可以这么写
        // execl("ps", "title", "a", "u", "x", NULL);
#endif
        // 如果成功当前子进程的代码区别 ps中的代码区代码替换
        // 下面的所有代码都不会执行
        // 如果函数调用失败了,才会继续执行下面的代码
        perror("execl");
        printf("++++++++++++++++++++++++\n");
        printf("++++++++++++++++++++++++\n");
        printf("++++++++++++++++++++++++\n");
        printf("++++++++++++++++++++++++\n");
        printf("++++++++++++++++++++++++\n");
        printf("++++++++++++++++++++++++\n");
    }
    else if(pid > 0)
    {
        printf("我是父进程.....\n");
    }

    return 0;
}

示例2:

//execl_test.c
 
#include<unistd.h>
 
int main()
{
    execl("/bin/ls","ls","-al","/usr",(char * )0);
    
    return 0;
}
//execlp_test.c
 
#include<unistd.h>
 
int main()
{
    execlp("ls","ls","-al","/usr",(char *)0);
 
    return 0;
}

 

5. 进程控制

进程控制主要是指进程的退出 , 进程的回收和进程的特殊状态 孤儿进程和僵尸进程。

5.1 结束进程

如果想要直接退出某个进程可以在程序的任何位置调用 exit() 或者_exit() 函数。函数的参数相当于退出码,如果参数值为 0 程序退出之后的状态码就是 0, 如果是 100 退出的状态码就是 100。

// 专门退出进程的函数, 在任何位置调用都可以
// 标准C库函数
#include <stdlib.h>
void exit(int status);

// Linux的系统函数
// 可以这么理解, 在linux中 exit() 函数 封装了 _exit()
#include <unistd.h>
void _exit(int status);


在 main 函数中直接使用 return 也可以退出进程,假如是在一个普通函数中调用 return 只能返回到调用者的位置,而不能退出进程。

// ***** return 必须要在main()函数中调用, 才能退出进程 *****
// 举例:
// 没有问题的例子
int main()
{
    return 0;	// 进程退出了
}

////////////////////////// 不能退出的例子 //////////////////////////

int func()
{
    return 666;	// 返回到调用者调用该函数的位置, 返回到 main() 函数的第19行
}

int main()
{
    // 调用这个函数, 当前进程能不能退出? ===> 不能
    int ret = func();
}

 

5.2 进程回收

为了避免僵尸进程的产生,一般我们会在父进程中进行子进程的资源回收,回收方式有两种,一种是阻塞方式 wait(),一种是非阻塞方式 waitpid()。

5.2.1 wait

这是个阻塞函数,如果没有子进程退出,函数会一直阻塞等待,当检测到子进程退出了,该函数阻塞解除回收子进程资源。这个函数被调用一次,只能回收一个子进程的资源,如果有多个子进程需要资源回收,函数需要被调用多次。

函数原型如下:

// man 2 wait
#include <sys/wait.h>

pid_t wait(int *status);


      
参数:传出参数,通过传递出的信息判断回收的进程是怎么退出的,如果不需要该信息可以指定为 NULL。取出整形变量中的数据需要使用一些宏函数,具体操作方式如下:

WIFEXITED(status): 返回 1, 进程是正常退出的

WEXITSTATUS(status):得到进程退出时候的状态码,相当于 return 后边的数值,或者 exit () 函数的参数

WIFSIGNALED(status): 返回 1, 进程是被信号杀死了

WTERMSIG(status): 获得进程是被哪个信号杀死的,会得到信号的编号

返回值:

成功:返回被回收的子进程的进程 ID

失败: -1

没有子进程资源可以回收了,函数的阻塞会自动解除,返回 - 1

回收子进程资源的时候出现了异常

下面代码演示了如何通过 wait() 回收多个子进程资源:

// wait 函数回收子进程资源
#include <sys/wait.h>

int main()
{
    pid_t pid;
    // 创建子进程
    for(int i=0; i<5; ++i)
    {
        pid = fork();
        if(pid == 0)
        {
            break;
        }
    }

    // 父进程
    if(pid > 0)
    {
        // 需要保证父进程一直在运行
        while(1)
        {
            // 回收子进程的资源
            // 子进程由多个, 需要循环回收子进程资源
            pid_t ret = wait(NULL);
            if(ret > 0)
            {
                printf("成功回收了子进程资源, 子进程PID: %d\n", ret);
            }
            else
            {
                printf("回收失败, 或者是已经没有子进程了...\n");
                break;
            }
            printf("我是父进程, pid=%d\n", getpid());
        }
    }
    else if(pid == 0)
    {
        // 子进程, 执行这句代码之后, 子进程退出了
        printf("我是子进程, pid=%d, 父进程ID: %d\n", getpid(), getppid());
    }
    return 0;
}


5.2.2 waitpid

waitpid () 函数可以看做是 wait () 函数的升级版,通过该函数可以控制回收子进程资源的方式是阻塞还是非阻塞,另外还可以通过该函数进行精准打击,可以精确指定回收某个或者某一类或者是全部子进程资源。

该函数函数原型如下:

// man 2 waitpid
#include <sys/wait.h>
// 这个函数可以设置阻塞, 也可以设置为非阻塞
// 这个函数可以指定回收哪些子进程的资源
pid_t waitpid(pid_t pid, int *status, int options);

参数:

pid:

-1:回收所有的子进程资源,和 wait () 是一样的,无差别回收,并不是一次性就可以回收多个,也是需要循环回收的

大于0:指定回收某一个进程的资源 ,pid 是要回收的子进程的进程 ID

0:回收当前进程组的所有子进程 ID

小于 -1:pid 的绝对值代表进程组 ID,表示要回收这个进程组的所有子进程资源

status: NULL, 和 wait 的参数是一样的

options: 控制函数是阻塞还是非阻塞

0: 函数是行为是阻塞的 ==> 和 wait 一样

WNOHANG: 函数是行为是非阻塞的

返回值:

如果函数是非阻塞的,并且子进程还在运行,返回 0

成功:得到子进程的进程 ID

失败: -1

没有子进程资源可以回收了,函数如果是阻塞的,阻塞会解除,直接返回 - 1

回收子进程资源的时候出现了异常

下面代码演示了如何通过 waitpid() 阻塞回收多个子进程资源:

// 和wait() 行为一样, 阻塞
#include <sys/wait.h>

int main()
{
    pid_t pid;
    // 创建子进程
    for(int i=0; i<5; ++i)
    {
        pid = fork();
        if(pid == 0)
        {
            break;
        }
    }

    // 父进程
    if(pid > 0)
    {
        // 需要保证父进程一直在运行
        while(1)
        {
            // 回收子进程的资源
            // 子进程由多个, 需要循环回收子进程资源
            int status;
            pid_t ret = waitpid(-1, &status, 0);  // == wait(NULL);
            if(ret > 0)
            {
                printf("成功回收了子进程资源, 子进程PID: %d\n", ret);
                                // 判断进程是不是正常退出
                if(WIFEXITED(status))
                {
                    printf("子进程退出时候的状态码: %d\n", WEXITSTATUS(status));
                }
                if(WIFSIGNALED(status))
                {
                    printf("子进程是被这个信号杀死的: %d\n", WTERMSIG(status));
                }
            }
            else
            {
                printf("回收失败, 或者是已经没有子进程了...\n");
                break;
            }
            printf("我是父进程, pid=%d\n", getpid());
        }
    }
    else if(pid == 0)
    {
        // 子进程, 执行这句代码之后, 子进程退出了
        printf("===我是子进程, pid=%d, 父进程ID: %d\n", getpid(), getppid());
    }
    return 0;
}

 

下面代码演示了如何通过 waitpid() 非阻塞回收多个子进程资源:

// 非阻塞处理
#include <sys/wait.h>

int main()
{
    pid_t pid;
    // 创建子进程
    for(int i=0; i<5; ++i)
    {
        pid = fork();
        if(pid == 0)
        {
            break;
        }
    }

    // 父进程
    if(pid > 0)
    {
        // 需要保证父进程一直在运行
        while(1)
        {
            // 回收子进程的资源
            // 子进程由多个, 需要循环回收子进程资源
            // 子进程退出了就回收, 
            // 没退出就不回收, 返回0
            int status;
            pid_t ret = waitpid(-1, &status, WNOHANG);  // 非阻塞
            if(ret > 0)
            {
                printf("成功回收了子进程资源, 子进程PID: %d\n", ret);
                // 判断进程是不是正常退出
                if(WIFEXITED(status))
                {
                    printf("子进程退出时候的状态码: %d\n", WEXITSTATUS(status));
                }
                if(WIFSIGNALED(status))
                {
                    printf("子进程是被这个信号杀死的: %d\n", WTERMSIG(status));
                }
            }
            else if(ret == 0)
            {
                printf("子进程还没有退出, 不做任何处理...\n");
            }
            else
            {
                printf("回收失败, 或者是已经没有子进程了...\n");
                break;
            }
            printf("我是父进程, pid=%d\n", getpid());
        }
    }
    else if(pid == 0)
    {
        // 子进程, 执行这句代码之后, 子进程退出了
        printf("===我是子进程, pid=%d, 父进程ID: %d\n", getpid(), getppid());
    }
    return 0;
}

 

 

————————————————

作者: 苏丙榅

链接: https://subingwen.cn/linux/process/#5-4-2-waitpid

来源: 爱编程的大丙

著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

posted @ 2023-01-06 11:50  [BORUTO]  阅读(20)  评论(0编辑  收藏  举报