进程控制:进程创建,进程终止,进程等待,进程替换,模拟实现shell

在前面的博客中我们知道了什么是进程,进程的一些信息,知道了这些基础的内容之后(可以参考我收藏夹中linux中的内容),我们就要开始对进程进行控制使用它了;

进程创建

创建进程

想要创建进程,我们可以把我们的可执行程序加载到内存中,使其成为进程;

fork创建进程

除此之外,我们还可以用进程创建进程,这就要用到我们之前学习到的fork函数;这个函数可以通过我们的进程创建子进程;为什么这个函数可以帮助我们创建一个进程呢,因为这个函数就是一个接口,我们通过这个接口让操作系统替我们做事从而创建的子进程和父进程一样拥有pcb结构体,数据,代码;而代码和数据都是我们在写父进程的代码的时候所输入或者说所产生的,这些东西是我们写的,而pcb结构体是操作系统创造出来的,所以子进程的pcb结构体必定是独立的,是自己独有一份的(进程独立性也满足),而代码一般我们是只读不修改与写的所以我们的代码一般子进程与父进程是共享的,剩下的数据数据是可读可写的,我们进程分流的时候,当数据发生改变时才会分离(写时拷贝);

上面就是fork创建子进程做的事情;

之后我们的子进程与父进程会一起运行,做他们自己的事情,之后子进程与父进程的执行顺序由调度器决定运行顺序随机;

为什么要写时拷贝呢?

既然数据可能会被写(被修改),为什么还要等到要修改的时候才分离呢,这是因为我们的数据不一定会全部都会被修改,我们可能只会修改一部分,甚至都只会读一部分,如果我们全部的分离的话会导致重复的数据出现在内存中占用内存的空间(冗余的数据)这样会降低我们内存使用效率;所以写时拷贝是一个非常聪明的分离数据的方式减少了空间的浪费;

拓展:

fork的返回值在子进程和父进程中不同,在子进程中返回0父进程中返回子进程的pid当创建进程失败的时候会返回-1;

进程终止

进程终止就是进程退出了;

进程退出又会有不同的情况:

1.程序运行完获得正确结果退出,返回值一般为0

2.程序运行完获得错误结果退出,返回值一般为非0的数(可以用不同的数来告诉操作者错误的原因方便我们去定位错误找bug)

3.程序运行错误退出,程序直接崩溃了

我们可以使用echo $?来查看返回值(linux)

上面几种情况出现的场景一般是(退出方法):

正常退出:

1.退出可以是程序运行完了自动退出,如return,一般是获得了我们需要的结果的时候正常退出

2.也可以是调用exit函数或者_exit接口我们运行到了某个地方我们人为的退出,

异常退出:

3.最后我们如果程序运行错误了会有操作系统或者我们人为输入指令让操作系统接受信号,通过信号来终止我们的进程;

接下来我们讲讲我们的exit函数;

下面是两个函数的简介和头文件

上面_exit的头文件好像错了应该是<unistd>头文件

exit函数

这个函数我们应该或多或少早就在我们的C语言或者C++中就见到过了,一般是用在我们的main函数之外的函数中,在函数中使用exit可以直接终止进程;我们可以使用exit来返回我们的退出码给我们的父进程,父进程接受退出码(用echo $?查看进程退出码)有了这个exit我们就可以在进程之间进程返回值的判断,通过返回值我们可以知道我们的进程的运行结果是不是我们所需要的结果如果结果错误就可以根据返回值来判断我们错误的原因;

exit是我们的C语言的库函数接口;它做的事情是在C语言库中封装了的一系列的动作;我们调用它的时候其实它的底层还会调用我们的系统接口_exit这个接口才是告诉系统让系统帮我们做事的接口;

_exit系统接口 

这个接口就是我们的exit库函数的底层接口了,他就是单纯的做终止进程的接口,如果我们想要做其他的事情让我们的函数做相应的事情就要对接口进行封装再附加我们自己的行为;

下面看我们的代码我们就会知道exit真的是比_exit多做了一些事情;

 看我们上图使用exit函数的成功打印出来了我们的字符串,而_exit的接口并没有刷新我们的缓存区直接终止了我们的进程;

这些是查询到资料上说的exit封装所作的行为:

1.执行用户通过atexit或on-exit定义的清理函数

2.关闭所有的流,并刷新缓冲区

3.调用_exit 

进程等待

进程等待可以与我们之前讲到的僵尸进程相关联,还记得我们之前说的僵尸进程吗?

僵尸进程可以参考这篇博客:我在这篇博客的进程的进程状态处有讲到计算机结构,操作系统管理,进程-CSDN博客icon-default.png?t=N7T8https://blog.csdn.net/m0_75260318/article/details/134076206?spm=1001.2014.3001.5502

当我们创建了子进程而子进程死亡后父进程没有进行回收这个我们的子进程的pcb结构体仍然存在在我们的内中,占用我们的内存空间,这个时候的僵尸进程没办法被任何信号杀死连kill -9这个型号都无法做到;并且由于我们的父进程一般是不会退出的(因为我们的大的进程一般跑起来之后除开更新与维护的时候就一般不会终止如LOL这种游戏)这个时候就会导致内存泄漏的发生;

所以我们在创建了子进程的时候子进程终止一定要回收我们的进程;我们如何回收我们的进程呢?这个时候我们的进程等待wait/waitpid系统调用;

wait接口

下面是我通过chat gpt获得的介绍:

在C语言中,wait系统调用是用于等待子进程结束并获取其终止状态的函数。它的头文件是<sys/wait.h>。

wait系统调用的主要作用是阻塞父进程,直到一个子进程终止。在子进程终止之前,父进程将一直等待。当子进程终止后,父进程可以通过wait系统调用获取子进程的终止状态,并且可以获取一些关于子进程终止的其他信息。

wait系统调用有几个常用的参数和返回值:
- pid_t wait(int *status):等待任意一个子进程终止,并获取其终止状态。status参数是一个指向整型变量的指针,用于存储子进程的终止状态。
- pid_t waitpid(pid_t pid, int *status, int options):等待指定的子进程pid终止,并获取其终止状态。status参数是一个指向整型变量的指针,用于存储子进程的终止状态。options参数是一些选项,用于指定等待的行为。

wait系统调用的返回值有以下几种情况:
- 如果成功等待到一个子进程的终止,返回该子进程的进程ID。
- 如果没有子进程可以等待(没有子进程或者子进程都不在终止状态),返回-1,并设置errno为ECHILD。
- 如果调用被一个信号中断,返回-1,并设置errno为EINTR。

通过wait系统调用,父进程可以等待子进程的终止,并根据子进程的终止状态来进行相应的处理,例如回收子进程的资源、获取子进程的退出码等。这对于进程间的协作和资源管理非常有用。

上面是细致的讲解,接下来我来简化一下,讲解一下最重要的地方吧;

其实我们的wait这个接口就是用来阻塞住我们的父进程让父进程停在我们的操作系统的接口中,在接口中阻塞等待我们的子进程的终止,当我们的子进程终止时,我们进程退出阻塞状态继续执行下面的代码;

pid_t wait(int *status)我们可以给wait一个指针让wait修改此地址的数据,我们通过这个wait放入的数据获取我们的子进程的退出码,以此来得到相应的进程退出信息;如果我们不需要这个信息的话,我们可以把status写成NULL;

下面我们来看看wait阻塞等待的现象:

waitpid接口

这个接口可以获得的信息更多,功能更强大,我们可以更好的进行进程等待,相对的学习的东西也就更多;

下面是gpt给出的介绍:

waitpid系统接口的头文件是<sys/wait.h>。该头文件定义了与进程等待相关的函数和常量。

waitpid函数用于等待指定的子进程结束,并获取其退出状态。其原型如下:

pid_t waitpid(pid_t pid, int *status, int options);

参数说明:
- pid:要等待的子进程的进程ID。可以有以下取值:
  - <-1:等待任意一个进程组ID为pid绝对值的子进程。
  - -1:等待任意一个子进程。
  - 0:等待与调用进程属于同一进程组的任意子进程。
  - >0:等待指定进程ID为pid的子进程。
- status:指向一个整型变量的指针,用于存储子进程的退出状态。
- options:用于指定等待子进程的一些选项,可以是以下常量的按位或:
  - WNOHANG:如果pid指定的子进程还没有结束,则立即返回,不阻塞。
  - WUNTRACED:如果子进程处于暂停状态,则也立即返回。

返回值为子进程的进程ID,若有错误发生则返回-1。

waitpid函数是一种更为灵活的等待子进程结束的方式,相较于wait函数,它可以通过参数pid和options来指定更精确的等待条件。

pid_t waitpid(pid_t pid, int *status, int options);上面的文档已经将我们的这三个参数说的非常清楚了,我再结合我的理解来解释一下这三个参数,status参数和我们前面说到wait的status是一样的;而pid可以指定或者任意等待相应进程id为pid的进程;options默认为0代表着进行阻塞等待,和wiat类似的等待,当options为1时代表进行非阻塞等待,WNOHANG是一个宏定义它的值是1,所以一般用WNOHANG来代替我们的1来提高程序的可阅读性;

上面我们介绍完了wait和waitpid它们的基础用法和基本内容,我们接下来开始学习使用这两个接口:

看了上面的代码我们使用wait时用&status传输了地址,使得我们可以接收到返回的数据,返回的数据存放在了status里面,那我们wait这个接口向status中存放的到底是什么数据呢?

 也就是说我们的数据是按数位存放的,status并不是直接表示返回值它含有很多的信息;我们可以使用一系列的宏函数来获取这些信息;

WIFEXITED可以用来检查进程是否正常终止;

WIFSIGNALED可以用来检查进程是否是收到信号终止;

waitpid中的pid参数一般是用来指定等待某个进程的这里就不作演示了;

waitpid中的options参数当参数位0时类似于wait进行阻塞等待当参数为WNOHANG宏时(这个宏代表1)就是进行非阻塞等待下面我们来演示一下非阻塞等待:

codefile/test_waitpid.cpp · future/Linux - 码云 - 开源中国 (gitee.com)icon-default.png?t=N7T8https://gitee.com/little-lang/linux/blob/master/codefile/test_waitpid.cpp

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

typedef void(*function)();
std::vector<function> load_vector;

void fun1()
{
  printf("这是要做的事情1\n");
}

void fun2()
{
  printf("这是要做的事情2\n");
}

void fun3()
{
  printf("这是要做的事情3\n");
}

void load()//当要在非阻塞等待时执行任务只需要在这个函数中注册即可
{
    load_vector.push_back(fun1);
    load_vector.push_back(fun2);
    load_vector.push_back(fun3);
    sleep(2);
}

int main()
{
  pid_t ret=fork();
  if(ret==0)
  {
    //这是子进程
      printf("我是子进程我的进程id是: %d我正在运行\n",getpid());
      //休息5秒让父进程的现象显示出来
      sleep(5);
      //告诉用户子进程还在运行
      printf("我是子进程我仍然还在运行\n");
      exit(128);
  }
  else if(ret>0)
  {
    //这是父进程
    int status=0;
    while(waitpid(-1,&status,WNOHANG)==0)
    {
      load();//注册我们的行为
      for(int i=0;load_vector[i];i++)//执行表中的行为
      {
        load_vector[i]();
      }
    }
    printf("获得返回值: %d\n",WEXITSTATUS(status));
  }
  return 0;
}

上面是我进行阻塞等待的代码

下面是现象:

我们在使用非阻塞等待的时候,我们一般可以定义一个函数指针类型顺序表,在这个顺序表中插入我们的函数,这样我们就可以对我们的非阻塞等待时所需要做到动作进行统一管理,使得我们的代码更为清晰明了,更好管理;

进程替换

上面我们学习了怎么创建怎么等待进程,这些其实都是为了让我们的进程为我们做事,我们需要做的事情在子进程中实现,这样我们的父进程就可以去做其他的事情;举一个例子:在我们的游戏加载页面,我们看到的表面就是进度条在不断增加,甚至有的时候加载界面还会有小游戏给我们休闲让等待不那么枯燥,这个表面的工作其实是我们父进程在做,真正加载我们的游戏,这个事情是我们的子进程在做;而我们子进程要做到事情是需要我们自己编辑的,但是如果已经有现成的代码写好了一个子进程,我们怎么去使用它呢?这个时候我们就可以使用进程替换了;

进程替换有六种函数都是exec开头的:

进程替换函数

上面就是我们的6个替换函数,这些函数怎么使用呢?

execv和execl

我们先看execl和execv这两个函数其实意义相似只是使用不同,l函数是我们参数以列表的形式传递,l函数的第一个参数是path这个参数是用来传递我们需要替换进程的路径和文件名的 ,l后面的参数则是按照列表的顺序将我们的需要替换的进程的命令行参数一个一个的填入我们的表中例如:

当我们想替换的进程是ls -l -a这个进程的时候(命令也是其实也是一个程序)我们可以使用

execl( "/usr/bin/ls","ls","-l","-a",NULL);注意最后一个参数要为NULL

而我们的v函数则传递的是一个数组,我们直接将命令放入我们的数组中传递;

execvp与execlp

接下来我们来看execlp和execvp这两个函数,它们两个函数再上面的基础上加上了环境变量,这样我们再对第一个参数输入时,如果环境变量path中有我们的要替换的子进程的路径的话,我们的第一个参数就可以省略路径只需要写文件名即可如:

execlp("ls","ls","-l","-a",NULL);和前面不同的是不需要再带路径了

execvp也一样这里就不重复写了;

 execvpe和execle

这两个函数,它们具备前面所有函数的功能,除此之外这两个函数的最后一个参数可以传递我们的参数列表用这个参数列表来覆盖我们父进程传递的参数列表;

下面是我们的使用方法和我们的产生的现象:

有了这些函数之后我们就可以实现对进程的替换,当我们需要别人写好的程序的时候,我们只需要用进程替换函数就可以获得别人程序的运行成果(不同语言写的程序都可以)减轻了程序员的工作负担;

模拟实现shell

我们的shell这个程序其实就是一个这样的程序,我们的shell用来显示我们的运行成果,而我们的指令其实是一个个的程序,这些程序被shell的子进程所替换;然后输出再shell上;我们接下来写一个shell程序的模拟实现;

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

#define SIZE_COMMAND 64
#define SIZE_COMLINE 1024

char* command[SIZE_COMMAND];
char com_line[SIZE_COMLINE];
char g_myenv[100];

//内置命令
bool inside_command()
{
    if(strcmp(command[0],"cd")==0)
    {
        if(command[1] != NULL) chdir(command[1]);
        return true;
    }

    if(strcmp(command[0],"export")==0)
    {
        strcpy(g_myenv,command[1]);
        //printf("this is test:%s\n",g_myenv);
        //printf("this is test:%s\n",command[1]);
        if(putenv(g_myenv)==0)
          printf("%s: export success\n",g_myenv);
        //if(putenv(command[1])==0)
        //printf("%s: export success\n",command[1]);
        return true;
    }

    return false;
}


//获得命令
bool get_command()
{
      printf("[this is myshell]$ ");      
      //把储存我们的命令字符串的字符初始化为全0      
      memset(com_line,0,SIZE_COMLINE);      
      if(NULL==fgets(com_line,sizeof(com_line),stdin))
      {
        printf("get_command fail\n");
        exit(0);
      }
      //处理特殊情况因为我们的回车\n也被fgets读入了      
      com_line[strlen(com_line)-1]='\0';      
      //开始分割字符串到数组command中      
      command[0]=strtok(com_line," ");      
      int index=0;
      //增加配色方案
      if(strcmp(command[0],"ls")==0)
      {
        command[++index]=(char*)"--color=auto";
      }
      //继续分割字符串
      while(command[index]!=NULL)      
      {      
          command[++index]=strtok(NULL," ");
      }    
      //测试一一下是否分隔成功    
      //int i=0;    
      //while(command[i]!=NULL)    
      //{    
      //    printf("%s\n",command[i++]);    
      //}
      
      //如果执行了内置命令就会返回false
      if(inside_command())
        return false;
      //正常要子进程执行命令则返回true
      return true;
}

int main()
{
    extern char **environ;
    while(1)
    {
        if(get_command()==false)
          continue;
        pid_t pid=fork();
        if(pid==0)
        {
          //子进程执行
          //execvp(command[0],command;//方法1
          execvpe(command[0],command,environ);
        }
        else if(pid<0)
        {
          printf("命令执行失败\n");
        }
        wait(NULL);
    }
    return 0;
}

codefile/my_shell.c · future/Linux - 码云 - 开源中国 (gitee.com)icon-default.png?t=N7T8https://gitee.com/little-lang/linux/blob/master/codefile/my_shell.c要是上面代码观感不佳可以点击卡片观看;

以上就是进程控制的内容;

2023.12.18;

  • 26
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值