Linux--进程控制

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6

前言

        这篇文章主要是讲解Linux下的进程控制我们会学习到进程等待进程程序替换, 微型shell重新认识shell运行原理 。最后也编写了一个属于我们自己的shell尽管功能不够齐全但是还是感觉挺有意思挺好玩的。这里也希望大家学的开心

目录

前言

进程创建

重温fork函数

写时拷贝

fork调用失败的原因

进程终止

进程退出场景

进程常见退出方法

进程等待

进程等待必要性

进程等待的方法

阻塞与非阻塞

进程程序替换

替换原理

替换函数

我的shell

第一步获取命令行

第二步解析命令行

第三步建立forkexecvp替换子进程

第四步父进程等待子进程退出wait

代码总括


进程创建


重温fork函数

在前面进程概念的时候就已经用到了fork函数。所以我们知道在linux中fork函数时非常重要的函数它从已存在进程中创建一个新进程。新进程为子进程而原进程为父进程。我们通过查man手册也知道了运用fork函数需要包含头文件 <unistd.h>。

#include <unistd.h> 

pid_t fork(void);

返回值自进程中返回0父进程返回子进程id出错返回-1

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

1.分配新的内存块和内核数据结构给子进程

2.将父进程部分数据结构内容拷贝至子进程

3.添加子进程到系统进程列表当中

4.fork返回开始调度器调度

过程图

当一个进程调用fork之后就有两个二进制代码相同的进程。而且它们都运行到相同的地方。但每个进程都将可以 开始它们自己的旅程看如下程序。

#include <stdio.h>    
#include <unistd.h>    
#include <stdlib.h>    
    
int main(void)    
{    
  pid_t pid;    
  printf("Before: pid is %d\n", getpid());    
  if ((pid=fork())==-1)perror("fork()"),exit(1);                                                                                                                                           
  printf("After:pid is %d, fork return %d\n", getpid(), pid);    
 sleep(1);    
 return 0;    
}    

运行结果

[qhx@VM-8-2-centos 12-14]$ ./test
Before: pid is 1853
After:pid is 1853, fork return 1854
After:pid is 1854, fork return 0

这里看到了三行输出一行before两行after。进程1853先打印before消息然后它有打印after。另一个after消息有1854打印的。注意到进程1854没有打印before为什么呢如下图所示

所以这段代码想让我们知道fork之前父进程独立执行fork之后父子两个执行流分别执行。注意fork之后谁先执行完全由调度器决定。

调度器

调度器是CPU中央处理器的管理员主要负责完成做两件事情

1.选择某些就绪进程来执行

2.是打断某些执行的进程让它们变为就绪状态。

fork函数返回值

子进程返回0 父进程返回的是子进程的pid。

fork常规用法

一个父进程希望复制自己使父子进程同时执行不同的代码段。例如父进程等待客户端请求生成子 进程来处理请求。

一个进程要执行一个不同的程序。例如子进程从fork返回后调用exec函数。

写时拷贝

当父子代码只读时父子的代码和数据是共享的。但是任意一方试图写入时便以写时拷贝的方式各自一份副本。具体见下图:

我们发现最开始父子进程都是指向的同一物理内存但是当发生写入时。我们发现通过写时拷贝子进程重新指向一块物理内存。

fork调用失败的原因

关于fork调用失败的两个原因

1.系统中有太多的进程

2.实际用户的进程数超过了限制

这段代码是测试你的用户能跑好多个进程但是不建议跑。因为跑了之后就会影响bash会导致系统出错代码如下

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

int main()
{
    int cnt = 0;
    while(1)
    {
        int ret = fork();
        if(ret < 0){
            printf("fork error!, cnt: %d\n", cnt);
            break;
        }
        else if(ret == 0){
            //child
            while(1) sleep(1);
        }
        //partent
        cnt++;
    }
    return 0;
}

当运行后就会出现如下错误

-bash: fork: retry: No child processes
-bash: fork: retry: No child processes
-bash: fork: retry: No child processes
-bash: fork: retry: No child processes
-bash: fork: Resource temporarily unavailable
-bash-4.2$ 

解决方法

1.kill -9 -1 //将进程全部杀死

2.重新增加一个用户使用

进程终止


进程退出场景

代码运行完毕结果正确 --return 0

代码运行完毕结果不正确 --return 0退出码

代码异常终止 --退出码无意义

进程常见退出方法

正常终止可以通过 echo $? 查看进程退出码

1. 从main返回--return 返回

2. 调用exit --任意地方调用

3. _exit

异常退出

ctrl + c信号终止

return退出

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

我们通过return返回0为正确返回其他为错误代码如下

#include <stdio.h>    
    
int addToTarget(int from, int to)    
{    
    
    int sum = 0;    
    int i=0;    
    for(i = from; i < to; i++)    
    {    
         sum += i;    
    }    
    return sum;    
}    
    
                                                                                                                                                                                           
int main()    
{    
    //进程退出的时候对应的退出码    
    //标定进程执行结果是否正确    
    
    int num = addToTarget(1, 100);    
    if(num == 5050)    
        return 0;    
    else    
        return 1;    
    
    return 0;    
}

运行结果

[hongxin@VM-8-2-centos 12-14]$ ./mytest 
[hongxin@VM-8-2-centos 12-14]$ echo $?
1
[hongxin@VM-8-2-centos 12-14]$ echo $?
0

代码解释

./mytest运行一个进程

echo显示

$?永远记录最近的一个在命令行中执行完毕时对应的退出码main->return ?:

这里的1标识错误是mytest进程中代码不正确

这里的0标识正确因为echo $?本来都是一个进程

前面也说过退出码0标识成功其他表示不正确但是不同的数字可以描述不同错误对于计算机来说很好识别数字但对于程序员来说语言描述是更加友好。所以在学习c语言的时候我们就学习过strerror就是用字符串进行描述它大概有134种标识

运行代码

 for(int i=0;i<200;i++)
   {
         printf("%d:%s\n",i,strerror(i) );
    }

运行结果

[hongxin@VM-8-2-centos 12-14]$ ./mytest 
0:Success

1:Operation not permitted
2:No such file or directory
3:No such process
4:Interrupted system call
5:Input/output error
6:No such device or address

....................................

127:Key has expired
128:Key has been revoked
129:Key was rejected by service
130:Owner died
131:State not recoverable
132:Operation not possible due to RF-kill
133:Memory page has hardware error
134:Unknown error 134

exit函数

#include <unistd.h> 

void exit(int status);

参数status 定义了进程的终止状态父进程通过wait来获取该值

说明虽然status是int但是仅有低8位可以被父进程所用。所以_exit(-1)时在终端执行$?发现返回值 是255。

 当exit(-1)时结果如下

运行结果

[hongxin@VM-8-2-centos 12-14]$ ./mytest 
hello bit![hongxin@VM-8-2-centos 12-14]$ echo $?
255

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

int addToTarget(int from, int to)
{    
    
    int sum = 0;    
    int i=0;    
    for(i = from; i < to; i++)    
    {    
         sum += i;    
    }    
//    return sum;    
    exit(12);                                                                                                                                                                              
}    
    
    
int main()    
{    
    
     printf("hello world!\n");    
     int ret = addToTarget(0, 100);    
     printf("sum=%d\n", ret);    
    
    while(1) sleep(1);    
}

[hongxin@VM-8-2-centos 12-14]$ ./mytest  
hello world!                                   //exit在调用addToTarget函数时直接退出

[hongxin@VM-8-2-centos 12-14]$ echo $?
12

_exit函数与exit函数

使用方法不变exit是库函数_exit系统调用。他们本质就是上下层关系。

实例

int main()

{

printf("hello");

exit(0);

}

运行结果:

[root@localhost linux]# ./a.out

hello[root@localhost linux]#

//前面两秒没有数据后两秒显示数据

------------------------------------------------------------------------------------------------------

int main()

{

printf("hello");

_exit(0);

}

运行结果:

[root@localhost linux]# ./a.out

[root@localhost linux]#

//前两秒没有数据两秒后程序直接退出

结论exit 终止进程主动刷新缓冲区_exit终止进程不会刷新缓冲区

那么缓存区再哪儿呢

我们发现如果缓冲区在操作系统层不管是exit还是_eixt都会刷新缓冲区。实则是在用户级的缓存区后面基础I/O会将。

exit最后也会调用exit, 但在调用exit之前还做了其他工作

1. 执行用户通过 atexit或on_exit定义的清理函数。

2. 关闭所有打开的流所有的缓存数据均被写入

3. 调用_exit

进程等待


进程等待必要性

之前讲过子进程退出父进程如果不管不顾就可能造成‘僵尸进程’的问题进而造成内存泄漏。

另外进程一旦变成僵尸状态那就刀枪不入“杀人不眨眼”的kill -9 也无能为力因为谁也没有办法 杀死一个已经死去的进程。

最后父进程派给子进程的任务完成的如何我们需要知道。如子进程运行完成结果对还是不对 或者是否正常退出。

父进程通过进程等待的方式回收子进程资源获取子进程退出信息

进程等待的方法

wait方法

#include <sys/types.h>

#include <sys/wait.h>

pid_t wait(int*status);

返回值

成功返回被等待进程pid失败返回-1。

参数

输出型参数获取子进程退出状态,不关心则可以设置成为NULL

通过fork创建进程 每次打印时睡眠1秒观察这5秒的状态。等子进程结束后睡眠10秒观察该进程的状态最后wiat等待后父进程接受到子进程后的状态。

脚本

while :; do  ps axj | head -1 && ps axj | grep mytest| grep -v grep ;sleep 1; done

#include <stdio.h>    
#include <unistd.h>    
#include <string.h>    
#include <stdlib.h>    
#include <sys/types.h>    
#include <sys/wait.h>    
    
int main()    
{    
    pid_t id = fork();    
    if(id == 0)    
    {    
        //子进程    
        int cnt = 5;    
        while(cnt)    
        {    
            printf("我是子进程: %d, 父进程: %d, cnt: %d\n", getpid(), getppid(), cnt--);            sleep(1);
        }                
        exit(0); //进程退出
    }                          
    sleep(10);                                                                             

    pid_t ret = wait(NULL);
    if(id > 0)                 
    {             
        printf("wait success: %d", ret);
    }                                       
         
    sleep(5);
} 

 通过观察我们发现最开始为S状态--等待状态子进程结束后未被父进程接受变成Z状态--僵尸状态最后通过等待后返回子进程信息子进程结束父进程运行。

waitpid方法

pid_ t waitpid(pid_t pid, int *status, int options);

返回值

当正常返回的时候waitpid返回收集到的子进程的进程ID

如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0

如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在

参数

        pid

                Pid=-1,等待任一个子进程。与wait等效。

                Pid>0.等待其进程ID与pid相等的子进程。

        status:

                WIFEXITED(status): 若为正常终止子进程返回的状态则为真。查看进程是否是正常退出

                WEXITSTATUS(status): 若WIFEXITED非零提取子进程退出码。查看进程的退出码

        options:

                 WNOHANG: 若pid指定的子进程没有结束则waitpid()函数返回0不予以等待。若正常结束则返回该子进程的ID。

获取子进程status

wait和waitpid都有一个status参数该参数是一个输出型参数由操作系统填充。

如果传递NULL表示不关心子进程的退出状态信息。 否则操作系统会根据该参数将子进程的退出信息反馈给父进程。

status不能简单的当作整形来看待可以当作位图来看待具体细节如下图只研究status低16比特位

 代码测试

int main()    
{    
    pid_t id = fork();    
    if(id == 0)    
    {    
        //子进程    
        int cnt = 5;    
        while(cnt)    
        {    
            printf("我是子进程: %d, 父进程: %d, cnt: %d\n", getpid(), getppid(), cnt--);    
           int *p = NULL;    
            *p = 100;    
            sleep(1);    
        }    
        // 运行完    
        // 1. 代码完结果对    
        // 2. 代码完结果不对    
        // 异常    
        // 3. 代码没跑完出异常了    
        exit(12); //进程退出    
       //`` exit(0); //进程退出    
    }       
                                                                                                                                                                                           
    int status = 0; // 不是被整体使用的有自己的位图结构    
    pid_t ret = waitpid(id, &status, 0);    
    if(id > 0)                  
    {                           
        printf("wait success: %d, sig number: %d, child exit code: %d\n", ret, (status & 0x7F), (status>>8)&0xFF);    
    }                           
                                
    sleep(5);                   
} 

运行结果

 [hongxin@VM-8-2-centos 12-15]$ ./mytest 
我是子进程: 6347, 父进程: 6346, cnt: 5
wait success: 6347, sig number: 11, child exit code: 0 

通过kill手册可以明确地得到是野指针的问题

阻塞与非阻塞

如果子进程已经退出调用wait/waitpid时wait/waitpid会立即返回并且释放资源获得子进程退出信息。

如果在任意时刻调用wait/waitpid子进程存在且正常运行则进程可能阻塞。

如果不存在该子进程则立即出错返回。

 用更直白的话就是在子进程运行的时候父进程一直处于等待状态这个时候父进程一直检测子进程状态这个时候父进程没有干其他事情这个过程就叫做---阻塞。

相反的如果在子进程运行的时候父进程一直处于等待状态这个时候父进程一直检测子进程状态如果没有就绪那么就直接返回这个时候父进程能干一其他的事这个过程就叫---非阻塞。每一次都是一次非阻塞等待多次非阻塞等待就叫--轮询

那么非阻塞有什么好处呢如果当父进程检查到子进程未就绪那么父进程就可以做一些其他的任务。

非阻塞的好处就是不会占用父进程的所有精力可以在轮询期间做其他任务。

非阻塞

代码测试

#include <assert.h>

#define NUM 10

typedef void (*func_t)(); //函数指针

func_t handlerTask[NUM];

//样例任务
void task1()
{
    printf("handler task1\n");
}
void task2()
{
    printf("handler task1\n");
}
void task3()
{
    printf("handler task1\n");
}

void loadTask()
{
    memset(handlerTask, 0, sizeof(handlerTask));
    handlerTask[0] = task1;
    handlerTask[1] = task1;                                                                                                                                                                
    handlerTask[2] = task1;
}

void addtask()
{}
int main()
{
    pid_t id = fork();
    assert(id != -1);
    if(id == 0)
    {                                                                                                                                                                                      
        //child
        int cnt = 10;
        while(cnt)
        {
            printf("child running, pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt--);
            sleep(1);
        //    int *p = 0;
        //    *p = 100; //野指针问题
        }

        exit(10);
    }

    loadTask();
    // parent
    int status = 0;
    while(1)
    {
        pid_t ret = waitpid(id, &status, WNOHANG); //WNOHANG: 非阻塞-> 子进程没有退出, 父进程检测时候立即返回
        if(ret == 0)
        {
            // waitpid调用成功 && 子进程没退出
            //子进程没有退出我的waitpid没有等待失败仅仅是监测到了子进程没退出.
            printf("wait done, but child is running...., parent running other things\n");
            for(int i = 0; handlerTask[i] != NULL; i++)
            {
                handlerTask[i](); //采用回调的方式执行我们想让父进程在空闲的时候做的事情
            }
        }
        else if(ret > 0)
        {
            // 1.waitpid调用成功 && 子进程退出了
  printf("wait success, exit code: %d, sig: %d\n", (status>>8)&0xFF, status & 0x7F);
            break;
        }
        else
        {
            // waitpid调用失败
            printf("waitpid call failed\n");
        //    break;
        }
        sleep(1);
    }
    return 0;

}

运行结果

[hongxin@VM-8-2-centos 12-16]$ make
gcc -o mychild mychild.c -std=c99
[hongxin@VM-8-2-centos 12-16]$ ./mychild 
wait done, but child is running...., parent running other things
handler task1
handler task1
handler task1
child running, pid: 22206, ppid: 22205, cnt: 10
wait done, but child is running...., parent running other things
handler task1
handler task1
handler task1

.......................................................................................................................

wait done, but child is running...., parent running other things
handler task1
handler task1
handler task1
wait done, but child is running...., parent running other things
handler task1
handler task1
handler task1
wait success, exit code: 10, sig: 0

进程程序替换


替换原理

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

代码测试

#include <stdio.h>    
#include <stdlib.h>    
#include <sys/types.h>    
#include <sys/wait.h>    
#include <assert.h>    
#include <unistd.h>    
    
    
int main()    
{    
    printf("process is running..\n");    
    
    execl("/usr/bin/ls","ls","-a",NULL);                                                                                                                                                   
    
    
    printf("process is running..\n");    
    return 0;    
} 

测试结果

[hongxin@VM-8-2-centos 12-16_1]$ ./myexec 
process is running..
.  ..  Makefile  myexec  myexec.c

通过测试结果我们发现没有打印最后的printf这个原因就是因为printf在execl之后当execl执行完之后代码和数据已经完全被覆盖开始执行新的代码了所以printf就无法执行了

当我们写错后又会是怎样的结果呢

测试代码

execl("/usr/bin/djhalshl","ls","-a",NULL);                                                                                   测试结果

[hongxin@VM-8-2-centos 12-16_1]$ ./myexec 
process is running..
process is running..

因为我们输入时是错误的地址检测不到这个时候execl函数就会调用失败那么代码和数据就没有被替换下面代码就继续执行。

我们通过查man手册我们又发现execl只有错误时返回值而没有正确时的返回值。

RETURE VALUE
       The exec() functions return only if an error has occurred.  The return value is -1, and errno is set to indicate the error.

这是因为成功后代码和数据就被覆盖了再对下面判断就毫无意义只要返回就一定是错误

通常写法 

int main()    
{    
    printf("process is running..\n");    
                                                                                                                                                                                           
    pid_t id  = fork();    
    assert(id != -1);    
    
    if(id == 0)    
    {    
        execlp("ls", "ls", "-a", "-l", "--color=auto", NULL);    
         exit(1);    
    
    }    
    
    int status = 0;    
    pid_t ret = waitpid(id, &status, 0);    
    if(ret>0) printf("wait success: exit code: %d, sig: %d\n", (status>>8)&0xFF, status & 0x7F);    
    
    printf("process is running..\n");    
    return 0;    
}

为了使execl不影响父进程的代码和数据让子进程来执行来执行父进程的一部分代码。因为虚拟地址空间加页表保证了进程的独立性一旦有执行流想要替换代码和数据就会发生写时拷贝。

运行结果

wait success: exit code: 0, sig: 0
process is running..

总结创建子进程就是想让子进程执行一个全新的程序

替换函数

#include <unistd.h> 

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

l--list将参数一个一个传入execl*中

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

p--path不用告诉execl程序的路径只需要告诉是谁,就会自动在环境变量PATH进行可执行程序的查找

int execle , const char *arg, ...,char *const envp[]);

e环境变量

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

v--vector可以将所有的执行参数放入数组中统一传递而不用进行使用可变参数方案

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

函数解释

这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回。

如果调用出错则返回-1

所以exec函数只有出错的返回值而没有成功的返回值

命名理解

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

l(list) : 表示参数采用列表

v(vector) : 参数用数组

p(path) : 有p自动搜索环境变量PATH

e(env) : 表示自己维护环境变量

 exec调用举例如下:

#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);

        execv("/bin/ps", argv); // 带p的可以使用环境变量PATH无需写全路径

        execvp("ps", argv); // 带e的需要自己组装环境变量

        execve("/bin/ps", argv, envp);

        exit(0);

}

事实上,只有execve是真正的系统调用,其它五个函数最终都调用 execve,所以execve在man手册 第2节,其它函数在man手册第3节。这些函数之间的关系如下图所示。 下图exec函数族 一个完整的例子:

结合以上知识我们就可以用自己的程序调用自己的程序了

测试代码

 myexecl.c

#include <stdio.h>    
#include <stdlib.h>    
#include <sys/types.h>    
#include <sys/wait.h>    
#include <assert.h>    
#include <unistd.h>    
    
    
int main()    
{    
    printf("process is running..\n");    
    
    pid_t id  = fork();    
    assert(id != -1);    
    
    if(id == 0)    
    {    
        execlp("./mybin", "mybin", NULL);                                                                                                                                                  
         exit(1);    
    
    }    
    
    int status = 0;    
    pid_t ret = waitpid(id, &status, 0);    
    if(ret>0) printf("wait success: exit code: %d, sig: %d\n", (status>>8)&0xFF, status & 0x7F);    
    
    printf("process is running..\n");    
    return 0;    
} 

mybin.c

#include <stdio.h>    
    
int main()    
{    
    printf("这是另一个C程序\n");    
    printf("这是另一个C程序\n");    
    printf("这是另一个C程序\n");    
    printf("这是另一个C程序\n");    
     
    return 0;                                                                                                                                                                              
}

Makefile

.PHONY:all    
all: mybin myexec    
    
mybin:mybin.c    
    gcc -o $@ $^ -std=c99    
myexec:myexec.c    
    gcc -o $@ $^ -std=c99    
.PHONY:clean    
clean:    
    rm -f myexec mybin                                                                                                                                                                     
~

运行结果

[hongxin@VM-8-2-centos 12-16_1]$ ./myexec 
process is running..
这是另一个C程序
这是另一个C程序
这是另一个C程序
这是另一个C程序
wait success: exit code: 0, sig: 0
process is running..

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

我的shell


第一步获取命令行

我们在使用shell的时候发现我们在输入命令是前面会有有用户名版本当前路径等信息。

[hongxin@VM-8-2-centos Myshell]$

这里我们可以用硬输入来直接写这个完成后就是需要接受键盘输入的命令了。

int main()
{
        // 输出提示符
        printf("用户名@主机名 当前路径# ");
        fflush(stdout);
        (void)s;
        // 清除最后一个\n , abcd\n
        lineCommand[strlen(lineCommand)-1] = 0; // ?
}

在打印的时候因为没有"\n",所以就会造成缓冲区不刷新的问题所以我就应该调用fflush刷新缓冲区。 

我们通过打印测试发现因为我们输入结束的时候都会输入"\n"那么就会造成多''\n"。那么在字符数组lineCommand中最后一个删除即可。

[hongxin@VM-8-2-centos Myshell]$ ./myshell 
用户名@主机名 当前路径# ll
test : ll

[hongxin@VM-8-2-centos Myshell]$ 

第二步解析命令行

我们在输入命令行时可能不仅仅只输入"ls"也有可能直接"ls -a -l -i"。那么在这个过程中命令行解释器得到应该是"ls" "-a" "-l" "-i",所以我们需要分割字符串

// 字符串切割
        myargv[0] = strtok(lineCommand, " ");
        int i = 1;
        if(myargv[0] != NULL && strcmp(myargv[0], "ls") == 0)
        {
            myargv[i++] = (char*)"--color=auto";
        }

        // 如果没有子串了strtok->NULL, myargv[end] = NULL
        while(myargv[i++] = strtok(NULL, " "));

 
        if(myargv[0] != NULL && strcmp(myargv[0], "cd") == 0)
        {
            if(myargv[1] != NULL) chdir(myargv[1]);
            continue;
        }
        if(myargv[0] != NULL && myargv[1] != NULL && strcmp(myargv[0], "echo") == 0)
        {
            if(strcmp(myargv[1], "$?") == 0)
            {
                printf("%d, %d\n", lastCode, lastSig);
            }
            else
            {
                printf("%s\n", myargv[1]);
            }
            continue;
        }

这里会有几个特殊的点

1.首先我们是输入字符串这里我们会用到strtok关于strtok的用法我们需要注意的是当我们第一次用strtok时

myargv[0] = strtok(lineCommand, " ");

那么第二次就应该从null开始分割

  while(myargv[i++] = strtok(NULL, " ")); 

2.在输入ls的时候我们都需要得到有颜色文件名--而是不需要输入系统默认的它是为了更好识别不同文件的不同性质。那么我们就可以对myargv进行判断如果命令是ls的时候--用是strcmp进行比较我就增加"--color=auto”即可

 if(myargv[0] != NULL && strcmp(myargv[0], "ls") == 0)
        {
            myargv[i++] = (char*)"--color=auto";
        }

3.关于cd命令如果直接执行cd命令我们会发现cd没有改变当前路径

[hongxin@VM-8-2-centos Myshell]$ ./myshell 
用户名@主机名 当前路径# pwd
/home/hongxin/Myshell
用户名@主机名 当前路径# cd ..
用户名@主机名 当前路径# pwd
/home/hongxin/Myshell
用户名@主机名 当前路径# cd ..
用户名@主机名 当前路径# ^C
[hongxin@VM-8-2-centos Myshell]$ 

这里是因为我们只是子进程中进行操作cd命令需要改变当前进程的工作目录不改变当前执行的磁盘下的工作目录那么这里我们就可以用chdir。

lrwxrwxrwx   1 hongxin hongxin 0 Dec 17 15:55 cwd -> /home/hongxin/Myshell

-----当前进程的工作目录
-r--------   1 hongxin hongxin 0 Dec 17 15:55 environ
lrwxrwxrwx   1 hongxin hongxin 0 Dec 17 15:55 exe -> /home/hongxin/Myshell/test
-----当前进程执行的磁盘下的那个一程序

又因为我们更改的是子进程的工作目录执行完毕后我们还是继续用的是父进程。最后我用strcmp判断是cd命令然后用chdir改变子进程的工作目录再用continue执行父进程因为外层是while循环所以又可以在另一个工作目录进行操作。

 if(myargv[0] != NULL && strcmp(myargv[0], "cd") == 0)
        {
            if(myargv[1] != NULL) chdir(myargv[1]);
            continue;
        }

4.关于echo命令它是需要输入之后显示的那么直接打印即可但是$?需要获取status,因为我们程序肯定是正确的我们直接定义退出状态和终止信号为0就好。

 if(myargv[0] != NULL && myargv[1] != NULL && strcmp(myargv[0], "echo") == 0)
        {
            if(strcmp(myargv[1], "$?") == 0)
            {
                printf("%d, %d\n", lastCode, lastSig);
            }
            else
            {
                printf("%s\n", myargv[1]);
            }
            continue;
        }

第三步建立forkexecvp替换子进程

这里我们需要知道为什么选择vp因为这里用到了指针数组所以我应该选择v参数用数组又因为我们需要自动搜索环境变量所以会用到pPATH。

pid_t id = fork();
        assert(id != -1); 
if(id == 0)
        {
            execvp(myargv[0], myargv);
            exit(1);
        }

第四步父进程等待子进程退出wait

最后一步就是为预防进程变成了僵尸进程

 int status = 0;
        pid_t ret = waitpid(id, &status, 0);
        assert(ret > 0);
        (void)ret;
        lastCode = ((status>>8) & 0xFF);
        lastSig = (status & 0x7F);

代码总括

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

#define NUM 1024
#define OPT_NUM 64

char lineCommand[NUM];
char *myargv[OPT_NUM]; //指针数组
int  lastCode = 0;
int  lastSig = 0;

int main()
{
    while(1)
    {
        // 输出提示符
        printf("用户名@主机名 当前路径# ");
        fflush(stdout);

        // 获取用户输入, 输入的时候输入\n
        char *s = fgets(lineCommand, sizeof(lineCommand)-1, stdin);
        assert(s != NULL);
        (void)s;
        // 清除最后一个\n , abcd\n
        lineCommand[strlen(lineCommand)-1] = 0; // ?
        //printf("test : %s\n", lineCommand);
        
        // "ls -a -l -i" -> "ls" "-a" "-l" "-i" -> 1->n
        // 字符串切割
        myargv[0] = strtok(lineCommand, " ");
        int i = 1;
        if(myargv[0] != NULL && strcmp(myargv[0], "ls") == 0)
        {
            myargv[i++] = (char*)"--color=auto";
        }

        // 如果没有子串了strtok->NULL, myargv[end] = NULL
        while(myargv[i++] = strtok(NULL, " "));

        // 如果是cd命令不需要创建子进程,让shell自己执行对应的命令本质就是执行系统接口
        // 像这种不需要让我们的子进程来执行而是让shell自己执行的命令 --- 内建/内置命令
        if(myargv[0] != NULL && strcmp(myargv[0], "cd") == 0)
        {
            if(myargv[1] != NULL) chdir(myargv[1]);
            continue;
        }
        if(myargv[0] != NULL && myargv[1] != NULL && 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
        // 内建命令 --> echo

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

        if(id == 0)
        {
            execvp(myargv[0], myargv);
            exit(1);
        }
        int status = 0;
        pid_t ret = waitpid(id, &status, 0);
        assert(ret > 0);
        (void)ret;
        lastCode = ((status>>8) & 0xFF);
        lastSig = (status & 0x7F);
    }
}

运行测试

hongxin@VM-8-2-centos Myshell]$ ./myshell 
用户名@主机名 当前路径# pwd
/home/hongxin/Myshell
用户名@主机名 当前路径# ls
Makefile  myshell  myshell.c  test  test.c
用户名@主机名 当前路径# ls -a
.  ..  Makefile  myshell  myshell.c  test  test.c

用户名@主机名 当前路径# echo "hello"
"hello"
用户名@主机名 当前路径# pwd
/home/hongxin/Myshell
用户名@主机名 当前路径# cd ..
用户名@主机名 当前路径# pwd
/home/hongxin
用户名@主机名 当前路径# ^C
[hongxin@VM-8-2-centos Myshell]$ 

                                                                       完结


 

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6
标签: linux