Linux进程控制

进程创建

回顾前面所讲的fork函数,在 Linux 中 fork 函数是非常重要的函数,它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。子进程中fork返回0,父进程中fork返回子进程的pid,出错返回-1。

#include <unistd.h>
pid_t fork();

进程调用 fork ,当控制转移到内核中的 fork 代码后,内核做:

  1. 分配新的内存块和内核数据结构给子进程。
  2. 将父进程部分数据结构内容拷贝至子进程。
  3. 添加子进程到系统进程列表当中。
  4. fork 返回,开始调度器调度。

在这里插入图片描述

当一个进程调用fork之后,就有两个二进制代码相同的进程。而且它们都运行到相同的地方。fork之前父进程独立执行,fork之后,父子两个执行流分别执行。注意,fork之后,谁先执行完全由调度器决定。

通常,父子代码共享,父子再不写入时,数据也是共享的。当任意一方试图写入,便以写时拷贝的方式各自一份副本。如下图所示,具体可看往期博客Linux进程。

在这里插入图片描述

写时拷贝(Copy-On-Write,COW),是一种延时申请技术。父进程调用fork()创建子进程后,子进程与父进程共享代码和数据,而不是在物理内存中直接开辟空间拷贝数据给子进程,这样就减少了创建子进程的时间减少了空间浪费,待任意进程要写入数据时再进行写时拷贝,提高了整机内存使用率

fork常规用法:

  1. 一个父进程希望复制自己,使父子进程同时执行不同的代码段。例如,父进程等待客户端请求,生成子进程来处理请求。
  2. 一个进程要执行一个不同的程序。例如子进程从fork返回后,调用exec函数。

fork调用失败的原因:

  1. 系统中有太多进程。
  2. 实际用户的进程数超过了限制。

进程终止

进程终止的本质是释放系统资源,即释放进程申请的相关内核数据结构和对应的数据和代码。

进程退出场景:

  1. 代码运行完毕,结果正确。
  2. 代码运行完毕,结果不正确。
  3. 代码异常终止。

main函数的返回值,通常表明程序的执行情况。代码运行完毕,结果正确,返回0;代码运行完毕,结果不正确,返回非0,如1,2,3……不同值表明不同的出错原因。

echo $? 可以查看最近进程的退出码。进程退出码会写到进程的task_struct中,以供父进程回收时读取。

在这里插入图片描述

由于 echo $? 查看的是最近进程的退出码,所以此时再执行一次该命令,查看的就是 echo $? 的退出码,即0。

在这里插入图片描述

库函数 strerror 可以根据错误码返回对应的错误描述。

在这里插入图片描述

使用如下代码,查看所有错误码对应的信息,不知道有多少错误码,先假定有200个。

在这里插入图片描述

运行代码后可以看到,一共有133个错误码。

fopen函数打开文件失败后不仅会返回空,还会把全局错误码变量 errno 设置为对应值以表明错误信息。

在这里插入图片描述

用命令打开不存在的文件,错误码也是相同的。

在这里插入图片描述

如果代码异常终止,错误码就会无意义。

在这里插入图片描述

exit 可以结束进程,在任何地方调用就在此结束。exit(n) 进程退出后返回退出码n。

在这里插入图片描述

进程在函数fun()中遇到exit结束,直接退出。

在这里插入图片描述

_exit 是系统调用,终止调用它的进程。

在这里插入图片描述

exit 是库函数,使用时会回收资源,刷新缓冲区,再调用系统级别的 _exit 。运行如下代码,可以看到main()并没有被打印出来。

在这里插入图片描述

在这里插入图片描述

return是一种更常见的退出进程方法。执行 return n 等同于执行 exit(n),因为调用 main 的运行时函数会将 main 的返回值当做 exit 的参数。

进程等待

子进程先于父进程退出,父进程还未对子进程回收,子进程就变成僵尸进程。进程变成僵尸进程后,命令 kill -9 也无法杀掉进程。父进程通过进程等待的方式,回收子进程资源获取子进程退出信息

wait(int* status) 是系统调用,成功返回被等待进程pid,失败返回-1。参数是输出型参数,获取子进程退出状态,不关心则可以设置成为NULL。

在这里插入图片描述

准备下面代码,父进程创建子进程后休眠15秒,子进程运行5秒后退出,成为僵尸进程。父进程休眠结束后调用wait,回收子进程。

在这里插入图片描述

运行代码,while :; do ps axj | head -1 && ps axj | grep myproc; sleep 1;done 观察进程。

在这里插入图片描述

等待子进程时,如果子进程还未退出,父进程就会阻塞在wait调用出,类似使用scanf时,键盘不输入就会一直阻塞在scanf处。如果有多个子进程,父进程会阻塞在wait处,等待任意一个子进程退出,一次只回收一个僵尸子进程。

pid_t waitpid(pid_t pid, int *status, int options) waitpid的返回值与wait一样,如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在,一般是没有子进程,或pid错误没有对应子进程。参数options用于阻塞控制,默认为0,表示阻塞等待。参数pid为要等待的子进程的pid,pid为-1则等待所有子进程,此时就等价于wait。

wait和waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充。如果传递NULL,表示不关心子进程的退出状态信息,否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程。

status不能简单地当作整型来看待,应该当作位图来看待。status的32个bit位中,高16位不使用,全为0。低16位中,8 ~ 15位记录退出状态,即退出码;第7位是core dump标志,代码异常退出时保存对应信号编号,为0时正常,为1时异常,kill -l 查看所有信号,这些信号其实是宏;0 ~ 6位是终止信号,正常退出时全为0,代码异常退出时非0,此时退出码也无意义。

在这里插入图片描述

退出码 exit code 可通过 (status >> 8) & 0xFF 求出,bash也是这样拿到退出码的。(0xFF即二进制11111111)

在这里插入图片描述

定义一个全局变量exitcode=0,子进程退出时把它修改为对应退出码,但这样会发生写时拷贝,父进程无法拿到exitcode,所以需要waitpid这样的系统调用。

查看status0 ~ 6位表示的退出信号 exit signal 通过 status & 0x7F 。(0x7F即二进制01111111)

在这里插入图片描述

可以看到exit signal为0代码正常退出,但exit code为10运行结果不正确。

把子进程的部分改为while(1),另开一个机器用 kill -9 杀掉子进程,可以看到退出信号对应为9。

在这里插入图片描述

子进程部分添加访问空指针和除0错误,代码异常退出,可以看到退出信号为11和8。

在这里插入图片描述

WIFEXITED(status) 返回真,子进程正常退出,返回假,子进程被信号杀死,异常退出。底层宏实现如下。

#define WIFEXITED(status)  ((status & 0x7F) == 0)

WEXITSTATUS(status)WIFEXITED(status) 为真,即status的0 ~ 6位全为0,进程正常退出时,才会返回退出码,否则返回的退出码无意义。

#define WEXITSTATUS(status)  (((status) >> 8) & 0xFF)

子进程部分有除0错误,用这两个宏后,得到退出信号为0,说明异常退出,此时退出码为0无意义。

在这里插入图片描述

所以,在获取退出码时,要先用宏 WIFEXITED(status) 判断是否为正常退出。

在这里插入图片描述

waitpid的参数options为0时,如果子进程还在运行,父进程就会卡在waitpid调用处,阻塞等待。当optinos为 WNOHANG 时,waitpid为非阻塞轮询模式,父进程调用waitpid后不再等待,如果返回的是0,则说明子进程还在运行。

用下面代码观察非阻塞轮询模式。

在这里插入图片描述

非阻塞轮询过程中,父进程也能执行别的任务。如下代码所示。

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

typedef void(*fun_t)();
#define N 5
fun_t handlers[N+1];

//任务
void DownLoad()
{
    printf("下载任务......\n");
}
void Flush()
{
    printf("刷新任务......\n");
}
void Log()
{
    printf("记录日志任务......\n");
}

void registerHandler(fun_t h[],fun_t f)
{
    int i=0;
    for(i=0;i<N;i++)
    {
        if(h[i]==NULL)
            break;
    }
    if(i==N)
        return;
    h[i]=f;
    h[i+1]=NULL;
}

int main()
{
    registerHandler(handlers,DownLoad);
    registerHandler(handlers,Flush);
    registerHandler(handlers,Log);

    pid_t id = fork();
    if(id == 0)
    {
        //子进程
        int cnt=3;
        while(cnt--)
        {
            printf("我是子进程,pid = %d  ppid = %d\n",getpid(),getppid());
            sleep(1);
        }
        exit(10);
    }

    //父进程
    while(1)
    {      
        int status = 0;
        pid_t rid = waitpid(id,&status,WNOHANG);
        if(rid>0)
        {
            if(WIFEXITED(status))
                printf("wait success, rid = %d\nstatus = %d  exit code = %d  exit signal = %d\n",rid,status,WEXITSTATUS(status),WIFEXITED(status));
            else
                printf("子进程退出异常\n");
            break;
        }
        else if(rid == 0)
        {
            printf("本轮调用结束,子进程没有退出。\n");
            for(int i=0;handlers[i];i++)
            {
                handlers[i]();
            }
            sleep(1);
        }
        else
        {
            printf("wait failed %d -> %s\n",errno,strerror(errno));
            break;
        }
    }
    
    return 0;
}

在这里插入图片描述

可以看到,子进程退出前,父进程不必阻塞等待,而是可以去执行一系列其他任务。

进程程序替换

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

在这里插入图片描述

代码演示。

在这里插入图片描述

执行到execl,发生程序替换,原始代码的后半部分就不存在了。所以第二个printf没有执行。

改成错误路径和指令,execl执行失败,两个printf正常执行。

在这里插入图片描述

man execl ,函数出现错误时返回-1并设置errno,成功时没有返回值,因为execl函数后面的代码已经被替换了此时返回值无意义

在这里插入图片描述

exec系列的函数不用做返回值判断,如果有返回值则失败,此时直接调用 exit(1)

在这里插入图片描述

man execl 查看exec系列函数。

在这里插入图片描述

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

函数名的 l 即 list 。参数path传路径+程序名。后面的可变参数列表按照命令行使用时传。execl的最后一个参数必须传入NULL。如命令行 ls -al ,调用execl函数 execl("/usr/bin/ls","ls","-al",NULL)

创建子进程,让子进程去执行另一个程序。

在这里插入图片描述

execl为什么没有影响父进程呢?进程具有独立性,数据和代码都发生了写时拷贝。

execl除了能把代码替换为命令,也能替换为我们自己写的程序。这些程序可以是C++,Java,Python等。

在这里插入图片描述

myproc.c子进程内调用getpid()打印出进程 PID 为 3122;执行execl完成程序替换后,code.cc中再次调用getpid(),输出的 PID 仍为 3122,两次 PID 完全一致。说明调用exec系列函数完成程序替换时,不会生成新进程,进程 PID 自始至终保持不变。

在这里插入图片描述

int execlp(const char *file, const char *arg, ...);

函数名中的 p 即 PATH。第一个参数只需要传入要执行的文件名即可,execlp会自动在环境变量PATH中查找。

在这里插入图片描述

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

函数名中的 v 即 vector 。第一个参数path传路径和文件名,可变参数列表改为传字符指针数组,数组的最后一个元素必须为NULL。

在这里插入图片描述

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

函数名中的 v 和 p 同理。参数file只需要传文件名。argv[]为命令行参数表。

在这里插入图片描述

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

函数名中的 v、p同理,e指环境变量。参数file、argv[]同理,envp[]为环境变量表,最后一个元素必须为NULL。

在这里插入图片描述

子进程调用 execvpe 加载运行 code,传入自定义的命令行参数表和环境变量表,code 启动后它的命令行参数、环境变量就被传入的这两张表完整替换。

子进程调用 execvp ,code的命令行参数表也会被替换,但环境变量表不会被替换,如果要替换,则需要修改全局指针environ = env.

在这里插入图片描述

可以看到直接调用 execvpe 会覆盖环境变量表,使用 putenv 能增量式地在环境变量表中添加环境变量。

在这里插入图片描述

创建全局数组env,输入要加入的环境变量,调用getenv把env中的环境变量加入到环境变量表中,最后把全局指针environ传入execvpe,子进程执行code时就能看到这些新加入的环境变量。

在这里插入图片描述

父进程调用 fork() 创建子进程,刚 fork 完成时,子进程共享父进程的内存,包含父进程的命令行参数、环境变量表。

子进程中通过 putenv() 追加自定义环境变量,修改环境变量表时触发写时拷贝,只会复制生成一份子进程独有的环境变量表,父进程的原始环境变量表完全不会被改动

进程全局指针 environ 指向进程自身的环境变量表,子进程修改后,environ 就指向子进程这份新增了自定义环境变量的环境变量表。

子进程调用 execvpe("./code", argv, environ),将当前进程完整环境变量表传给新程序;执行 execvpe 后子进程镜像被 code 替换codemain 就能读取到原有系统环境 + 我们追加的 MYVAL/MYVAL1/MYVAL2 全部环境变量。

我们程序本身,即main,也是父进程 bash 通过 fork+exec 传递命令行参数与环境变量启动的。

exec系列的6个函数是语言层面的函数,底层调用系统级别的execve实现。

在这里插入图片描述

自主Shell命令行解释器

shell执行命令的流程是这样的。shell收到用户输入的字符串ls,创建子进程,子进程运行命令ls,父进程阻塞等待,直到子进程退出。之后shell恢复运行,回到初始状态,等待读取新命令。(bash是shell的一种)

在这里插入图片描述

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

  1. 获取命令行。
  2. 解析命令行。
  3. 创建一个子进程。(fork)
  4. 替换子进程。(正常使用命令行解释器时不带路径,故选用execvp)
  5. 父进程等待子进程退出。(wait)

创建文件 myshell.cc ,实现我们自己的shell。把myshell当作bash,自然myshell要有自己的一套命令行参数和环境变量。

#define COMMAND_SIZE 1024

//命令行参数
#define ARGV_SIZE 128
char* g_argv[ARGV_SIZE];
int g_argc=0;

//环境变量
#define ENV_SIZE 128
char* g_env[ENV_SIZE];
int g_envs=0;

XShell中显示的命令行由命令行提示符和命令组成。

命令行参数从输入的命令中解析获得。环境变量初始化时,先读取父 bash 传递给 myshell 的系统原生环境表environ,逐个 malloc 复制所有环境字符串,存入自定义全局指针数组g_env

fork 产生子进程时父子进程环境内存相互隔离(写时拷贝仅为 fork 阶段内存优化),我们手动拷贝环境到g_env脱离原生只读内存,再调用putenv注册自定义环境字符串,最后将系统全局环境指针environ指向g_env,让 myshell 拥有一套完全自主可控、独立于父进程 bash 的环境变量表。

void InitEnv()
{
    extern char** environ;
    memset(g_env,0,sizeof(g_env));
    g_envs=0;

    for(int i=0;environ[i];i++)
    {
        g_env[i] = (char*)malloc(strlen(environ[i])+1); //
        strcpy(g_env[i],environ[i]);
        g_envs++;
    }
    g_env[g_envs]=NULL;

    for(int i=0;i<g_envs;i++)
        putenv(g_env[i]);
    environ=g_env;
}

命令行提示符由用户名、主机名、所处目录组成。这些分别可以通过调用 getenv ,从环境变量USER、PWD、HOME、HOSTNAME中获得。但是当前工作目录不能直接通过 getenv("PWD") 获取,如果后续使用命令 cd 更改了工作目录,环境变量PWD不会自动更新,getcwd() 直接和内核交互,读取进程在内核中保存的真实工作目录。

在这里插入图片描述

const char* GetUser()
{
    const char* user = getenv("USER");
    return user == NULL?"None":user;
}

const char* GetHome()
{
    const char* home = getenv("HOME");
    return home == NULL?"None":home;
}
const char* GetHostName()
{
    const char* host = getenv("HOSTNAME");
    return host == NULL?"None":host;
}

char cwd[1024];    //当前工作路径
char cwd_env[1024];//转成环境变量形式
const char* GetPwd()
{
    memset(cwd,0,sizeof(cwd));
    const char* pwd = getcwd(cwd,sizeof(cwd));

    if(pwd!=NULL)
    {
        if(strcmp(pwd,GetHome())==0)
            return pwd;
        snprintf(cwd_env,sizeof(cwd_env),"PWD=%s",cwd);
        putenv(cwd_env);
        return pwd;
    }

    return "None";
}

生成命令行提示符。

const char* DirName(const char* pwd)//从工作路径中截取出所在目录
{
    if(pwd==NULL)
        return "None";
    if(strcmp(pwd,"/")==0 || strcmp(pwd,"~")==0)
        return pwd;

    int i=strlen(pwd)-1;
    while(pwd[i]!='/')
        i--;

    return &pwd[++i];
}

void MakeCommandLinePrompt(char* prompt,int size)
{
    snprintf(prompt,size,FORMAT,GetUser(),GetHostName(),DirName(GetPwd()));
}
void PrintCommandLinePrompt()
{
    char prompt[COMMAND_SIZE];
    memset(prompt,0,sizeof(prompt));
    MakeCommandLinePrompt(prompt,sizeof(prompt));
    printf("%s",prompt);
    fflush(stdout);
}

接下来获取命令并解析命令。在XShell输入命令后会按回车,所以要把获取的命令去掉末尾的 \n 。命令带有空格,所以用fgets从键盘获取命令。解析命令如 ls -al ,根据空格把命令分割成子字符串,放入g_argv[]中。函数strtok用于切分字符串。

//获取命令
bool GetCommand(char* command,int size)
{
    char* s = fgets(command,size,stdin);
    if(s==NULL)
        return false;
    int len = strlen(command);
    command[len-1]='\0';
    if(strlen(command)==0)
        return false;
    return true;
}

//解析命令 ls -al
#define DELIM " "
bool CommandParse(char* command)
{
    g_argc=0;
    g_argv[g_argc++]=strtok(command,DELIM);
    while((bool)(g_argv[g_argc] = strtok(NULL,DELIM)))
        g_argc++;
    
    //printf("%d\n",g_argc);

    return g_argc>0?true:false;
}

封装 Execute() 函数,在该函数中创建子进程,让子进程去执行我们输入的命令。

int Execute()
{
    pid_t id = fork();
    if(id<0)
        exit(1);
    else if(id==0)
    {
        //printf("子进程Execute()\n");
        execvp(g_argv[0],g_argv);
        exit(1);
    }
    int status=0;
    pid_t rid=waitpid(id,&status,0);
    if(rid>0)
    {
        last_exit_code = WEXITSTATUS(status);        
    }
    return 0;
}

在XShell中,我们输入命令,命令执行完毕后又会显示出一行命令行提示符,这本质就是死循环。所以main函数如下。

int main()
{
    //初始化环境变量表
    InitEnv();
    //PrintG_env();
    while(1)
    {
        //输出命令行提示符
        PrintCommandLinePrompt();

        //获取命令
        char command[COMMAND_SIZE];
        if(!GetCommand(command,sizeof(command)))
            continue;

        //命令解析
        if(!CommandParse(command))
            continue;
        //PrintG_argv();
        //内建命令与别名
        if(CheckAndExecBuilt()) //内建命令返回true,不创建子进程
            continue;

        //执行命令
        Execute();

    }

    return 0;
}

运行结果如下,能正常使用外部命令。

在这里插入图片描述

cdechoexport 等内建命令就不能通过在子进程中调用execvp来使用。所以需要另外实现。

cd 改变当前所处路径,需要调用 chdir() 修改进程内核层面的当前工作目录。XShell中 cdcd ~ 跳转到家目录,cd - 跳转到上一次所处目录,cd .. 跳转到上级目录。

echo 目前只考虑打印字符串 echo "..." 、打印退出码 echo $? 、打印环境变量 echo $env

对于 export ,我们输入命令 export X=999 ,只需把g_argv[1]拷贝到g_env[g_envs]中,再调用putenv注册环境变量即可。

char oldpwd[1024];
void Cd()
{
    const char* pwd=GetPwd();
    //printf("pwd: %s\n",pwd);

    if(g_argc==1)
    {
        const char* home=GetHome();
        if(strcmp(home,"None")==0)
            exit(1);
        chdir(home);
    }
    else
    {
        if(strcmp(g_argv[1],"~")==0)
        {
            const char* home=GetHome();
            if(strcmp(home,"None")==0)
                exit(1);
            chdir(home);
        }
        else if(strcmp(g_argv[1],"-")==0)
        {
            if(strlen(oldpwd)==0)
                return;
            printf("oldpwd: %s\n",oldpwd);
            chdir(oldpwd);        
        }
        else if(strcmp(g_argv[1],"..")==0)
        {
            int len=strlen(pwd);
            int i=len-1;
            while(pwd[i]!='/')
                i--;
            char dest[1024];
            memset(dest,0,sizeof(dest));
            strncpy(dest,pwd,i+1);
            chdir(dest);
        }
        else
        {
            chdir(g_argv[1]);
        }
    }
    strcpy(oldpwd,pwd);
}
void Echo()
{
    //echo "..."
    //echo $?
    //echo $env
    char* s = g_argv[1];
    if(strcmp(s,"$?")==0)
    {
        std::cout<<last_exit_code<<std::endl;
        last_exit_code=0;
    }
    else if(s[0]=='$')
    {
        char* env_val=getenv(&s[1]);
        std::cout<<env_val<<std::endl;
    }
    else
    {
        printf("%s ",&g_argv[1][1]);
        for(int i=2;i<g_argc-1;i++)
            printf("%s ",g_argv[i]);
        g_argv[g_argc-1][strlen(g_argv[g_argc-1])-1]='\0';
        printf("%s\n",g_argv[g_argc-1]);
    }
}
bool CheckAndExecBuilt()
{
    char* cmd=g_argv[0];
    if(strcmp(cmd,"cd")==0)
    {
        Cd();
        return true;
    }
    else if(strcmp(cmd,"echo")==0)
    {
        Echo();
        return true;
    }
    else if(strcmp(cmd,"export")==0)
    {
        g_env[g_envs]=(char*)malloc(strlen(g_argv[1])+1);
        strcpy(g_env[g_envs],g_argv[1]);
        g_envs++;
        g_env[g_envs]=NULL;
        putenv(g_env[g_envs-1]);
        return true;
    }

    return false;
}

总的代码如下。

#Makefile
myshell:myshell.cc
	g++ $^ -o $@ -std=c++11

.PHONY:clean
clean:
	rm -f myshell
#include <iostream>
#include <cstdio>
#include <cstring>
#include <cstdlib>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

#define COMMAND_SIZE 1024
#define FORMAT "[%s@%s %s]# "

//命令行参数
#define ARGV_SIZE 128
char* g_argv[ARGV_SIZE];
int g_argc=0;

//环境变量
#define ENV_SIZE 128
char* g_env[ENV_SIZE];
int g_envs=0;

int last_exit_code=0;//最近退出码

const char* GetUser()
{
    const char* user = getenv("USER");
    return user == NULL?"None":user;
}

const char* GetHome()
{
    const char* home = getenv("HOME");
    return home == NULL?"None":home;
}
const char* GetHostName()
{
    const char* host = getenv("HOSTNAME");
    return host == NULL?"None":host;
}

char cwd[1024];    //当前工作路径
char cwd_env[1024];//转成环境变量形式
const char* GetPwd()
{
    memset(cwd,0,sizeof(cwd));
    const char* pwd = getcwd(cwd,sizeof(cwd));

    if(pwd!=NULL)
    {
        if(strcmp(pwd,GetHome())==0)
            return pwd;
        snprintf(cwd_env,sizeof(cwd_env),"PWD=%s",cwd);
        putenv(cwd_env);
        return pwd;
    }

    return "None";
}

const char* DirName(const char* pwd)
{
    if(pwd==NULL)
        return "None";
    if(strcmp(pwd,"/")==0 || strcmp(pwd,"~")==0)
        return pwd;

    int i=strlen(pwd)-1;
    while(pwd[i]!='/')
        i--;

    return &pwd[++i];
}

void MakeCommandLinePrompt(char* prompt,int size)
{
    snprintf(prompt,size,FORMAT,GetUser(),GetHostName(),DirName(GetPwd()));
}
void PrintCommandLinePrompt()
{
    char prompt[COMMAND_SIZE];
    memset(prompt,0,sizeof(prompt));
    MakeCommandLinePrompt(prompt,sizeof(prompt));
    printf("%s",prompt);
    fflush(stdout);
}

//获取命令
bool GetCommand(char* command,int size)
{
    char* s = fgets(command,size,stdin);
    if(s==NULL)
        return false;
    int len = strlen(command);
    command[len-1]='\0';
    if(strlen(command)==0)
        return false;
    return true;
}

//解析命令 ls -al
#define DELIM " "
bool CommandParse(char* command)
{
    g_argc=0;
    g_argv[g_argc++]=strtok(command,DELIM);
    while((bool)(g_argv[g_argc] = strtok(NULL,DELIM)))
        g_argc++;
    
    //printf("%d\n",g_argc);

    return g_argc>0?true:false;
}

char oldpwd[1024];
void Cd()
{
    const char* pwd=GetPwd();
    //printf("pwd: %s\n",pwd);

    if(g_argc==1)
    {
        const char* home=GetHome();
        if(strcmp(home,"None")==0)
            exit(1);
        chdir(home);
    }
    else
    {
        if(strcmp(g_argv[1],"~")==0)
        {
            const char* home=GetHome();
            if(strcmp(home,"None")==0)
                exit(1);
            chdir(home);
        }
        else if(strcmp(g_argv[1],"-")==0)
        {
            if(strlen(oldpwd)==0)
                return;
            printf("oldpwd: %s\n",oldpwd);
            chdir(oldpwd);        
        }
        else if(strcmp(g_argv[1],"..")==0)
        {
            int len=strlen(pwd);
            int i=len-1;
            while(pwd[i]!='/')
                i--;
            char dest[1024];
            memset(dest,0,sizeof(dest));
            strncpy(dest,pwd,i+1);
            chdir(dest);
        }
        else
        {
            chdir(g_argv[1]);
        }
    }
    strcpy(oldpwd,pwd);
}
void Echo()
{
    //echo "..."
    //echo $?
    //echo $env
    char* s = g_argv[1];
    if(strcmp(s,"$?")==0)
    {
        std::cout<<last_exit_code<<std::endl;
        last_exit_code=0;
    }
    else if(s[0]=='$')
    {
        char* env_val=getenv(&s[1]);
        std::cout<<env_val<<std::endl;
    }
    else
    {
        printf("%s ",&g_argv[1][1]);
        for(int i=2;i<g_argc-1;i++)
            printf("%s ",g_argv[i]);
        g_argv[g_argc-1][strlen(g_argv[g_argc-1])-1]='\0';
        printf("%s\n",g_argv[g_argc-1]);
    }
}
bool CheckAndExecBuilt()
{
    char* cmd=g_argv[0];
    if(strcmp(cmd,"cd")==0)
    {
        Cd();
        return true;
    }
    else if(strcmp(cmd,"echo")==0)
    {
        Echo();
        return true;
    }
    else if(strcmp(cmd,"export")==0)
    {
        g_env[g_envs]=(char*)malloc(strlen(g_argv[1])+1);
        strcpy(g_env[g_envs],g_argv[1]);
        g_envs++;
        g_env[g_envs]=NULL;
        putenv(g_env[g_envs-1]);
        return true;
    }

    return false;
}


void InitEnv()
{
    extern char** environ;
    memset(g_env,0,sizeof(g_env));
    g_envs=0;

    for(int i=0;environ[i];i++)
    {
        g_env[i] = (char*)malloc(strlen(environ[i])+1); //
        strcpy(g_env[i],environ[i]);
        g_envs++;
    }
    g_env[g_envs]=NULL;

    for(int i=0;i<g_envs;i++)
        putenv(g_env[i]);
    environ=g_env;
}

int Execute()
{
    pid_t id = fork();
    if(id<0)
        exit(1);
    else if(id==0)
    {
        //printf("子进程Execute()\n");
        execvp(g_argv[0],g_argv);
        exit(1);
    }
    int status=0;
    pid_t rid=waitpid(id,&status,0);
    if(rid>0)
    {
        last_exit_code = WEXITSTATUS(status);        
    }
    return 0;
}

void PrintG_env()
{
    std::cout<<"g_envs = "<<g_envs<<std::endl;
    for(int i=0;g_env[i];i++)
        printf("g_env[%d]: %s\n",i,g_env[i]);
    printf("\n");
}
void PrintG_argv()
{
    std::cout<<"g_argc = "<<g_argc<<std::endl;
    for(int i=0;g_argv[i];i++)
        printf("g_argv[%d]: %s\n",i,g_argv[i]);
    printf("\n");
}

int main()
{
    //初始化环境变量表
    InitEnv();
    //PrintG_env();
    while(1)
    {
        //输出命令行提示符
        PrintCommandLinePrompt();

        //获取命令
        char command[COMMAND_SIZE];
        if(!GetCommand(command,sizeof(command)))
            continue;

        //命令解析
        if(!CommandParse(command))
            continue;
        //PrintG_argv();
        //内建命令与别名
        if(CheckAndExecBuilt()) //内建命令返回true,不创建子进程
            continue;

        //执行命令
        Execute();

    }

    return 0;
}

在这里插入图片描述

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐