Linux学习记录——십오 进程控制(2)


1、程序替换

在之前的父子进程中,创建子进程之前,父进程就有了一些代码,fork创建子进程后,子进程就运行父进程的一部分代码。进程替换则是为了让子进程运行不属于父进程的代码。

1、了解替换

程序替换的命令是exec。

在这里插入图片描述

前两个函数参数里有三个点…,三个点意味着可变参数列表。

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

int main()
{   
    printf("begin.....\n");
    printf("begin.....\n");
    printf("begin.....\n");
    printf("进程开始,PID是: %d\n", getpid());
    execl("/bin/ls", "ls", "-a", "-l", NULL);
    printf("end.....\n");
    printf("end.....\n");
    printf("end.....\n");
    return 0;
}

结果如图

在这里插入图片描述

在计算机内部,指令是在磁盘里的,程序运行后,它的pcb就在内存中。当遇到execl后,会把磁盘中的可执行程序替换到当前进程的数据。实际打印出来时,后面的end没有出现,这时候已经替换过了,也就没有打印end的语句了。

2、基本原理

每个进程都有自己的虚拟内存,页表,和映射好的物理内存。进行程序替换时,会把原来在物理内存中映射过去的空间的代码和数据替换为磁盘中可执行程序的代码和数据,这就是程序替换。

那么程序替换有没有创建新的进程呢?它没有创建进程。它只是把一个加载好的新的程序给替换了过来,内存中原有进程的pid并没有变。

对于磁盘中的程序来说,这个程序是直接被加载进了内存中,程序就是通过程序替换加载进内存的。exec等函数可以看做加载器。

操作系统内部会先把数据结构都创建出来,然后再去找程序,这也就是加载。

3、多进程

刚才的程序看不到打印end是因为新的代码和数据替换过去了,老代码已经没有人去执行了,所以就没有执行。所以程序替换只能全局替换,不能局部替换。

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>  
                                                                                                                                                                                                        
int main()
{   
    pid_t id = fork();
    if(id == 0)
    {
        //child
        printf("我是子进程: %d\n", getpid());
        execl("/bin/ls", "ls", "-a", "-l", NULL);
    }
    sleep(5);
    //father
    printf("我是父进程: %d\n", getpid());
    waitpid(id, NULL, 0);
    return 0;
}

在这里插入图片描述

像上面这样,可以发现,程序替换只能影响当前进程。因为当进行程序替换时,子进程会改变物理内存的数据,系统就会写时拷贝,给子进程另开一个空间,所以它们互不干扰。这样的替换让子进程运行了新代码,所以物理内存的代码区可以发生进程替换。

如果程序替换失败,那么就继续执行老代码,并且会返回-1。

int n = execl("/bin/lssssssss", "lssssssss", "-a", "-l", NULL);
printf("Fail : %d\n", n); 
exit(0);

这样写确实会看到-1,不过没有意义,因为失败就会往下进行,成功就不继续了,所以不需要看返回值。

也写一下父进程获取退出码的代码

    //father
    int status = 0;
    printf("我是父进程: %d\n", getpid());
    waitpid(id, &status, 0);
    printf("child exit code: %d\n", WEXITSTATUS(status));

这样子进程出了什么错误,父进程就会展现出对应的退出码。

4、程序替换接口

execl函数

int execl(const char* path,const char* arg,…);

path是路径,arg就是命令执行的方式。使用的时候把命令行参数一个个传过来,最后以NULL结尾。

execv函数

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

argv意味要传数组。

char* const myargv[] = {"ls", "-a", "-l", "-n", NULL};
execv("/bin/ls", myargv);

execlp函数

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

execl后面加上p,第一个参数也改为file,这个函数的特点,当我们执行指定程序的时候,只需要传指定程序名即可,系统会在环境变量PATH中查找。所以第一个是ls,不需要传路径。

execlp("ls", "ls", "-a", "-l", "-n", NULL);  

execvp函数

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

综合上面四个可以看出规律,带p说明函数会去环境变量里找路径,我们只需要传指令名,带v则说明要传数组。

execle函数

int execle(const char* path, const char* argv,…,char* const envp[])

除了像上面一样调用系统命令,还可以调用自己写的程序。我们自己定好路径,execl(“path”, “程序名”, NULL)就可以调用另一个程序。

envp代表环境变量,可以传自定义的环境变量。

我们在当前目录声明一个新目录,然后在新目录里定义一个文件otherproc.cc,用原先目录中的某个文件来调用它。

otherproc里,访问一个自定义的环境变量,没有就设为NULL。

#include <iostream>
#include <unistd.h>
using namespace std;

int main()
{
    for(int i = 0; i < 5; ++i)
    {
        cout << "我是另一个程序,我的pid是: " << getpid() << endl;
        cout << (getenv("MYENV") == NULL ? "NULL" : getenv("MYENV")) << endl;
        sleep(1);
    }
    return 0;
}

如果单独调用这个程序,那就是打印NULL。现在在看调用这个程序的文件myproc.c

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

int main()
{
    pid_t id = fork();
    if(id == 0)
    {
        //child
        printf("我是子进程: %d\n", getpid());
        char* const myenv[] = {"MYENV=YouCanSeeMe", NULL};//这是要传的自定义环境变量,只写一个就得以NULL结尾
        execle("./di/otherproc", "otherproc", NULL, myenv);//otherproc也就是otherproc.cc生成好的可执行文件,di是当前目录下的一个目录
        exit(1);
    }
    sleep(1);
    //father
    int status = 0;
    printf("我是父进程: %d\n", getpid());
    waitpid(id, &status, 0);
    printf("child exit code: %d\n", WEXITSTATUS(status));
    return 0;
}

这时候运行这个myproc就会打印出MYENV了。那么这个自定义环境变量会不会影响原有的?在other里再写上一句

cout << "PATH: " << (getenv("PATH") == NULL ? "NULL" : getenv("PATH")) << endl;

如果只运行other,那么MYENV打印NULL,PATH打印本来有的,而如果调起myproc来运行,那么PATH就变为NULL,而MYENV就出现了我们自己写的。也就是说会把原有的给清空,写上自定义的。

如果不想传自定义,想把父进程本来有的环境变量传给子进程,那么得在fork之前写上extern char** environ,然后execle的第四个参数传environ就可以了,那么调起myproc就会打印PATH,而MYENV是NULL。

如果现在自定义和原有的都想打印,那就用putenv,在环境变量中新增一个。那么这样写

int main()
{
    extern char** environ;
    pid_t id = fork();
    if(id == 0)
    {
        //child
        printf("我是子进程: %d\n", getpid());
        //char* const myenv[] = {"MYENV=YouCanSeeMe", NULL};//这是要传的自定义环境变量,只写一个就得以NULL结尾
        putenv("MYENV=YouCanSeeMe");
        execle("./di/otherproc", "otherproc", NULL, environ);
        exit(1);
    }
    sleep(1);
    //father
    int status = 0;
    printf("我是父进程: %d\n", getpid());
    waitpid(id, &status, 0);
    printf("child exit code: %d\n", WEXITSTATUS(status));
    return 0;
}

那么自定义环境变量就被添加到了当前进程environ指向的环境变量列表中,那么就可以全部都打印了。

环境变量具有全局属性,可以被子进程继承下去,就是通过execle接口来传下去的。即使在命令行中export一个自定义环境变量,也可以让子进程们得到这个环境变量。

实际上是操作系统调起了程序中的main函数,传给了环境变量。所以我们即使用命令行参数来添加环境变量,也能让程序打印出对应的环境变量。

execvpe函数

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

execve函数

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

有时候少传了参数也能运行通过。

总结

总共7个函数,只有最后一个execve是系统调用的函数,其他6个都是对它的封装。也就是无论调哪一个,内部都是调用了execve这个函数。

2、编写极简的shell(bash)

要先打印出左边的用户,然后后面可以输入命令。

myshell:myshell.c
        gcc -o $@ $^ -std=c99
.PHONY:clean
clean:
        rm -f myshell
#include <stdio.h>
#include <string.h>
#include <assert.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

#define MAX 1024

int main()
{
    char commandstr[MAX] = {0};
    while(1)
    {
        printf("[zyd@hjdkhs2houqwie iiruoeg]# ");
        fflush(stdout);
        char* s = fgets(commandstr, sizeof(commandstr), stdin);
        assert(s);
        (void)s;//保证在release方式发布的时候,因为去掉了assert,所以会出现因s没有被带来的编译警告,这里什么都
不做仅充当一次使用
        commandstr[strlen(commandstr) - 1] = '\0';
        
        pid_t id = fork();
        assert(id >= 0);
        (void)id;
        if(id == 0)
        {
            //child
        }
        int status = 0;
        waitpid(id, &status, 0);
    }
    return 0;
}

fgets可以从特定的标准输入中获取对应的命令行输入,获取之后放到缓冲区里。

在这里插入图片描述

添加进父子进程后,我们继续深入。面对输入进来的指令,我们要做字符串切割,把"ls -a -l" 换成 “ls” “-a” “-l”。

现在整体的代码如下:

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

#define MAX 1024
#define ARGC 64
#define SEP " "

int split(char* commandstr, char* argv[])
{
    assert(commandstr);
    assert(argv);
    argv[0] = strtok(commandstr, SEP);
    if (argv[0] == NULL) return -1;
    int i = 1;
    while(argv[i++] = strtok(NULL, " "))
    {
        ;
    }
    return 0;
}

void debugPrint(char* argv[])
{
    for(int i = 0; argv[i]; i++)
    {
        printf("%d: %s\n", i, argv[i]);
    }
}

int main()
{
    char commandstr[MAX] = {0};
    char* argv[ARGC] = {NULL};
    while(1)
    {
        printf("[zyd@hjdkhs2houqwie iiruoeg]# ");
        fflush(stdout);
        char* s = fgets(commandstr, sizeof(commandstr), stdin);
        assert(s);
        (void)s;//保证在release方式发布的时候,因为去掉了assert,所以会出现因s没有被使用带来的编译警告,这里什么都不做仅充当一次使用
        commandstr[strlen(commandstr) - 1] = '\0';

        int n = split(commandstr, argv);
        if(n != 0) continue;
        //debugPrint(argv);
        pid_t id = fork();
        assert(id >= 0);
        (void)id;
        if(id == 0)
        {
            //child
            execvp(argv[0], argv);
            exit(0);
        }
        int status = 0;
        waitpid(id, &status, 0);
    }
}

对输入的内容进行分割,然后从环境变量中找到对应的命令。子进程做这些,父进程等待就行。

在这里插入图片描述

1、配色方案

        int n = split(commandstr, argv);
        if(n != 0) continue;
        //debugPrint(argv);
        if(strcmp(argv[0], "ls") == 0)//找到argv的NULL,把它往后挪一位,然后之前的位置加上配色方案
        {
            int pos = 0;
            while(argv[pos]) pos++;
            argv[pos++] = (char*)"--color=auto";
            argv[pos] = NULL;
        }
        pid_t id = fork();
        assert(id >= 0);
        (void)id;

2、pwd

调用程序后,pwd可以查看位置,但是用cd更换位置后,pwd显示的位置没有更新。这是因为虽然cd命令也可以正常分割,但是cd是在子进程里运行的,而父进程并没有相应地改变位置。所以cd要让bash自己去执行,这样的命令也就是内建/置命令。

在这里插入图片描述

我们用chdir命令去改变工作路径。

        if(strcmp(argv[0], "cd") == 0)
        {
            if(argv[1] != NULL) chdir(argv[1]);
            continue;//会直接回到上面,继续循环输入命令 
        }
        if(strcmp(argv[0], "ls") == 0)//找到argv的NULL,把它往后挪一位,然后之前的位置加上配色方案
        {
            int pos = 0;
            while(argv[pos]) pos++;
            argv[pos++] = (char*)"--color=auto";
            argv[pos] = NULL;
        }

3、export和env

环境变量是给bash设置的,也就是当前的父进程,export也是一个内建函数。

else if(strcmp(argv[0], "export") == 0)
{
    if(argv[1] != NULL) putenv(argv[1]);
    continue;
}

这样还不行,因为env命令会交给父进程执行。在进入整体的while循环前,先
extern char** environ,然后用execvpe函数来交给bash。

if(id == 0)
{
    execvpe(argv[0], argv, environ);
    exit(0);
}

但是还不是正确的。因为现在输入的指令都是给到argv分割的,argv又是读取commandstr,随着一次次输入指令,数组里面的内容一直在变,会覆盖掉之前输入的环境变量,所以需要用户自己维护加入的环境变量。下面的子进程那里就还是写成

execvp(argv[0], argv);

在这里插入图片描述

        else if(strcmp(argv[0], "export") == 0)
        {
            if(argv[1] != NULL)
            {
                strcpy(myenv[env_index], argv[1]);
                putenv(myenv[env_index++]);
            }
            continue;
        }

虽然这样可以正常进行,但还是有点问题。当我们查看环境变量的时候,我们想查看的是bash的环境变量,也就是父进程,但现在的程序查的还是子进程的。

void showEnv()
{
    extern char** environ;
    for(int i = 0; environ[i]; ++i)
    {
        printf("%d:%s\n", i, environ[i]);
    }
}
        else if(strcmp(argv[0], "env") == 0)
        {
            showEnv();
            continue;
        }

这样打印出来就很正常了。

在这里插入图片描述

基本上环境变量相关的命令都是内建命令。

4、echo

在进入while循环前就声明一个int类型变量last_exit = 0。

        else if(strcmp(argv[0], "echo") == 0)
        {
            const char* target_env = NULL;
            if(argv[1][0] == '$')
            {
                if(argv[1][1] == '?')
                {
                    printf("%d\n", last_exit);
                    continue;
                }
                else target_env = getenv(argv[1] + 1);
                if(target_env != NULL) printf("%s=%s\n", argv[1] + 1, target_env);
            }
            continue;
        }
       if(id == 0)
        {
            //child
            execvp(argv[0], argv);
            exit(0);
        }
        int status = 0;
        pid_t ret = waitpid(id, &status, 0);
        if(ret > 0)
        {
            last_exit = WEXITSTATUS(status);
        }

在这里插入图片描述

至此myshell就写完了,基本上常用的操作都能用。

5、bash整体代码

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

#define MAX 1024
#define ARGC 64
#define SEP " "

int split(char* commandstr, char* argv[])
{
    assert(commandstr);
    assert(argv);
    argv[0] = strtok(commandstr, SEP);
    if (argv[0] == NULL) return -1;
    int i = 1;
    while(argv[i++] = strtok(NULL, " "))
    {
        ;
    }
    return 0;
}

void debugPrint(char* argv[])
{
    for(int i = 0; argv[i]; i++)
    {
        printf("%d: %s\n", i, argv[i]);
    }
}

void showEnv()
{
    extern char** environ;
    for(int i = 0; environ[i]; ++i)
    {
        printf("%d:%s\n", i, environ[i]);
    }
}

int main()
{
    char commandstr[MAX] = {0};
    char* argv[ARGC] = {NULL};
    char myenv[32][256];
    int env_index = 0;
    int last_exit = 0;
    while(1)
    {
        printf("[zyd@hjdkhs2houqwie iiruoeg]# ");
        fflush(stdout);
        char* s = fgets(commandstr, sizeof(commandstr), stdin);
        assert(s);
        (void)s;//保证在release方式发布的时候,因为去掉了assert,所以会出现因s没有被带来的编译警告,这里什么都不做仅充当一次使用
        commandstr[strlen(commandstr) - 1] = '\0';

        int n = split(commandstr, argv);
        if(n != 0) continue;
        //debugPrint(argv);
        if(strcmp(argv[0], "cd") == 0)
        {
            if(argv[1] != NULL) chdir(argv[1]);
            continue;//会直接回到上面,继续循环输入命令 
        }
        else if(strcmp(argv[0], "export") == 0)
        {
            if(argv[1] != NULL)
            {
                strcpy(myenv[env_index], argv[1]);
                putenv(myenv[env_index++]);
            }
            continue;
        }
        else if(strcmp(argv[0], "env") == 0)
        {
            showEnv();
            continue;
        }
        else if(strcmp(argv[0], "echo") == 0)
        {
            const char* target_env = NULL;
            if(argv[1][0] == '$')
            {
                if(argv[1][1] == '?')
                {
                    printf("%d\n", last_exit);
                    continue;
                }
                else target_env = getenv(argv[1] + 1);
                if(target_env != NULL) printf("%s=%s\n", argv[1] + 1, target_env);
            }
            continue;
        }
        if(strcmp(argv[0], "ls") == 0)//找到argv的NULL,把它往后挪一位,然后之前的位置加上配色方案
        {
            int pos = 0;
            while(argv[pos]) pos++;
            argv[pos++] = (char*)"--color=auto";
            argv[pos] = NULL;
        }
        pid_t id = fork();
        assert(id >= 0);
        (void)id;
        if(id == 0)
        {
            //child
            execvp(argv[0], argv);
            exit(0);
        }
        int status = 0;
        pid_t ret = waitpid(id, &status, 0);
        if(ret > 0)
        {
            last_exit = WEXITSTATUS(status);
        }
    }
}

Gitee:

完整代码

结束。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值