【Linux】进程控制

简介: Linux下进程的创建、终止、等待和进程的程序替换。

一、进程创建

1. fork函数初识

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

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

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

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

我们看下面的代码:

int main()
{
   
   
  pid_t id = fork();
  int cnt = 0;
  if(id>0)
  {
   
   
    while(1)
    {
   
   
      printf("父进程,pid:%d,ppid:%d,id:%d,cnt的值:%d\n",getpid(),getppid(),id,cnt);
      sleep(1);
    }
  }
  else if(id==0)
  {
   
   
     while(1)
     {
   
   

       printf("子进程,pid:%d,ppid:%d,id:%d,cnt的值:%d\n",getpid(),getppid(),id,cnt);  
       cnt++;
       sleep(1);  
     }  
  }
  else{
   
   
    printf("fork fail\n");
     return -1;
  } 
  return 0;
}

image.png

当一个进程调用fork之后,就有两个二进制代码相同的进程。而且它们都运行到相同的地方。fork之前由父进程独立运行,fork之后父子进程分别执行。当然了,fork之后,谁先执行完全由调度器决定。

image.png

💕 如何理解fork之后,给父进程返回子进程的ID,给子进程返回0呢?

这是因为父进程可能有多个子进程,但是子进程只能有一个父进程,所以需要给父进程返回子进程的ID,将子进程管理起来。

💕 如何理解if-else-if-else语句能够同时执行呢?

当fork函数在return之前就已经将子进程创建好了,创建好的子进程可能已经在运行队列中了,而且fork之后代码是供父进程和子进程共享的,在fork内部已经有父子两个执行流了,因为两个return语句都将会被执行,但是两个执行流谁先执行是由调度器决定的,因为返回的本质是写入,所以谁先返回就将谁写入id,由于写时拷贝的发生,所以地址一样,但是内容不一样,因此 就可以让父子进程执行不同的代码了。


2. 写时拷贝

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

image.png


3. fork常规的用法

💕 fork的常规用法:

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

💕 fork调用失败的原因:

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

二、进程终止

1. 进程退出码

我们创建进程的目的是为了让进程帮我们执行任务,但是任务执行的结果如何应该如何衡量呢?如果一个程序正常运行完了,那他会有两种结果:结果正确或者结果不正确 ,一般来说,不同的退出码表示的是不同的结果。

一般来说:0表示运行结果正确,非0表示运行结果不正确。这里我们来看一下C语言中的库函数strerror,他表示从内部数组中搜索错误号errnum,然后返回一个指向错误消息的字符串的指针。

image.png
image.png

查看进程的退出码

在Linux下,我们可以使用echo $?来查看进程退出码,但在这里我们需要注意的是它==只能查看最近的一个进程的进程执行完成时的退出码。==

image.png


2. 进程退出场景及常见的退出方法

💕 进程退出场景

  • 代码运行完毕,结果正确
  • 代码运行完毕,结果不正确
  • 代码异常终止

💕 进程退出的方法

进程正常退出有如下几种方法:

  • 从main函数返回
  • 调用exit
  • 调用_exit

库函数:exit

image.png

exit的用法:

image.png

exit函数将我们的程序直接终止,并没有执行exit之后的语句,exit后,组成进程的程序,数据,进程控制块全部消失。

系统调用:_exit

image.png

下面我们对上面的程序调用,系统调用的_exit函数来看一下效果:

image.png

这里我们可以看到:它的效果和exit的效果是一样的。

_exit和exit的区别

既然exit和_exit的效果一样,那为什么要出现两份呢?其实他们还是有区别的。

image.png
image.png

这里我们需要知道的是exit的内部调用了_exit,为什么调用exit会打印,而调用_exit不会打印呢?这是因为数据将会先被写入缓冲区,待缓冲区刷新的时候才能被写入到显示器上。在上面的程序中,因为没有使用'\n'进行行缓冲的刷新。所以exit在终止程序后会刷新缓冲区,而_exit不会。但由于exit的底层封装了_exit,所以我们可以得出结论:==缓冲区并不在操作系统内部,而是在用户空间。==

💕 进程异常退出

我们的进程也是可以异常退出的,比如:Ctrl C终止进程、或者程序中有遇到野指针,/0、空指针野指针等问题。

例如,当我们在程序中使用一个整数去除零的时候:

image.png


三、进程等待

进程等待==指的是通过系统调用获取子进程退出码或者退出信号的方法。==

1. 进程等待的必要性

  • 之前讲过,子进程退出,父进程如果不管不顾,就可能造成‘僵尸进程’的问题,进而造成内存泄漏。
  • 另外,进程一旦变成僵尸状态,那就刀枪不入,“杀人不眨眼”的kill -9 也无能为力,因为谁也没有办法杀死一个已经死去的进程。
  • 最后,父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是不对,或者是否正常退出。
  • 父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息

进程等待的本质就是父进程需要通过进程等待的方式,回收子进程剩余资源(PCB,内核栈等),获取子进程退出信息,父进程需要知道子进程的退出码和执行时间等信息,形象化的比喻就是父进程通过进程等待来给僵尸进程收尸。


2. 进程等待的方法

wait方法

如果等待进程终止成功,wait函数将会返回终止进程的id值,等待失败则会返回-1,下面我们来看一下wait函数。

image.png

我们先来看一下wait的用法:

image.png
image.png

子进程在执行完之后的五秒内由于父进程处于休眠状态,所以子进程正在处于僵尸状态,五秒过后父进程调用wait回收子进程,子进程由僵尸状态退出结束了进程。最后父进程在等待五秒后自动退出。

waitpid方法

image.png

如果我们将最后一个options设置了WNOHANG,如果调用中waitpid发现没有已退出的子进程可以收集,则返回0;如果调用中出错,则返回-1,这是errno会被设置成相应的值以指示错误信息。

下面我们改一下我们的代码:

image.png
image.png

但在这里我们发现status并不是我们想要得到的111,这里的status表示的是位置信息 ,不能被当作普通的整形看待。

image.png

下面我们来看一下status如何得到 退出状态终止信号

终止信号(退出码) ==status>>8&0xFF==

退出状态(正常退出还是异常退出) ==status&0x7F==

image.png

image.png
image.png

  • wait和waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充。
  • 如果传递NULL,表示不关心子进程的退出状态信息。
  • 否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程。
  • status不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究status低16比特位):

image.png

通过宏来拿到子进程的退出信息

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

WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
image.png
image.png


3. 非阻塞等待

阻塞等待 表示的是当父进程执行到waitpid函数时,如果子进程还没有退出,父进程就只能阻塞在waitpid函数,直到子进程退出,父进程通过waitpid读取退出信息后接着执行后面的语句。

非阻塞等待 表示父进程执行到waitpid函数时,如果子进程未退出,父进程会直接读取子进程的退出状态并返回,然后接着执行后面的语句,不会等待子进程的退出。==由于非阻塞等待不会等子进程退出,所以我们需要以轮询的方式来不断获取子进程 的退出信息。==

image.png

轮询 父进程在非阻塞式状态的前提下,以循环的方式对子进程进行进程等待,直到子进程退出。

image.png
image.png


四、进程程序替换

1. 替换原理

image.png

用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。


2. 替换函数

具有六种以exec开头的函数,统称为exec函数:

#include <unistd.h>

extern char **environ;

int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execle(const char *path, const char *arg,..., char * const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[],char *const envp[]);
//系统调用
int execve(const char *path, char *const argv[], char *const envp[]);

这些函数提供了一个在进程中启动另一个程序的执行方法,可以根据指定的文件名或者目录名找到可执行程序,并用它来取代原调用进程的数据段,代码段和堆栈段。在执行完之后,原调用进程,的内容除了进程ID外,其他的内容全部被新的进程替换了。

关于这些函数,我们需要注意几点:

  • 这些函数==如果调用成功则加载新的程序从启动代码开始执行,不再返回==。
  • 如果调用出错则返回-1
  • 所以==exec函数只有出错的返回值而没有成功的返回值。==(这是因为exec函数调用成功之后,exec函数之后的代码就不会再执行了,所以exec函数调用成功之后返回值没有任何的意义,因此,需要调用失败时的返回值)

这些函数原型看起来很容易混,但只要掌握了规律就很好记。

  • l(list) : 表示参数采用列表
  • v(vector) : 参数用数组
  • p(path) : 有p自动搜索环境变量PATH
  • e(env) : 表示自己维护环境变量

image.png


execlp

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

image.png
image.png


execv

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

image.png
image.png


execvp

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

image.png
image.png
前几个函数没有传递环境变量,但是子进程依然能够通过environ拿到环境变量,是通过进程地址空间的方式让子进程拿到的。


execle

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

这个函数以及下一个我们需要讲解的函数中最后一个'e'代表的是环境变量和argv一样,我们可以显式的初始化envp(指针数组)来传递我们的系统环境变量,当然也可以传递我们的自定义的环境变量。

传递系统环境变量

image.png

image.png

image.png

这里我们可以看到,如果我们传递的是系统环境变量,所以系统环境变量被打印了出来了,那么如果我们传递的是自定义的环境变量呢?

image.png
image.png

这里我们可以看到我们仅仅获取了自定义的环境变量MYENV,而系统环境变量PATH则获取失败了,如果我们想要同时获取自定义环境变量和系统环境变量该如何做呢?在这里我们就可以使用 ==putenv将自定义环境变量导入系统环境变量。然后通过传递系统环境变量environ来实现:==

image.png
image.png


execvpe

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

image.png
image.png


五、shell的模拟实现

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

#define NUM 1024    //一个命令的最大长度
#define OPT_NUM 64  //一个命令的做多选项

char lineCommand[NUM];
char* argv[OPT_NUM];  
int EXIT_CODE;  //保存进程退出码

int main() {
   
   
    while(1) {
   
   

        //输出提示符
        printf("[用户名@主机名 当前路径]$ ");
        fflush(stdout);

        //获取输入
        char* ret = fgets(lineCommand, sizeof(lineCommand)-1, stdin);  //最后留一个位置来存放极端情况下的\0
        if( ret == NULL ) {
   
   
            perror("fgets");
            exit(1);
        }
        //消除命令行中最后的换行符
        lineCommand[strlen(lineCommand) - 1] = '\0';  

        //将输入解析为多个字符串存放到argv中,即字符串切割
        argv[0] = strtok(lineCommand, " ");
        int i = 1;
        //ls颜色显示
        if(argv[0] != NULL && strcmp(argv[0], "ls") == 0)  
        {
   
   
            argv[i++] = (char*)"--color=auto";
        }

        while(argv[i++] = strtok(NULL, " "));
        //cd改变父进程工作路径
        if(argv[0] != NULL && strcmp(argv[0], "cd") == 0)  
        {
   
   
            if(argv[1] != NULL) 
                chdir(argv[1]);  //myargv[1]中保存着指定路径
            continue;
        }
        //处理echo内建命令
        if(argv[0] != NULL && strcmp(argv[0], "echo") == 0) 
        {
   
   
            if(strcmp(argv[1], "{
   
   mathJaxContainer[0]}?
                printf("%d\n", EXIT_CODE);
                EXIT_CODE = 0;
            } else {
   
     //echo $变量
                printf("%s\n", argv[1]+1);
            }
            continue;
        }

        //创建子进程
        pid_t id = fork();
        if(id == -1) {
   
   
            perror("fork");
            exit(1);
        } else if (id == 0) {
   
     //子进程
            int ret = execvp(argv[0], argv);  //进程程序替换
            if(ret == -1) {
   
     
                printf("No such file or directory\n");
                exit(1);
            }
        } else {
   
     //父进程
            int status = 0;
            pid_t ret = waitpid(id, &status, 0);  //进程等待
            EXIT_CODE = (status >> 8) & 0xFF;  //获取退出状态
            if(ret == -1){
   
   
                perror("wait");
                exit(1);
            }
        } 
    }
    return 1;
}
相关实践学习
CentOS 7迁移Anolis OS 7
龙蜥操作系统Anolis OS的体验。Anolis OS 7生态上和依赖管理上保持跟CentOS 7.x兼容,一键式迁移脚本centos2anolis.py。本文为您介绍如何通过AOMS迁移工具实现CentOS 7.x到Anolis OS 7的迁移。
相关文章
|
13天前
|
资源调度 Linux 调度
Linux c/c++之进程基础
这篇文章主要介绍了Linux下C/C++进程的基本概念、组成、模式、运行和状态,以及如何使用系统调用创建和管理进程。
26 0
|
2月前
|
网络协议 Linux
Linux查看端口监听情况,以及Linux查看某个端口对应的进程号和程序
Linux查看端口监听情况,以及Linux查看某个端口对应的进程号和程序
441 2
|
2月前
|
Linux Python
linux上根据运行程序的进程号,查看程序所在的绝对路径。linux查看进程启动的时间
linux上根据运行程序的进程号,查看程序所在的绝对路径。linux查看进程启动的时间
58 2
|
8天前
|
算法 Linux 调度
深入理解Linux操作系统的进程管理
【10月更文挑战第9天】本文将深入浅出地介绍Linux系统中的进程管理机制,包括进程的概念、状态、调度以及如何在Linux环境下进行进程控制。我们将通过直观的语言和生动的比喻,让读者轻松掌握这一核心概念。文章不仅适合初学者构建基础,也能帮助有经验的用户加深对进程管理的理解。
12 1
|
13天前
|
消息中间件 Linux API
Linux c/c++之IPC进程间通信
这篇文章详细介绍了Linux下C/C++进程间通信(IPC)的三种主要技术:共享内存、消息队列和信号量,包括它们的编程模型、API函数原型、优势与缺点,并通过示例代码展示了它们的创建、使用和管理方法。
16 0
Linux c/c++之IPC进程间通信
|
13天前
|
Linux C++
Linux c/c++进程间通信(1)
这篇文章介绍了Linux下C/C++进程间通信的几种方式,包括普通文件、文件映射虚拟内存、管道通信(FIFO),并提供了示例代码和标准输入输出设备的应用。
15 0
Linux c/c++进程间通信(1)
|
13天前
|
Linux C++
Linux c/c++之进程的创建
这篇文章介绍了在Linux环境下使用C/C++创建进程的三种方式:system函数、fork函数以及exec族函数,并展示了它们的代码示例和运行结果。
17 0
Linux c/c++之进程的创建
|
1月前
|
Linux Shell
6-9|linux查询现在运行的进程
6-9|linux查询现在运行的进程
|
13天前
|
Linux C++
Linux c/c++进程之僵尸进程和守护进程
这篇文章介绍了Linux系统中僵尸进程和守护进程的概念、产生原因、解决方法以及如何创建守护进程。
15 0
|
2月前
|
消息中间件 Linux 开发者
Linux进程间通信秘籍:管道、消息队列、信号量,一文让你彻底解锁!
【8月更文挑战第25天】本文概述了Linux系统中常用的五种进程间通信(IPC)模式:管道、消息队列、信号量、共享内存与套接字。通过示例代码展示了每种模式的应用场景。了解这些IPC机制及其特点有助于开发者根据具体需求选择合适的通信方式,促进多进程间的高效协作。
114 3