Linux进程控制

1、进程创建

在Linux中,我们可以通过fork来创建子进程。fork调用失败返回-1,调用成功给父进程返回子进程的pid,给子进程返回0。
在这里插入图片描述
下面演示使用fork创建一批子进程:

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

#define N 5

void runChild()
{
    int cnt = 10;
    while (cnt--)
    {
        printf("我是子进程, pid: %d, ppid: %d\n", getpid(), getppid());
        sleep(1);
    }
}

int main()
{
    int i = 0;
    for (i = 0; i < N; i++)
    {
        pid_t id = fork();
        if (id == 0)
        {
            runChild();
            exit(0);
        }
    }
    sleep(3000);
    return 0;
}

在这里插入图片描述

fork创建子进程后,操作系统要给子进程创建task_struct结构体,以父进程的task_struct为模板初始化子进程,同时拷贝父进程的地址空间和页表给子进程。fork之后,父子进程代码共享,数据写时拷贝。

在这里插入图片描述
fork创建子进程后,它们指向的数据和代码是同一份,对于数据来说父子进程在页表的标志位都设置为只读。当某一方尝试对数据进行写入时,比如子进程写入,操作系统会辨别发现这里的数据是只读的,然后触发缺页中断,在内存中开辟新空间,拷贝一份数据,然后修改子进程页表中的物理地址,同时将父子进程对于该数据的权限修改为可读可写。


2、进程终止

进程退出有三种场景:
1、代码运行完毕,结果正确。
2、代码运行完毕,结果不正确。
3、代码异常终止。

为什么我们平时写的代码main函数的返回值是0呢?如果返回1、2…等其他数字呢?
我们就写个main函数返回return 1的代码:
运行后,我们可以通过echo $?查看我们main函数的返回值:
在这里插入图片描述
我们平时main函数都是返回0,代表的是success,表示程序执行完毕,结果正确。

那么对于程序执行完毕,结果正确不正确我们是不是应该关心呢?那么谁来关心呢?
当然是该进程的父进程来关心,父进程要关心他所交给子进程的任务子进程完成的如何了。如果代码运行完结果正确,那当然是皆大欢喜。如果代码运行完结果不正确,子进程应该要告诉父进程为什么出错了。
所以通过return返回不同的值,表征不同的出错原因。我们把return的值称为——退出码。

使用echo $?可以查看最近一次进程的退出码。
在这里插入图片描述
我们在main函数中return 1,所以运行程序后使用echo $?查看最近进程的退出码就是1。那么再次查看为什么变成了0呢?因为echo命令也要有进程来执行,echo命令正常执行结果正确,所以退出码是0。我们再次查看,看到的就是echo程序的退出码。

在C语言中我们知道有错误码描述,下面介绍一个函数:strerror
在这里插入图片描述
sterror返回错误码所对应的字符串描述信息。

那么我们也不知道多少个错误码,我们可以直接循环两百次打印看看:
在这里插入图片描述
总共一百多个错误码描述,可以看到每个值对应的错误码描述。0表示的就是success。
在这里插入图片描述
我们在使用ls指令查看不存在的文件时,就会显示No such file or directory,这不就是错误码2对应的描述吗。我们执行ls指令,结果不正确就通过退出码来标识,然后打印给用户看。
另外C语言还存在一个全局变量errno ,当我们malloc失败,调用某些库函数失败时errno就会被设置。所以调用库函数失败我们可以通过strerror(errno)打印查看错误码信息。

系统提供的错误码和错误码描述是有对应关系的。
那么我们可不可以自己设计一套退出码体系呢?当然可以,类似下面的方式:
在这里插入图片描述


再来看代码异常终止,代码异常终止本质上代码可能没有跑完,那么我们就不需要再关系进程的退出码了,你代码都没跑完我关心你退出码有什么意义。所以我们要关心进程发生了什么异常。
下面我们看C语言中除0错误和指针越界访问异常:
在这里插入图片描述
在这里插入图片描述
进程出现异常,本质是进程收到了信号。我们可以使用kill -l查看所有信号:
在这里插入图片描述
除零错误本质上是CPU状态寄存器出现了溢出错误,给对应进程发送8号信号。
野指针本质上是CPU访问虚拟地址,对应页表没有映射关系,或者建立了映射关系,但是权限设置为只读,所以给进程发送11号信号。

如何验证?我们可以给一个正在运行的进程发送8号和11号信号:
在这里插入图片描述
在这里插入图片描述

所以对于一个进程退出,我们要先关心该进程是否出现了代码异常,也就是要先看它有没有收到信号。其次再看它的退出码,判断进程运行结果是否正确。


说了这么多,终止进程如何操作呢?
在这里插入图片描述
使用exit终止进程,status就是进程的退出码,用status表征进程运行结果是否正确。
在这里插入图片描述
在这里插入图片描述
可以看到,调用exit函数后,进程直接退出,不会打印后面的内容,并且我们使用echo $?查看进程退出码就是我们传给exit的参数。

exit和return在main函数里面是等价的。
return:在main函数中表示进程退出。在其他函数中表示函数返回,还会继续向后执行。
exit:在任意地方被调用都表示进程退出。

_exit系统调用接口:
在这里插入图片描述
下面对比_exit和exit:
在这里插入图片描述
在这里插入图片描述
把exit换成_exit再次运行:
在这里插入图片描述
我们发现第一份代码需要经过一段时间才会将hello linux打印出来。而调用_exit则是连打印都没有了。这是为什么呢?
在进度条我们说过,C语言存在缓冲区,那么hello linux肯定是先存入缓冲区中,然后等待某种条件就绪才会刷新,这里的条件就包括(遇到\n,进程退出)。所以调用exit后进程退出它才刷新出来。
而_exit是系统调用接口,它会直接到系统内核去执行。
在这里插入图片描述
从上图可以看到,_exit会直接到系统内核中去执行,而exit会先刷新缓冲区,然后再到内核中去执行。
exit本质上就是先执行清理,刷新缓冲区、关闭流等,然后再调用_exit,所以它们是上下层关系。

那么我们现在就可以判断,这个缓冲区就对不在哪里?——这个缓冲区绝对不可能在内核中。因为如果在内核中_exit肯定会刷新缓冲区,操作系统不会做任何浪费资源的事情。


3、进程等待

为什么要进程等待?
僵尸进程无法被杀死,需要通过进程等待来杀掉它,进而解决内存泄漏的问题。——这是必须解决的
我们通过进程等待可以获取子进程的退出情况——知道我布置给子进程的任务它完成得怎么样了——可选的。

什么是进程等待?
通过系统调用wait/waitpid,来对子进程进行状态检测与回收的功能。

在这里插入图片描述
下面我们先看wait的使用,wait有一个int*的参数,我们暂时不关心它,后面会讲,所以我们在下面的代码调用wait传参的时候暂时设为NULL。
在这里插入图片描述
wait成功返回子进程的pid,失败则返回-1。

3.1、使用wait等待子进程退出:

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

int main()
{
    pid_t id = fork();
    if (id < 0)
    {
        perror("fork:");
        return 1;
    }
    else if (id == 0)
    {
        int cnt = 5;
        while (cnt)
        {
            printf("I am child, pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt);
            cnt--;
            sleep(1);
        }
        exit(0);
    }
    sleep(10);
    pid_t ret = wait(NULL);
    if (ret > 0)
    {
        printf("parent wait %d success\n", ret);
    }
    return 0;
}

在这里插入图片描述
可以看到父进程先休眠10秒,子进程运行结束后退出,子进程变成僵尸状态,然后父进程休眠结束后通过wait回收子进程资源,右侧就看不到子进程了,然后最后父进程退出。
需要注意的是,如果父进程不休眠10秒,那么就会在wait处一直等待子进程返回,我们称为阻塞式等待。并且wait函数等待的是任意一个进程。

3.2、使用wait等待多个子进程退出:
在这里插入图片描述
在这里插入图片描述

waitpid的第一个参数可以传对应进程的pid,表示等待对应进程,也可以传-1,表示等待任意一个进程。后面两个参数暂时设置为NULL和0。waitpid等待成功返回子进程pid,等待失败返回-1。这里的等待失败表示的是你传的第一个参数不正确,并不是它的子进程,那当然就会失败。

3.3、使用waitpid等待指定进程:
在这里插入图片描述
如果第一个参数给-1就是等待任意子进程,效果是一样的。

3.4、使用waitpid等待多个子进程:
在这里插入图片描述


在这里插入图片描述
下面来看它们都有的共同的参数status。
那么父进程等待,期望获得子进程的那些信息呢?1、子进程代码是否异常?2、没有异常结果是否正确?通过退出码来标识。
这里的status是一个输出型参数,int类型会被分成几部分使用。int类型有32个比特位,但是这里我们只考虑它的低16位。如下图:
在这里插入图片描述

这16位又被分成三个部分,低7位表示终止信号,第8位标识core dump标志这个我们到动静态库那边再讲,高8位标识退出状态。
我们可以使用kill -l命令查看所有信号,发现信号是从1开始的,所以我们可以判断status的低7位是否为零,从而判断进程是否收到了信号,进而判断进程代码是否异常。那么如果进程代码没有异常,我们就可以提取高8位的值,来判断进程的退出码是什么,从而判断进程执行结果是否正确,或者出错的原因。

3.5、提取子进程的退出状态:status&0x7f可以获取低7位的值,(status>>8)&0xff可以获取高8位的值
在这里插入图片描述
在这里插入图片描述

3.6、我们让子进程除零错误或空指针访问,那么子进程就会被信号所杀,然后我们期望在父进程看到子进程的低7位是不为0的。
在这里插入图片描述
我们在子进程中直接除零错误或者空指针访问,父进程成功等待子进程后,提取子进程的退出信息,可以看到低7位不为0,而是11或8,标识收到的几号信号。而退出码我们此时就不关心了。

3.7、通过宏来提取信息:
WIFEXITED(status):若子进程正常退出,则为正。用来判断子进程是否代码异常。
WEXITSTATUS(status):提取子进程的退出码。

在这里插入图片描述


waitpid原理:
在这里插入图片描述
首先子进程退出对应的PCB绝对不能被释放,而它的代码和数据是可以被释放的,节省操作系统资源。那子进程的退出信息应该保存在哪里?当然是子进程的PCB中,所以调用waitpid就是到子进程的PCB中获取其PCB内的sigcode和exitcode,然后将这两个值通过位运算组合成status,这样上层就可以获取子进程的退出信息了。那么父进程获取后,就可以将子进程PCB释放掉了。
在这里插入图片描述


最后来谈waitpid的第三个参数options,我们之前给的都是0,表示阻塞式等待,我们还可以给一个宏:WNOHANG,表示非阻塞等待。那么非阻塞等待就有一个对应的返回值,就是0,表示子进程还未退出,父进程等待的条件还未就绪。

下面讲一个例子加深理解:
假设你们学校老师说过三天要考C语言了,但是你一点都不慌,过了两天,明天已经准备考试了,这时候你才想起来得好好复习一下,但是你们班有个学霸小张,上课笔记也做的特别认真,所以这天早上你赶紧去到小张宿舍楼下,并打电话给他说,小张你在干嘛呢,小张说我在复习呢,你说小张你赶紧跟我去复习时给我复习一下C语言,然后你的笔记给我看一下,小张说好的,不过得等我先复习完。电话挂了,然后你就在楼下等小张了,但是过了一段时间小张还没下来,你又打电话去问,小张说快了。电话挂了,然后你继续等,过了一会小张还没下来,所以你又打电话给小张,小张说马上了。但是过了一会他还是没下来,前前后后你可能打了二十个电话,最终小张终于到楼下了,你们一起去吃饭,吃完饭就去自习室复习了。那么,在这个过程中,小张相当于是操作系统,你相当于是父进程,打电话的本质就是系统调用,那么小张还没好就是检查不成功,电话挂掉就是系统调用返回。所以这个过程中你就是处于非阻塞,且时不时去询问小张,这就是非阻塞+轮询。

C语言考完了,你考了61分,刚好过线,很高兴。但是这天老师又说过两天要考数据结构了,你还是心不在焉。考试前一天,你又到小张宿舍楼下,并打电话给小张说,小张赶紧跟我去自习室,帮我复习一下数据结构,小张说好的,不过我现在在复习,你得等一会。这时候你说,那这样,电话也别挂了,等下你好了跟我说一声。小张想竟然还有这种需求,好吧,那我就把电话放在旁边,等下复习好了直接跟他说。所以你就一直等一直等,等了好久终于小张复习完了,然后人下了宿舍楼跟你说他到了,你也看到了小张了,这时候才把电话挂了。然后你们就去吃饭,然后愉快的去自习室了。这时候你就相当于是处于阻塞状态,一直等到小张复习完。

很幸运,你数据结构59分,但是老师给了你机会,让你60分过了。那么又来了,这天老师说过两天要考操作系统,那么这时候你就很慌,操作系统这么难,没考过可咋办,所以你又去小张宿舍楼下,不过这时候你还带了本操作系统的书,然后打电话给小张说,小张老样子,小张说好的,得等我复习完。有了前两次的经验,你也知道小张一时半会下不来,所以就带了本书,然后打完电话你就在那看了会书。然后又继续打电话问小张什么时候下来,小张说快了。挂了电话后你又继续看了会书,就这样直到小张下来,你们就一同去自习室了。这时候你不断打电话去问小张,小张还没复习完,你就做自己的事情。这就是非阻塞+轮询+自己的事情。

3.8、非阻塞轮询等待子进程:
在这里插入图片描述
在这里插入图片描述
父进程每隔一秒对子进程进行轮询,同时做自己的事情:
在这里插入图片描述


4、进程程序替换

man查看3号手册:man 3 execl
在这里插入图片描述
存在以上这六个函数,我们先挑一个来演示看看现象:

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

这六个函数可以进行程序替换。execl的第一个参数path表示可执行程序的路径,而指令本身就是可执行程序,所以我们就以/usr/bin下的可执行程序来演示。然后arg表示选项,…是可变参数,所以后面跟你需要加的选项。那么最后可变参数需要跟NULL。用法如下:

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

int main()
{
    printf("before: I am a process, pid:%d, ppid:%d\n", getpid(), getppid());
	
    execl("/usr/bin/ls", "ls", "-l", "-a", NULL);

    printf("after: I am a process, pid:%d, ppid:%d\n", getpid(), getppid());
    return 0;
}

在这里插入图片描述
运行程序后,我们发现打印了before,然后执行了ls指令,但是after并没有执行。这就是最简单的程序替换。


进程程序替换的原理:
CPU调度进程,操作系统给进程创建了PCB、地址空间、页表,程序的代码和数据也加载了一部分到内存中,虚拟地址通过页表映射物理地址。这时候程序替换执行了execl,那么进程就要去执行磁盘下的ls可执行程序。本质上就是把ls程序的数据和代码替换掉原来进程的数据和代码。那么该进程就去执行ls的代码了。这就是程序替换的基本原理。

下面再来看多进程的程序替换:

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

int main()
{
    pid_t id = fork();
    if (id == 0)
    {
        printf("before: I am child, pid:%d, ppid:%d\n", getpid(), getppid());
        sleep(3);
        execl("/usr/bin/ls", "ls", "-a", "-l", NULL);
        printf("after: I am child, pid:%d, ppid:%d\n", getpid(), getppid());
        exit(0);
    }
    pid_t ret = waitpid(id, NULL, 0);
    if (ret == id) 
    {
        printf("father wait %d success\n", ret);
    }
    sleep(3);
    
    return 0;
}

在这里插入图片描述
父进程创建子进程,操作系统要给子进程创建task_struct、mm_struct、页表,由于父子进程数据和代码共享,所以它们映射的物理内存是一样的。而子进程进行程序替换,需要将ls程序的代码和数据替换到物理内存中,那么就是要写入,所以发生写时拷贝,重新开辟新空间,然后将ls程序的数据和代码加载到内存中,修改子进程页表中的物理地址,从而保证了进程间的独立性。所以在这里我们可以看到,不仅数据发生了写时拷贝,代码也是。这样才不会影响到父进程。

那么程序替换有没有创建新进程??——不创建新进程,只进行进程的程序代码和数据替换工作。
并且我们注意到,子进程程序替换成功之后,并不会执行后面的打印after,所以程序替换成功,不会执行exec*后续的代码。只有替换失败才可能执行后面的代码。所以exec*系列函数没有成功返回值,只有失败返回值,失败返回-1。
小知识:Linux中可执行程序是有格式的——ELF,可执行程序是有表头的,表头存储了可执行程序的入口地址。所以程序替换之后可以找到替换的程序的入口地址。

在这里插入图片描述
以上是对execl函数的分析,这个l就是list,表示传参以上面这种方式,就类似链表一个一个节点一样,然后最后给NULL。
下面看其他函数:

int execlp(const char *file, const char *arg, ...);
execlp("ls", "ls", "-a", "-l", NULL); 

execlp:p表示的就是环境变量的PATH,也就是说第一个参数我们可以直接传指令,execlp会直接帮我们在系统环境变量PATH中的路径进行搜索。


int execv(const char *path, char *const argv[]);
//execl("/usr/bin/ls", "ls", "-a", "-l", NULL);
//execlp("ls", "ls", "-a", "-l", NULL);
char* const myargv[] = {
    "ls",
    "-a",
    "-l",
    NULL
};
execv("/usr/bin/ls", myargv);

execv:v表示vector,所以第二个参数argv是以指针数组的形式去传参的。


int execvp(const char *file, char *const argv[]);
//execl("/usr/bin/ls", "ls", "-a", "-l", NULL);
//execlp("ls", "ls", "-a", "-l", NULL);
char* const myargv[] = {
    "ls",
    "-a",
    "-l",
    NULL
};
//execv("/usr/bin/ls", myargv);
execvp("ls", myargv);

execvp:以字符指针的传参方式,并且默认会在环境变量PATH中进行搜索。


思考:进行程序替换成ls可执行程序的时候,我们传了myargv参数,那么是不是会把myargv作为命令行参数传给ls程序中的main函数呢。
下面我们可以进行验证,程序替换并不是只能指令,也可以替换成我们写的可执行程序。
新建otherExe.cc,我们以C++程序为例,演示C程序替换C++程序运行的效果。我们在otherExe.cc中只执行简单的打印语句。
C++的源文件可以以.cxx、.cpp、.cc结尾。
下面介绍如何使用make/makefile形成多个可执行文件:
在这里插入图片描述
默认执行make只会编译第一个依赖关系的依赖方法,所以需要添加伪目标修饰all,all的依赖关系就是对应的两个可执行程序,但是没有依赖方法。
在这里插入图片描述
然后我们在mycommand.c中程序替换成我们自己写的可执行程序otherExe:

execl("/home/zzy/test/otherExe", "otherExe", NULL);

在这里插入图片描述
下面我们使用execl传点命令行参数,然后我们在otherExe.cc中获取命令行参数并打印出来,并且我们知道子进程会继承父进程的环境变量,因此我们把环境变量也打出来看看:

// mycommand.c
execl("/home/zzy/test/otherExe", "otherExe", "-a", "-b", "-c",NULL);                                             
// otherExe.cc
#include <iostream>
using namespace std;
int main(int argc, char* argv[], char* env[])
{
    cout << "hello c++ linux" << endl;
    printf("这是命令行参数:\n");
    for (int i = 0; i < argc; i++)
    {
        cout << i << ":" << argv[i] << endl;
    }
    printf("这是环境变量:\n");
    for (int i = 0; env[i]; i++)
    {
        cout << i << ":" << env[i] << endl;
    }
    return 0;
}

在这里插入图片描述
可以看到,我们传给exec*函数的第二个参数会传给可执行程序的命令行参数,并且子进程继承了父进程的环境变量,因此环境变量具有全局属性。所以程序替换中,环境变量信息不会被替换。


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

这两个函数的e表示的就是环境变量,le表示的传参方式是以list,并且可以传递环境变量。vpe传参方式是以字符指针数组的形式,并且会在环境变量PATH路径中搜索,可以传递环境变量。

如果我想要给子进程传递环境变量,该如何传递?
1、子进程会自动继承父进程的环境变量,所以子进程使用main函数的第三个参数,或者导入第三方环境变量extern char** environ。

2、父进程新增环境变量,子进程继承后可以获取父进程新增的环境变量。

对于方式2,我们需要介绍一个函数:putenv
在这里插入图片描述
putenv的作用就是导入一个环境变量。
现在我们在父进程中使用putenv导入MY_VAL=1234,然后创建子进程,子进程经过程序替换执行otherExe,我们期望在otherExe通过第三方变量environ获取到父进程导入的环境变量。
在这里插入图片描述
在这里插入图片描述
运行后我们确实发现,子进程的环境变量中确实有MY_VAL=1234。


下面使用execle程序替换,并传递环境变量,我们看看子进程程序替换后打印的环境变量都有什么?这里的传参是追加还是覆盖?
在这里插入图片描述
在这里插入图片描述
我们发现,这里并不是追加,而是直接覆盖了。所以execle和execvpe是直接将myenv传给子进程,彻底替换掉子进程的环境变量。

3、使用execle/execvpe彻底替换。


最后,再来看个系统调用:execve
在这里插入图片描述
2号手册,参数fliename就是可执行程序路径,argv就是命令行参数,env就是环境变量。
而我们上面讲的那六个函数都是库函数,上面六个库函数都是调用了这个execve系统调用,所以他们是上下层调用和被调用关系。
所以exec*执行的是加载器的功能,将可执行程序的代码和数据替换到内存中。


5、实现myshell

shell/bash也是一个进程,执行指令的时候,本质是通过创建子进程来执行的。那么shell的环境变量从哪来的呢?用户家目录下的.bash_profile里面保存了导入环境变量的方式。
在这里插入图片描述
具体实现步骤:
1、获取命令行。
2、解析命令行。
3、创建子进程。
4、子进程进行程序替换。
5、父进程等待子进程退出。
对于一些内建命令直接由父进程完成。

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

#define LINE_SIZE 1024
#define ARGC_SIZE 32
#define LEFT "["
#define RIGHT "]"
#define LABLE "$"
#define DELIM " \t"

int lastcode = 0;
int quit = 0;
char commandline[LINE_SIZE];
char* argv[ARGC_SIZE];
char pwd[LINE_SIZE];
char hostname[LINE_SIZE];

// 环境变量
char myenv[LINE_SIZE];

char* GetHostName()
{
    //return getenv("HOSTNAME");
    gethostname(hostname, sizeof(hostname));
    return hostname;
}

char* GetUserName()
{
    return getenv("USER");
}

void GetPwd()
{
    getcwd(pwd, sizeof(pwd));
}

void SplitPwd()
{
    int j = 0;
    int len = strlen(pwd);
    for (int i = len - 1; i >= 0; i--)
    {
        if (pwd[i] == '/')
        {
            j = i;
            break;
        }
    }
    if (!j) {
        pwd[0] = '/';
        pwd[1] = '\0';
        return;
    }
    int l = 0;
    for (int i = j + 1; i < len; i++)
    {
        pwd[l++] = pwd[i];
    }
    pwd[l] = 0;
}

void Interact(char* cline, size_t size)
{
    GetPwd();
    SplitPwd();
    
    printf(LEFT"%s@%s %s"RIGHT""LABLE" ", GetUserName(), GetHostName(), pwd);
    char* s = fgets(cline, size, stdin);
    assert(s);
    (void)s;
    cline[strlen(cline)-1] = '\0';
}

int SplitString(char* _argv[], char* cline)
{
    int i = 0;
    _argv[i++]  = strtok(cline, DELIM);
    while (_argv[i++] = strtok(NULL, DELIM));
    return i - 1;
}

void NormalExcute(char* argv[])
{
    pid_t id = fork();  
    if (id < 0)
    {
        perror("fork");
        return;
    }
    else if (id == 0)
    {
        execvp(argv[0], argv);
        exit(0);
    }
    else
    {
        int status = 0;
        pid_t ret = waitpid(id, &status, 0);
        if (ret == id)
        {
            lastcode = WEXITSTATUS(status);
        }
    }
}

int BuildExcute(int _argc, char* _argv[])
{   
    if (_argc == 2 && strcmp(_argv[0], "cd") == 0)
    {
        chdir(_argv[1]);
        GetPwd();
        sprintf(getenv("PWD"), "%s", pwd);
        return 1;
    }
    else if(_argc == 2 && strcmp(_argv[0], "export") == 0)
    {
        strcpy(myenv, _argv[1]);
        putenv(myenv);
        return 1;
    } 
    else if (_argc == 2 && strcmp(_argv[0], "echo") == 0)
    {
        if (strcmp(_argv[1], "$?") == 0)
        {
            printf("%d\n", lastcode);
            lastcode = 0;
        }
        if (*_argv[1] == '$')
        {
            char* val = getenv(_argv[1] + 1);
            if (val) printf("%s\n", val);
        }
        else
        {
            printf("%s\n", _argv[1]);
        }
        return 1;
    }
    
    // 单独处理ls
    if (strcmp(_argv[0], "ls") == 0)
    {
        _argv[_argc++] = "--color";
        _argv[_argc] = NULL;
    }
    return 0;
}

int main()
{
    while (!quit)
    {
        Interact(commandline, sizeof(commandline)); 
        int argc = SplitString(argv, commandline);
        if (argc == 0) continue;
        
        int n = BuildExcute(argc, argv);
     
        if (!n)
            NormalExcute(argv);     
    }
    return 0;
}

演示效果:
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值