【Linux篇】第九篇——Linux下的进程控制(二)

简介: 【Linux篇】第九篇——Linux下的进程控制

进程程序替换


fork创建子进程后一般会有两种行为:

  • 想让子进程执行父进程的一部分代码(可以理解为子承父业)
  • 想让子进程执行和父进程完全不同的代码,也就是程序替换(可以理解为儿子创业)

原理


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

问题:

1.程序替换的本质是什么?

把磁盘中的程序的代码和数据用加载器加载进特定的进程的上下文,底层用到了exec系列的程序替换了函数。

2.程序替换后,有没有新进程被创建?

没有。因为进程替换前后,没有创建新的PCB,虚拟内存和页表等数据结构,也就是进程的这些数据结构没有发生变化,进程替换只是对物理内存中的数据和代码进行了修改,前后进程的ID没有发生改变,所以程序替换不创建新进程.

3. 子进程发生程序替换后,代码和数据都发生写时拷贝嘛?

由于进程替换会把新程序的代码和数据加载到特定的进程,为了让父子进程之间具有独立性,修改的代码和数据都要发生写时拷贝,这样才不会影响父进程的数据和代码。

替换函数


其中有六种以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 execve(const char *path, char *const argv[], char *const envp[]);

函数返回值:只要exec*返回,就一定调用失败了,调用成功不需要有返回值检测。

函数参数:

  • path:用来替换的程序所在的路径
  • file:程序名
  • arg,...:列表的形式传参
  • arg[]:数组的形式传参
  • envp[]:自己维护的环境变量

函数名解释:

  • l(list):表示参数采用列表
  • v(vector):参数用数组
  • p(path):有p自动搜索环境变量PATH
  • e(env):表示自己维护环境变量
void myfun(char *arg1,char *arg2,char *arg3);//列表式传参
myfun(a1,a2,a3);//list
void myfun(char *arg[]);//非列表式的传参
char *arg[] = {a1,a2,a3};
myfun(arg);

参数命名中有I的需要一个一个进行传参,有v的需要将参数放入数组,通过数组传参,有p的第一个参数是file,而不带p的第一个参数是path,有p自动去环境变量PATH中搜索;传参时可以直接传想要使用的命令,不需要传路径,会自动搜索,只需要告诉执行的命令是谁

函数的使用方法

函数名 参数格式 是否带路径 是否使用当前环境变量
execl 列表
execlp 列表
execle 列表 否,自己组装环境变量
execv 数组
execvp 数组
execve 数组 否,自己组装环境变量

execl

第一个参数是你要执行哪个程序(需要带路径),因为执行程序需要知道你在哪,你是谁,第二个是要执行的程序名,命令行怎么执行,传入什么选项,你就可以在这里直接按照顺序填写参数,命令行上怎么写,这里就怎么写,这种传参方式叫做list方式,最后必须以NULL结尾,告知execl传参结束。  

#include<stdio.h>
#include<unistd.h>
int main()
{
  execl("/usr/bin/ps","ps","-e","-l","-f",NULL);
  return 0;
}

运行结果

image.png

第一个参数代表你要执行谁,第二个参数是你在命令行怎么调用执行,在后面的参数中你就怎么传递。

再看一组程序

#include<stdio.h>
#include<unistd.h>
int main()
{
    printf("begin............................\n");
    execl("/usr/bin/ls","ls","-a","-l","-i",NULL);
    printf("hello world\n");
    printf("hello world\n");
    printf("hello world\n");
    printf("hello world\n");
    return 0;
}

运行结果

image.png

./test是自己写的可执行程序,./test变成了进程,代码执行到execl,进行程序替换,用ls进程的代码和数据替换test进程的代码和数据,执行ls进程。

注意:这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回。如果调用出错则返回-1,所以exec函数只有出错的返回值而没有成功的返回值,所以exec系列函数是没有返回值的,如果返回了,或者执行了后续的代码,一定是程序替换错了。

举个例子,加深理解

#include<stdio.h>
#include<unistd.h>
int main()
{
 printf("begin...........\n");
 execl("/usr/bi/ps","ls","-a","-l","-i",NULL);
 printf("hello world!\n");
 printf("hello world!\n);
 printf("hello world!\n");
 printf("hello world!\n");
 return 0;
}

运行结果

image.png

可以让子进程去干程序替换这件事情:

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
int main()
{
    //execl("/usr/bin/ls", "ls",_"-a","-l","-i",NULL);
  //execl("/usr/bin/top" , "top" ,NULL);
    pid_t id = fork();
    if(id<0)
    {
        perror("fork");
        return 1;
    }
    else if(id == 0)
    {
        //child
        printf("i am a child , pid:%d, ppid: %d\n",getpid(),getppid());
        execlp("ls","ls","-al",NULL);
        printf("hello world!\n);
        exit(1);
    }
    //father
    int status = 0;
    pid_t ret = waitpid(id,&status,0);
    if(ret > 0)
    {
        printf("wait success!\n");
      //  printf("exit code: %d,exit status: %d\n",(status>>8)&0xFF,status & 0x7F);
    }
    else{
        printf("wait failed!\n");
    }
    return 0;    
}

这里子进程进行了程序替换,退出的进程其实就是ls程序

image.png

exec系列函数的理解

软件被加载进内存,需要加载器,一个软件加载到内存就成了进程,首先软件先运行起来变成进程,然后进程调用exec系列函数,就可以完成加载到内存的过程,exec可以理解成一种特殊的加载器.

execv

传参以数组进行传参

#include<stdio.h>
#include<unistd.h>
int main()
{
    printf("begin............................\n");
    char *arg[] = {"ls","-a","-l","-i",NULL};
    execv("/usr/bin/ls",arg);
    printf("you should running here\n");
    return 0;
}

image.png

execvp

带v以及带p,参数用数组传,带p说明第一个参数不需要传路径,它会自动的去环境变量PATH里面去找可执行程序,所以传命令名字就行。

#include<stdio.h>
#include<unistd.h>
int main()
{
    printf("begin............................\n");
    char *arg[] = {"ls","-a","-l","-i",NULL};
    execvp("ls",arg);
    printf("you should running here\n");
    return 0;
}

execlp

带l以及带p,参数用列表形式传,带p说明第一个参数不需要传路径,传命令名字就行。

#include<stdio.h>
#include<unistd.h>
int main()
{
    printf("begin............................\n");
    execlp("ls","ls","-a","-l","-i",NULL);
    printf("you should running here\n");
    return 0;
}

elecle

带l和带e的,带e表示自己维护环境变量,传入默认的或者自定义的环境变量给目标可执行程序。

在说明elecle函数之前,我门先想一个问题:

exec系列函数能调用系统程序,那么他能调用自己的程序嘛?答案是可以的。

#include<stdio.h>
int main()
{
    int i = 0;
    int sum = 0;
    for(;i<=100;i++)
    {
        sum+=i;
    }
    printf("result[1~100] sum is:%d\n",sum);
    return 0;
}

Makefile的编写:

makefile默认只生成一个可执行程序,默认是自顶向下扫描makefile文件遇到的第一个目标

输入make bin命令 ,默认就生成bin

.PHONY:all
all:mytest mycmd
mytest:Test.c
    gcc -o $@ $^  
mycmd:mycmd.c
    gcc -o $@ $^
.PHONY:clean
clean:
    rm mytest mycmd

我们在Test.c中使用程序替换execl函数去执行我们写的程序:

#include<stdio.h>
#include<unistd.h>
int main()
{
    printf("begin............................\n");
    execl("./mycmd","./mycmd",NULL);
    return 0;
}

image.png

可以看到我们成功的通过exec系列函数调用自己写的程序:

image.png

上面说的都是为了说明execle系统调用所做的铺垫,下面我们再来看execle:

我们在mycmd.c中获取一个环境变量myenv,而这个程序它本身是没有myenv这个环境变量的,所以我们就可以通过execle函数给我们写的程序将环境变量传过去:

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
int main()
{
    char *env[] = {"myenv = you_can_see_me!",NULL};//自定义环境变量
    printf("begin............................\n");
    execle("./mycmd","./mycmd",NULL,env);//调用该函数并将自定义的环境变量数据传给目标程序
    return 0;
}
#include<stdio.h>
#include<stdlib.h>
int main()
{
    int i = 0;
    int sum = 0;
    for(;i<=100;i++)
    {
        sum+=i;
    }
    printf("result[1~100] sum is:%d\n",sum);
    printf("myenv: %s\n",getenv("myenv"));
    return 0;
}

image.png

我们运行mytest,发现完成的程序替换,而且将环境变量也传过去了。

man查看exec系列函数:

image.png

我们发现execve是和上面的函数分开的,本质上是因为,execve是最底层的系统调用,其他都是去调用它去完成的:

image.png

实际中,我们可以fork出子进程,让子进程去进行程序替换,替父进程完成事情:

#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
int main()
{
    pid_t id = fork();
    if(id<0)
    {
        perror("fork error\n");
        return 1;
    }
    if(id == 0)
    {
        //child
        execl("usr/bin/ls","ls","-a","-l","-i",NULL);
        //如果返回则替换失败
        exit(-1);
    }
    pid_t ret = waitpid(id,NULL,0);
    if(ret > 0)
    {
        printf("wait success,cmd exit\n");
    }
    return 0;
}

image.png

父进程正常执行自己要干的事情,因为替换的是子进程,进程是有独立性的,所以,父进程是不受影响的!

简易shell的实现


要写一个shell,需要循环以下过程:

1.获取命令行

2.解析命令行

3.建立一个子进程(fork)

4.替换子进程(execvp)

5.父进程等待子进程退出(waitpid)

我们发现shell运行原理就是用户执行命令,shell解释器创建子进程去执行命令,子进程将执行结构告诉shell,最后再反馈给用户,其实就是给上面的程序套上一层循环去创建子进程去执行命令:

#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
int main()
{
    while(1)
    {
        pid_t id = fork();
        if(id<0)
        {
            perror("fork error\n");
            return 1;
        }
        if(id == 0)
        {
            //child
            execl("usr/bin/ls","ls","-a","-l","-i",NULL);
            //如果返回则替换失败
            exit(-1);
        }
        pid_t ret = waitpid(id,NULL,0);
        if(ret > 0)
        {
            printf("wait success,cmd exit\n");
        }
    }
    return 0;
}

下面我们来实现我们的myshell.c,首先我们登录主机后,会打印命令提示符(用户名@主机名 当前目录)提示符,这里我们为了简单,就直接打印一个主机名:

const char* cmd_line = "[temp@VM-0-3-centos myshell]#";

然后我们需要做的就是数据读取,C语言有一个fgets函数,我们可以这样读取数据:

image.png

fgets(cmd,SIZE,stdin);

cmd是保存输入命令的一个数组,大小自己决定即可,size是读取的字符个数,stream是从哪里读,这里需要注意的是,我们读取结束后最后一个字符是\n,所以需要将它置为\0

cmd[strlen(cmd)-1] = '\0';//标准输入会输入\n,将\n改为\0

接下来就要进行字符串数据分析,怎么分析呢?我们首先要把输入的字符串以空格为标志进行分割,然后放进一个字符指针数组,不了解strtok函数的可以去了解一下:

//字符串(命令行数据分析)
char* args[NUM];
args[0] = strtok(cmd," ");//字符串分割
int i = 1;
do{
     rgs[i] = strtok(NULL," ");
     if(args[i] == NULL)
     {
           break;
     }
     ++i;
}while(1);

然后就是创建子进程进行执行命令,子进程通过调用程序替换函数去执行命令,那么我们想一下我们用哪个函数呢?我们的命令是用数组存起来的,所以需要带v,那么就用execvp,并且不用传路径,传命令名就好,会自动去环境变量PATH里找:

pid_t id = fork();
if(id < 0)
{
     perror("fork error!\n");
     continue;
}
 //4.执行非内置命令
if(id == 0)
{
     //child
     execvp(args[0],args);
     exit(1);//替换失败了就直接退出
}
int status = 0;
pid_t ret = waitpid(id,&status,0);
if(ret>0)
{
    printf("status code :%d\n",(status>>8)&0xff);
}

代码实现如下:

#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<stdlib.h>
#define SIZE 256
#define NUM 16
int main()
{
    char cmd[SIZE];//保存命令
    const char* cmd_line = "[temp@VM-0-3-centos ~]#";
    while(1)
    {
        cmd[0] = 0;//清空数据
        //memset(cmd,'\0',sizeof(cmd));
        printf("%s",cmd_line);
        //数据读取
        fgets(cmd,SIZE,stdin);
        //printf("%s",cmd);
        cmd[strlen(cmd)-1] = '\0';//标准输入会输入\n,将\n改为\0
        //字符串(命令行数据分析)
        char* args[NUM];
        args[0] = strtok(cmd," ");//字符串分割
        int i = 1;
        do{
            args[i] = strtok(NULL," ");
            if(args[i] == NULL)
            {
                break;
            }
            ++i;
        }while(1);
    //shell内的函数调用,内置命令
        pid_t id = fork();
        if(id < 0)
        {
            perror("fork error!\n");
            continue;
        }
        //4.执行非内置命令
        if(id == 0)
        {
            //child
            execvp(args[0],args);
            exit(1);//替换失败了就直接退出
        }
        int status = 0;
        pid_t ret = waitpid(id,&status,0);
        if(ret>0)
        {
            printf("status code :%d\n",(status>>8)&0xff);
        }
    }
    return 0;
}

上面的代码只是支持非内置命令,内置命令不可以,比如cd:

我们期望改的是父进程shell的当前路径,这里则是修改的是子进程的当前路径,子进程干完事就退出了,所以不能创建子进程执行cd,也不能让父进程通过程序替换去执行cd,因为执行了父进程会影响,所以需要系统接口来完成命令的执行。

如果想要支持cd命令,就需要在创建子进程前判断命令:

if(strcmp( args[0],"cd" ) == 0 && chdir(args[1])== 0)//chdir修改当前路径
{
    continue;
}

执行结果

image.png

最终代码

#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<stdlib.h>
#define SIZE 256
#define NUM 16
int main()
{
    char cmd[SIZE];//保存命令
    const char* cmd_line = "[temp@VM-0-3-centos ~]#";
    while(1)
    {
        cmd[0] = 0;//清空数据
        //memset(cmd,'\0',sizeof(cmd));
        printf("%s",cmd_line);
        //数据读取
        fgets(cmd,SIZE,stdin);
        //printf("%s",cmd);
        cmd[strlen(cmd)-1] = '\0';//标准输入会输入\n,将\n改为\0
        //字符串(命令行数据分析)
        char* args[NUM];
        args[0] = strtok(cmd," ");//字符串分割
        int i = 1;
        do{
            args[i] = strtok(NULL," ");
            if(args[i] == NULL)
            {
                break;
            }
            ++i;
        }while(1);
        //3.判断命令
        if(strcmp( args[0],"cd" ) == 0 && chdir(args[1])== 0)//chdir修改当前路径
        {
            continue;
        }
    //shell内的函数调用,内置命令
        pid_t id = fork();
        if(id < 0)
        {
            perror("fork error!\n");
            continue;
        }
        //4.执行非内置命令
        if(id == 0)
        {
            //child
            execvp(args[0],args);
            exit(1);//替换失败了就直接退出
        }
        int status = 0;
        pid_t ret = waitpid(id,&status,0);
        if(ret>0)
        {
            printf("status code :%d\n",(status>>8)&0xff);
        }
    }
    return 0;
}


相关文章
|
3天前
|
NoSQL Linux 程序员
【linux进程信号(一)】信号的概念以及产生信号的方式
【linux进程信号(一)】信号的概念以及产生信号的方式
|
3天前
|
Linux
【linux进程间通信(一)】匿名管道和命名管道
【linux进程间通信(一)】匿名管道和命名管道
|
3天前
|
Java Shell Linux
【linux进程控制(三)】进程程序替换--如何自己实现一个bash解释器?
【linux进程控制(三)】进程程序替换--如何自己实现一个bash解释器?
|
3天前
|
算法 Linux Shell
【linux进程(二)】如何创建子进程?--fork函数深度剖析
【linux进程(二)】如何创建子进程?--fork函数深度剖析
|
3天前
|
存储 Linux Shell
【linux进程(一)】深入理解进程概念--什么是进程?PCB的底层是什么?
【linux进程(一)】深入理解进程概念--什么是进程?PCB的底层是什么?
|
4天前
|
消息中间件 Unix Linux
Linux的学习之路:17、进程间通信(1)
Linux的学习之路:17、进程间通信(1)
20 1
|
4天前
|
存储 安全 Linux
Linux的学习之路:9、冯诺依曼与进程(1)
Linux的学习之路:9、冯诺依曼与进程(1)
18 0
|
9天前
|
算法 Linux 调度
深入理解Linux内核的进程调度机制
【4月更文挑战第17天】在多任务操作系统中,进程调度是核心功能之一,它决定了处理机资源的分配。本文旨在剖析Linux操作系统内核的进程调度机制,详细讨论其调度策略、调度算法及实现原理,并探讨了其对系统性能的影响。通过分析CFS(完全公平调度器)和实时调度策略,揭示了Linux如何在保证响应速度与公平性之间取得平衡。文章还将评估最新的调度技术趋势,如容器化和云计算环境下的调度优化。
|
11天前
|
监控 Linux
linux监控指定进程
请注意,以上步骤提供了一种基本的方式来监控指定进程。根据你的需求,你可以选择使用不同的工具和参数来获取更详细的进程信息。
14 0
|
12天前
|
消息中间件 监控 Linux
Linux进程和计划任务管理
通过这些命令和工具,你可以有效地管理Linux系统中的进程和计划任务,监控系统的运行状态并保持系统的稳定和可靠性。 买CN2云服务器,免备案服务器,高防服务器,就选蓝易云。百度搜索:蓝易云
103 2