前往小程序,Get更优阅读体验!
立即前往
发布
社区首页 >专栏 >【Linux系统编程】八、进程程序替换

【Linux系统编程】八、进程程序替换

作者头像
利刃大大
发布2025-02-23 08:27:25
发布2025-02-23 08:27:25
4400
代码可运行
举报
文章被收录于专栏:csdn文章搬运csdn文章搬运
运行总次数:0
代码可运行

Ⅰ. 进程替换的概念

将磁盘中指定的程序加载到内存中,让指定的进程进行执行。不论是哪种后端语言写的程序,exec* 类的函数都可以调用。

​ 创建子进程的目的:

  • 执行父进程的部分代码。我们之前所写的代码都属于这种情况。
  • 执行其它程序的代码。想办法让子进程加载磁盘中的程序,执行新程序的代码和数据。

​ 所以 进程替换是为了让子进程能够执行其它程序的代码;进程替换就是以写时拷贝的策略,让第三方进程的代码和数据替换到父进程的代码和数据,给子进程用,因为进程间具有独立性,所以不会影响父进程。以前我们说数据是可写的,代码是不可写的,现在看来,确实如此。但是接下来要把其它程序的代码通过进程替换放在内存里让子进程与之关联,此时就要给代码进行写时拷贝。99% 的情况是对数据进行写时拷贝,1% 的情况是代码依旧是只读,本质就是对父进程不可写,子进程后续调用某些系统调用,实际给子进程重新开辟空间把新进程的代码加载,不让子进程执行父进程的代码(其实也是因为新程序的代码覆盖了子进程的代码)。

Ⅱ. 进程替换的原理

程序替换的本质:用磁盘指定位置上的程序的代码和数据,覆盖进程自身的代码和数据,达到让进程执行指定程序的目的。

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

​ 所以进程替换不会改变进程内核的数据结构,只会修改部分页表数据,然后把新进程的代码和数据加载至内存,重新构建页表映射关系,和父进程彻底脱离。

Ⅲ. 进程替换函数

一、替换函数

​ 一共有七个替换函数,统称为 exec 函数:

代码语言:javascript
代码运行次数:0
复制
#include <unistd.h>
// 系统调用
int execve(const char *filename, char *const argv[],char *const envp[]);

// execve的封装(也就是拓展版本)
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[]);

​ 上面的函数中 execve 是系统级别的函数,而其它的是它的一个封装,也就是功能上的一个拓展,底层还是调用的 execve

​ 这些函数的功能都是一样的,如果用 C++ 去设计这样的接口,一定是重载。这里是使用 C 去设计的,函数名的命名也有区分。下面我们会对这些接口进行演示,但实际在后面常用的也只是部分而已。

​ 🔴 注意:上面参数中的 ... 代表的是参数列表,也就是我们可以传多个参数,就像 printf 一样;除此之外,最后一个参数必须以 NULL 结尾代表结束!(包括 envp[] 中也要以 NULL 结尾

二、共同特性

  • 调用成功则加载新的程序从启动代码开始执行,不再返回
  • 调用出错则返回 -1
  • exec 函数 只有出错的返回值 而没有成功的返回值(因为成功调用的时候已经开始执行新程序,所以没必要有成功的返回值)

三、函数的理解方法

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

  • llist):表示参数采用列表,将参数一个一个传入 exec*
  • vvector):表示参数采用数组,也就是将参数列表写进数组后传数组即可
  • ppath):表示自动搜索环境变量 PATH,只需要传入对应环境变量 PATH 的字符串即可,系统会进行可执行程序的查找
  • eenv):表示自定义环境变量
代码语言:javascript
代码运行次数:0
复制
#include <unistd.h>
int main()
{
    char *const argv[] = {"ps", "-ef", NULL};
    char *const envp[] = {"PATH=/bin:/usr/bin", "TERM=console", NULL};
    
    execl("/bin/ps", "ps", "-ef", NULL);
    
    // 带p的,可以使用环境变量PATH,无需写全路径
    execlp("ps", "ps", "-ef", NULL);
    
    // 带e的,需要自己组装环境变量
    execle("ps", "ps", "-ef", NULL, envp);
    
    // 带v的,可以使用数组传参
    execv("/bin/ps", argv);

    // 带p的,可以使用环境变量PATH,无需写全路径
    execvp("ps", argv);
    
    // 带e的,需要自己组装环境变量
    execve("/bin/ps", argv, envp);
    
    exit(0);
}

四、代码测试

​ 下面我们来讲解几个 exec* 函数,其它类似拓展功能的都是一样的,学会了每个关键字功能就行!

① execl
代码语言:javascript
代码运行次数:0
复制
int execl(const char *path, const char *arg, ...);
// path代表要执行文件的路径
// arg以及...(可变参数列表)就是我们要输入的参数列表,最后以NULL结尾

如果只有单进程的情况:

代码语言:javascript
代码运行次数:0
复制
#include <stdio.h>
#include <unistd.h>
int main()
{
    printf("single process running...\n");

    execl("/usr/bin/ls", "ls", "-a", "-l", NULL);

    printf("single process done\n");
    return 0;
}

运行结果:
[liren@VM-8-2-centos pro_replace]$ ./mypro 
single process running...
total 28
drwxrwxr-x 2 liren liren 4096 Jan 19 12:47 .
drwxrwxr-x 6 liren liren 4096 Jan 19 12:42 ..
-rw-rw-r-- 1 liren liren   72 Jan 19 12:43 makefile
-rwxrwxr-x 1 liren liren 8408 Jan 19 12:47 mypro
-rw-rw-r-- 1 liren liren  246 Jan 19 12:47 repro.c
[liren@VM-8-2-centos pro_replace]$

​ 可以看到执行完 execl最后一句 printf 是被覆盖了,不会执行的

多进程的情况,子进程完成调用函数工作:

代码语言:javascript
代码运行次数:0
复制
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

int main()
{
    pid_t id = fork();
    if(id == 0)
    {
        // child
        printf("我是子进程,pid:%d,ppid:%d\n", getpid(), getppid());
        execl("/usr/bin/ls", "ls", "-a", "-l", NULL);
        exit(1);
    }
    
    // father
    int status = 0;
    pid_t ret = waitpid(id, &status, 0);
    if(ret > 0)
        printf("child status -> sig: %d, code: %d\n", status & 0x7F, (status >> 8) & 0xFF);
    else
        printf("wait error!\n");
    return 0;
}

运行结果:
[liren@VM-8-2-centos pro_replace]$ ./mypro 
我是子进程,pid:20462,ppid:20461
total 28
drwxrwxr-x 2 liren liren 4096 Jan 19 12:54 .
drwxrwxr-x 6 liren liren 4096 Jan 19 12:42 ..
-rw-rw-r-- 1 liren liren   72 Jan 19 12:43 makefile
-rwxrwxr-x 1 liren liren 8720 Jan 19 12:54 mypro
-rw-rw-r-- 1 liren liren  604 Jan 19 12:54 repro.c
child status -> sig: 0, code: 0
[liren@VM-8-2-centos pro_replace]$

可以看到子进程中调用 execl 后并没有去调用到 exit(1),同样验证了上面的说法!

② execv
代码语言:javascript
代码运行次数:0
复制
int execv(const char *path, char *const argv[]);
// path代表要执行文件的路径
// argv代表传入的是一个参数数组,其中每个元素都是字符指针也就是字符串,其中要注意的是这个数组的类型是char* const

execvexecl 较为类似,它们的唯一差别是,如果需要传多个参数,那么 execl 是以可变参数的形式进行列表传参,而 execv 是以指针数组的形式传参。

代码语言:javascript
代码运行次数:0
复制
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
    pid_t id = fork();
    if(id == 0)
    {
        // child
        //const char* const my_argv[] = {"ls", "-l", "-a", "-i", NULL}; ❌ 注意要与函数原型的参数类型匹配
        char* const my_argv[] = {"ls", "-l", "-a", "-i", NULL};
        printf("我是子进程,pid:%d,ppid:%d\n", getpid(), getppid());
        execv("/usr/bin/ls", my_argv);
        exit(1);
    }
    
    // father
    int status = 0;
    pid_t ret = waitpid(id, &status, 0);
    if(ret > 0)
        printf("child status -> sig: %d, code: %d\n", status & 0x7F, (status >> 8) & 0xFF);
    else
        printf("wait error!\n");
    return 0;
}

运行结果:
[liren@VM-8-2-centos pro_replace]$ ./mypro 
我是子进程,pid:22396,ppid:22395
total 28
1180646 drwxrwxr-x 2 liren liren 4096 Jan 19 13:03 .
 789602 drwxrwxr-x 6 liren liren 4096 Jan 19 12:42 ..
1180647 -rw-rw-r-- 1 liren liren   72 Jan 19 12:43 makefile
1180649 -rwxrwxr-x 1 liren liren 8720 Jan 19 13:03 mypro
1180648 -rw-rw-r-- 1 liren liren  776 Jan 19 13:03 repro.c
child status -> sig: 0, code: 0
[liren@VM-8-2-centos pro_replace]$
③ execlp
代码语言:javascript
代码运行次数:0
复制
int execlp(const char *file, const char *arg, ...);
// file代表的是可执行文件的名称,如"ls"、"touch"等等
// arg以及...(可变参数列表)就是我们要输入的参数列表,最后以NULL结尾

execlp 相比 execl 在命名上多了 1p,且参数只有第 1 个不同:即 execlp 不需要带路径,在执行时它会拿着你要执行的程序自动的在系统 PATH 环境变量中查找你要执行的目标程序。

代码语言:javascript
代码运行次数:0
复制
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
    pid_t id = fork();
    if(id == 0)
    {
        // child
        printf("我是子进程,pid:%d,ppid:%d\n", getpid(), getppid());
        execlp("ls", "ls", "-a", "-l", NULL);
        exit(1);
    }
    
    // father
    int status = 0;
    pid_t ret = waitpid(id, &status, 0);
    if(ret > 0)
        printf("child status -> sig: %d, code: %d\n", status & 0x7F, (status >> 8) & 0xFF);
    else
        printf("wait error!\n");
    return 0;
}

运行结果:
[liren@VM-8-2-centos pro_replace]$ ./mypro 
我是子进程,pid:23307,ppid:23306
total 28
drwxrwxr-x 2 liren liren 4096 Jan 19 13:07 .
drwxrwxr-x 6 liren liren 4096 Jan 19 12:42 ..
-rw-rw-r-- 1 liren liren   72 Jan 19 12:43 makefile
-rwxrwxr-x 1 liren liren 8720 Jan 19 13:07 mypro
-rw-rw-r-- 1 liren liren  785 Jan 19 13:07 repro.c
child status -> sig: 0, code: 0
[liren@VM-8-2-centos pro_replace]$

​ 带 p 的含义就是不用带路径,系统会自动搜索你要执行的程序,不带 p 则相反。当然这里的 搜索默认只有系统的命令才能找到,如果需要执行自己的命令,需要提前把自己的命令与 PATH 关联。

④ execvp
代码语言:javascript
代码运行次数:0
复制
int execvp(const char *file, char *const argv[]);
// file代表的是可执行文件的名称,如"ls"、"touch"等等
// argv代表传入的是一个参数数组,其中每个元素都是字符指针也就是字符串,其中要注意的是这个数组的类型是char* const

​ 通过上面我们知道 execvp 无非就是不带路径,使用指针数组传参。

代码语言:javascript
代码运行次数:0
复制
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
    pid_t id = fork();
    if(id == 0)
    {
        // child
        char* const my_argv[] = {"ls", "-l", "-a", NULL};
        printf("我是子进程,pid:%d,ppid:%d\n", getpid(), getppid());
        execvp("ls", my_argv);
        exit(1);
    }

    // father
    int status = 0;
    pid_t ret = waitpid(id, &status, 0);
    if(ret > 0)
        printf("child status -> sig: %d, code: %d\n", status & 0x7F, (status >> 8) & 0xFF);
    else
        printf("wait error!\n");
    return 0;
}

运行结果:
[liren@VM-8-2-centos pro_replace]$ ./mypro 
我是子进程,pid:24517,ppid:24516
total 28
drwxrwxr-x 2 liren liren 4096 Jan 19 13:13 .
drwxrwxr-x 6 liren liren 4096 Jan 19 12:42 ..
-rw-rw-r-- 1 liren liren   72 Jan 19 12:43 makefile
-rwxrwxr-x 1 liren liren 8720 Jan 19 13:13 mypro
-rw-rw-r-- 1 liren liren  639 Jan 19 13:13 repro.c
child status -> sig: 0, code: 0
[liren@VM-8-2-centos pro_replace]$
⑤ 带 eexec* 函数以及调用自定义的可执行文件
在这里插入图片描述
在这里插入图片描述

e 表示传入默认的或者自定义的环境变量给目标可执行程序

​ 下面我们分别演示传入系统中的环境变量以及我们自己写的环境变量,在这之前我们先演示一下如何调用我们自己的写的程序,所以我们创建一个 mycmd.c 文件:

代码语言:javascript
代码运行次数:0
复制
// mycmd.c
#include <stdio.h>
int main()
{
    for(int i = 0; i < 5; ++i)
        printf("cmd:%d\n", i);
    
    // 自定义的环境变量
    printf("MYENV:%s\n", getenv("MYENV"));
    
    // 系统自带的环境变量
    printf("PATH:%s\n", getenv("PATH"));
    printf("PWD:%s\n", getenv("PWD"));
    return 0;
}

​ 由于我们需要生成多个可执行文件,但是由于 makefile 中的机制是默认只生成从上往下的第一个可执行文件,所以我们在 makefile 中添加一个伪目标 all,这个时候 makefile 从上往下扫描 all 中定义的可执行文件名称,从而达到生成它们的目的!

值得注意的是 all 要放在文件中的开头

代码语言:javascript
代码运行次数:0
复制
# makefile文件
.PHONY:all
all: mypro mycmd

mypro:repro.c
	gcc -o $@ $^ -std=c99 
mycmd:mycmd.c
	gcc -o $@ $^ -std=c99

.PHONY:clean
clean:
	rm -f mypro mycmd

​ 接下来我们修改一下 repro.c 中的一些细节:

代码语言:javascript
代码运行次数:0
复制
// repro.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
    pid_t id = fork();
    if(id == 0)
    {
        // child
        char* const my_env[] = {"MYENV=helloLinux", NULL};
        printf("我是子进程,pid:%d,ppid:%d\n", getpid(), getppid());
        
        // 调用自己写的mycmd文件,并将my_env传过去
        execle("./mycmd", "mycmd", NULL, my_env);

        exit(1);
    }

    // father
    int status = 0;
    pid_t ret = waitpid(id, &status, 0);
    if(ret > 0)
        printf("child status -> sig: %d, code: %d\n", status & 0x7F, (status >> 8) & 0xFF);
    else
        printf("wait error!\n");
    return 0;
}

​ 嘶~~怎么这么奇怪对不对,我们明明第二个和第三个打印的是 PATHBASH,但是最后打印出来的是 null ❓❓❓

​ 其实原因就是因为只是替换了程序,而我们调用 exec*e 的时候,只是将参数中的 envp 数组中内的环境变量传了过去,所以替换后只能查看到我们传过去的环境变量,而不能看到系统中的环境变量!

​ 那如果我们要实现既能看到系统中的环境变量,又想自己传环境变量过去,该怎么办呢 ❓❓❓

​ 这里就要用到我们之前在讲环境变量的时候埋下的伏笔,也就是 putenv 函数,将指定环境变量导入到系统中 environ 指向的环境变量表

​ 🔴 下面程序在 mycmd.c 中做了一些小改动,将 BASH 改成了 PWD,请注意

代码语言:javascript
代码运行次数:0
复制
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
    pid_t id = fork();
    if(id == 0)
    {
        // child
        printf("我是子进程,pid:%d,ppid:%d\n", getpid(), getppid());

        putenv("MYENV=helloLinux"); // 将自定义环境变量添加到系统中去
        extern char** environ; 	    // 声明一下防止报错
        execle("./mycmd", "mycmd", NULL, environ); // 将系统中的环境变量传过去
        
        exit(1);
    }

    // father
    int status = 0;
    pid_t ret = waitpid(id, &status, 0);
    if(ret > 0)
        printf("child status -> sig: %d, code: %d\n", status & 0x7F, (status >> 8) & 0xFF);
    else
        printf("wait error!\n");
    return 0;
}

​ 执行结果:

⑥ 不同语言之间的相互调用

程序替换可以调用任何后端语言对应的可执行文件

​ 我们这里用 C 语言来调用 C++ 举个例子,首先写一个 C++ 文件叫做 otherLanguage.cc ,并用 C 程序调用:

代码语言:javascript
代码运行次数:0
复制
// otherLanguage.cc
#include <iostream>
using namespace std;
int main()
{
    cout << "hello C++" << endl;
    cout << "hello C++" << endl;
    cout << "hello C++" << endl;
    cout << "hello C++" << endl;
    return 0;
}

// repro.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
    pid_t id = fork();
    if(id == 0)
    {
        // child
        char* const my_env[] = {"MYENV=helloLinux", NULL};
        printf("我是子进程,pid:%d,ppid:%d\n", getpid(), getppid());
        
        // 调用otherLanguage生成的可执行文件
        execl("./otherLanguage", "otherLanguage", NULL);
        exit(1);
    }

    // father
    int status = 0;
    pid_t ret = waitpid(id, &status, 0);
    if(ret > 0)
        printf("child status -> sig: %d, code: %d\n", status & 0x7F, (status >> 8) & 0xFF);
    else
        printf("wait error!\n");
    return 0;
}

​ 执行结果:

Ⅳ. main 函数的调用

​ 既然我们可以用自己的程序调用自己的程序,main 函数也是一个程序,它也需要被调用,也要被传参。

​ 它的 原理其实就是被 execve 这个系统函数调用的,通过 execve 函数传递 argcargvenvmain 函数!所以 exec*linux 中也称为 加载器

​ 一个完整的集成开发环境的组件肯定包括编辑器、编译器、调试器、加载器等。一个软件被加载到内存里,肯定是运行起来,形成进程,进程再调用 exec 系列的函数就可以完成加载的过程。所以 exec 可以理解成一种特殊的加载器

Ⅴ. 模拟实现一个简易的 shell 程序

​ 用下图的时间轴来表示事件的发生次序。其中时间从左向右。shell 由标识为 sh 的方块代表,它随着时间的流逝从左向右移动。shell 从用户读入字符串 "ls"shell 建立一个新的进程,然后在那个进程中运行 ls 程序并等待那个进程结束。

​ 然后我们的 shell 读取新的一行输入,建立一个新的进程,在这个进程中运行程序并等待这个进程结束。所以要写一个shell,需要循环以下过程:

  1. 获取命令行
  2. 解析命令行
  3. 建立一个子进程(fork
  4. 替换子进程(execvp
  5. 父进程等待子进程退出(wait

​ 根据这些思路,和我们前面的学的技术,就可以自己来实现一个 shell 了。

① 获取命令行

​ 这里命令行的格式比较随意,我们就用 用户+主机名+当前地址 为命令行格式!

代码语言:javascript
代码运行次数:0
复制
printf("[%s@%s %s]~ ", getenv("USER"), getenv("HOSTNAME"), getenv("PWD"));
fflush(stdout); // 刷新一下缓冲区

​ 调用效果:

② 解析命令行

​ 因为用的是 C 语言,所以我们可以用宏来定义字符数组的大小和指针数组的大小,要注意的是因为每次我们 fgets 后要按一次回车,回车也会被放到缓冲区,也就是说加入输入了 "123\n" 那么我们要将这个 \n 换成 \0 才能保证字符数组的正确性

​ 在切割字符串的时候调用的是 strtok 这个函数,详细的参数可以自行去网上或者手册查看,第一个参数是要切割的目标,第二个参数是分割标志,然后利用其最后没有子串也就是切割完后的返回值是 NULL,我们利用 while 循环赋值给 myargv[i] ,最后让 myargv[i] 去判断是否终止即可!

代码语言:javascript
代码运行次数:0
复制
#define NUM 1024  // 定义用户输入的最大数,其中个数为1023个,最后一位给'\0'
#define OPTION_NUM 64 // 定义切割后每个选项的最大个数
char lineCommand[NUM];  // 存放用户输入的字符串数组
char* myargv[OPTION_NUM]; // 存放切割后单词的指针数组

	// 获取用户输入
    char* s = fgets(lineCommand, sizeof(lineCommand) - 1, stdin);
    assert(s != NULL); // 检测一下输入是否正确
    lineCommand[strlen(lineCommand) - 1] = '\0'; // 由于我们按回车会有一个'\n',我们将其设为'\0'
    (void)s; // 防止编译器在release模式下告警

	// 切割字符串
    myargv[0] = strtok(lineCommand, " ");
    int i = 1;

	// 没有子串后,strtok最后切割完返回的是NULL,用myargv[i]接收判断后退出
    while(myargv[i++] = strtok(NULL, " "))
    {}
③ 加入条件编译方便调试

​ 将 myargv 中的子串打印出来看看是否正确,编译的时候需要在 makefile 中加上 -DDEBUG 即可

代码语言:javascript
代码运行次数:0
复制
// 用条件编译,测试是否成功
#ifdef DEBUG
        for(int i = 0; myargv[i]; ++i)
            printf("myargv[%d]:%s\n", i, myargv[i]);
#endif
④ 创建子进程,替换子进程后等待子进程退出

​ 因为我们用的是 myargv 数组存放我们的子串,并且我们习惯输入指令是不带路径的,所以我们要用带有 vp 的,最后选择的函数就是 execvp

代码语言:javascript
代码运行次数:0
复制
    // 执行命令
    pid_t id = fork();
    assert(id != -1);

    if(id == 0)
    {
        // 由于指令输入习惯,所以选择最适合的execvp
        execvp(myargv[0], myargv);

        // 若失败终止信号设为1
        exit(1);
    }

    int status = 0;
    pid_t ret = waitpid(id, &status, 0); // 回收已退出的子进程的信息
    assert(ret > 0);
    (void)ret; // 防止编译器在release模式下告警
⑤ 完善一些指令的问题

​ 这些代码在完整代码中可以看,主要说一下几个指令:

​ 第一个就是 ls 指令,如果我们想要它每次打印的时候带上颜色,我们可以在一开始切割之前就判断是否为 ls ,是的话我们将第二个参数变成 --color=auto 这样子就能带上颜色了!

​ 第二个就是 cd 指令,这里就要扯到一个知识点,就是 内建/内置指令 ,其实就是不用子进程帮我们去完成,直接让当前父进程自己完成该命令即可,因为 cd 的目的是为了让当前父进程抵达某个路径,但是如果我们用子进程去帮我们的话,由于进程之间的独立性,最后并不会让父进程也就是当前进程得到任何效果,所以对于这种指令,我们只需要在父进程里面直接完成即可!!!并且我们可以调用一个叫做 chdir 的指令帮我们更改当前的路径,因为当前路径的本质其实就是当前文件!

​ 下面通过进入 /proc 中查看 myshell 进程,其中 cwd 才是我们需要去更改的当前目录也就是当前路径!

​ 第三个就是 echo 指令了,和 cd 指令一样,它们都是 内建/内置指令 ,所以执行它们的时候不需要去创建子进程去帮我们完成任务,直接在当前进程中执行即可~

​ 要注意的是我们这里主要实现的还是 echo $? ,就是维护两个全局变量 lastSiglastCode ,然后每次调用的时候将它们打印出来即可!

完整代码
代码语言:javascript
代码运行次数:0
复制
// makefile
myshell:myshell.c
	gcc -o myshell myshell.c -std=c99 #-DDEBUG
.PHONY:clean
clean:
	rm -f myshell
        
------------------------------------------------------
        
// myshell.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <assert.h>
#include <string.h>

#define NUM 1024  // 定义用户输入的最大数,其中个数为1023个,最后一位给'\0'
#define OPTION_NUM 64 // 定义切割后每个选项的最大个数
char lineCommand[NUM];  // 存放用户输入的字符串数组
char* myargv[OPTION_NUM]; // 存放切割后单词的指针数组

int lastSig = 0; // 终止信号
int lastCode = 0; // tui'chu

int main()
{
    // 因为shell是循环输入的,所以要套在死循环里面
    while(1)
    {
        // 获取命令行
        printf("[%s@%s %s]~ ", getenv("USER"), getenv("HOSTNAME"), getenv("PWD"));
        fflush(stdout);
    
        // 获取用户输入
        char* s = fgets(lineCommand, sizeof(lineCommand) - 1, stdin);
        assert(s != NULL); // 检测一下输入是否正确
        lineCommand[strlen(lineCommand) - 1] = '\0'; // 由于我们按回车会有一个'\n',我们将其设为'\0'
        (void)s; // 防止编译器在release模式下告警

        // 切割字符串
        myargv[0] = strtok(lineCommand, " ");
        int i = 1;

        // 若是ls,可以给它加上颜色
        if(myargv[0] != NULL && strcmp("ls", myargv[0]) == 0)
            myargv[i++] = (char*)"--color=auto";

        // 没有子串后,strtok最后切割完返回的是NULL,用myargv[i]接收判断后退出
        while(myargv[i++] = strtok(NULL, " "))
        {}
    
        // 若是cd,则可以直接在当前进程修改
        // 像这种指令不需要子进程来执行,而是让shell自己执行的命令,叫做内建/内置命令
        if(myargv[0] != NULL & strcmp("cd", myargv[0]) == 0)
        {
            if(myargv[1] != NULL)
                chdir(myargv[1]);
            continue;
        }

        // echo $? 时候打印出退出码和终止信号
        if(myargv[0] && myargv[1] && strcmp(myargv[0], "echo") == 0)
        {
            if(strcmp(myargv[1], "$?") == 0)
                printf("退出状态:%d,终止信号:%d\n", lastCode, lastSig);
            else
                printf("%s\n", myargv[1]);
            continue;
        }

// 用条件编译,测试是否成功
#ifdef DEBUG
        for(int i = 0; myargv[i]; ++i)
            printf("myargv[%d]:%s\n", i, myargv[i]);
#endif

        // 执行命令
        pid_t id = fork();
        assert(id != -1);

        if(id == 0)
        {
            // 由于指令输入习惯,所以选择最适合的execvp
            execvp(myargv[0], myargv);

            // 若失败终止信号设为1
            exit(1);
        }

        int status = 0;
        pid_t ret = waitpid(id, &status, 0); // 回收已退出的子进程的信息
        assert(ret > 0);
        (void)ret; // 防止编译器在release模式下告警

        // 将最近一次的退出码和终止信号更新
        lastCode = (status >> 8) & 0xFF;
        lastSig = status & 0x7F;
    }
    return 0;
}
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2025-02-22,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • Ⅰ. 进程替换的概念
  • Ⅱ. 进程替换的原理
  • Ⅲ. 进程替换函数
    • 一、替换函数
    • 二、共同特性
    • 三、函数的理解方法
    • 四、代码测试
      • ① execl
      • ② execv
      • ③ execlp
      • ④ execvp
      • ⑤ 带 e 的 exec* 函数以及调用自定义的可执行文件
      • ⑥ 不同语言之间的相互调用
  • Ⅳ. main 函数的调用
  • Ⅴ. 模拟实现一个简易的 shell 程序
    • ① 获取命令行
    • ② 解析命令行
    • ③ 加入条件编译方便调试
    • ④ 创建子进程,替换子进程后等待子进程退出
    • ⑤ 完善一些指令的问题
    • 完整代码
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档