Linux进程控制

目录

我们学习的进程控制主要包括进程创建、进程终止、进程等待以及进程程序替换

一、进程创建

1.fork函数

(1)fork函数初识:

(2)问:请你描述一下,fork创建子进程,OS都做了些什么?

(3)再问:fork创建子进程,是不是系统中多了一个进程呢?

(3)于是便有了写时拷贝策略: 如图:

(4)问:“fork之后的代码共享”,这句话中,所谓的”代码共享”是指“fork()”下面的代码共享还是全部代码共享呢?

 2.fork的常规用法

3.fork调用失败的原因 

二、进程终止

1.进程终止的场景

2.进程退出的常用方式

三、进程等待

1.进程等待的必要性

 2.进程等待的方法

wait 方法

waitpid方法

另外,需要注意的是,status并不是按照整型来整体使用的,而是按照比特位的方式。将32个比特位进行划分,只看低16位的。​编辑 在上面这张图中,waitpid所等待进程正常终止情况下的status的低16位中的次低16位(也就是8到15位),表示的就是退出码。

 3.模拟实现

4.为什么要进行进程等待?

5.非阻塞式等待

四、进程替换

1.进程替换的原理

2.初识exec 

 3.进程函数

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

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

 ​编辑

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

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

(5)int execve(const char *path, char *const argv[], char *const envp[]);

(6)总结

五、写一个简易的SHELL(命令行解释器) 

0.首先我们需要知道的是SHELL进程肯定是一个常驻的进程——不退出

1.我们平时在使用shell时,会有提示行,比如这样​编辑 

 2.我们需要获取键盘上输入的指令和选项

3.命令行解析 

5.父进程解析,子进程执行

4.TODO内置命令 


我们学习的进程控制主要包括进程创建、进程终止、进程等待以及进程程序替换

一、进程创建

1.fork函数

(1)fork函数初识:

#include <unistd.h>

pid_t fork(void) 

返回值:父进程返回子进程的pid,子进程返回-1;

(2)问:请你描述一下,fork创建子进程,OS都做了些什么?

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

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

3.将父进程的PCB拷贝给子进程

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

(3)再问:fork创建子进程,是不是系统中多了一个进程呢?

:是的

进程 = PCB + 进程代码和数据 在这其中,我们知道PCB是由OS维护的,而进程的代码和数据则是由C/C++程序加载后的结果。

创建子进程,给子进程分配对应的内核数据结构———拷贝的父进程的 ,必须让子进程独有它,因为进程具有独立性!那代码和数据呢?一般而言,我们没有加载代码和数据的过程,也就是说子进程没有自己的代码和数据!所以,子进程只能“使用”父进程的代码和数据。

在这其中:代码——代码是只读的,所以父子共享,没什么问题。

数据——数据是可读可写的,因为数据是可能被修改的,如果此时还要让父子进程共用的话,那来个什么全局变量之类的,就很有可能出现问题,因此,父子进程的数据必须分离!

(3)于是便有了写时拷贝策略: 如图:

在上图中,子进程的task_struct 是拷贝的父进程的,那也就导致mm_struct和父进程的相同,进程地址空间上数据的地址就指向同一个物理内存。但是如果数据要发生写入(修改),此时子进程正如我们上面所说,必须和父进程的数据分离,而OS又不能提前知道哪些数据会发生改变,并且如果提前拷贝了数据也不一定用的上,于是就有了写时拷贝策略——当数据要发生写入时,页表将其其映射到另一个物理地址上,来将父子进程分离。 

这也就很好的解释“了父子进程中相同地址的变量的值不同”这个现象了——task_struct 是抄的父进程的,导致了进程地址空间相同,而由于变量发生了写入,导致其映射在物理内存上的另一个空间。 

(4)问:“fork之后的代码共享”,这句话中,所谓的”代码共享”是指“fork()”下面的代码共享还是全部代码共享呢?

答:全部代码共享!,但子进程执行是从”fork();”下面开始的。 

我们需要有这样的概念:

1.我们的代码汇编之后,会有很多行代码,而且每行代码加载到内存之后都有对应的地址。

2.因为进程随时可能被中断(可能并没有执行完),下次回来,还必须从之前的位置继续运行(不是最开始!),就要要求CPU必须能随时记录下来当前进程执行的位置,所以CPU中有对应的寄存器数据用来记录当前进程的执行位置! 

CPU中有一个叫EIP的寄存器(又被称为PC指针,来指向当前执行代码的下一行代码的地址) 

 

CPU干三件事:取指令、分析指令、执行指令 (代码编译->汇编->二进制语言 就是指令)CPU只能通过寄存器EIP来执行指令

 寄存器在CPU上只有一套,而寄存器内的数据,是可以有多份的!——这就是所谓的上下文数据,每个进程都有自己的那一份上下文数据!而子进程创建的时候,也会认为自己的EIP起始值是"fork();"下面的代码(父进程的上下文数据存储在PCB中,被子进程给抄过去了),虽然父子进程各自调度,各自会修改EIP,但是这已经不重要了,这都已经是后话了(起始值一样,执行起点就相同)。

 2.fork的常规用法

1.一个父进程希望复制自己,使父子进程同时执行不同的代码段。

例如父进程等待客户端请求,生成子进程来处理请求。

2.一个进程要执行一个不同的程序。

例如父进程从fork返回后,调用exec函数。(进程替换)

3.fork调用失败的原因 

1.系统中有太多的进程了。

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

二、进程终止

 问:进程终止的时候,OS做了什么?

答:释放进程申请的相关内核数据结构(PCB)和对应的数据和代码。(本质就是释放系统资源) 

1.进程终止的场景

1.代码跑完,结果正确。

2.代码跑完,结果不正确。

3.代码没跑完,程序崩溃。

注意:这里的结果正确不正确,就是看退出码的值是否为0(0代表结果正确)。当程序崩溃时,退出码没有任何意义!一般而言,程序崩溃的话退出吗对应的return语句不会执行。 

问:main函数的返回值的意义是什么?(main函数为什么要在后面加个return 0?)

答:

(1)main函数返回值并不总是0

(2)main函数的返回值叫做“进程的退出码”(若为0则代表结果正确 ,若为非0则代表结果错误以及相关错误原因) 

(3)返回给上一级进程,用来评判该进程执行结果用的。父进程可以忽略。

注意:0代表正确,而不正确可以用任意非0,这可以使不同的非0值对应不同的错误原因。

Linux命令:echo $?     获得最近一次进程执行完毕的错误码。

函数:sterror(int 错误码)   显示错误码对应的错误(退出码)

我们自己可以使用这些退出码和含义,但是,如果想要自己定义,也可以自己去设计一套方案。

2.进程退出的常用方式

1.return 语句,注意是main函数的return语句

2.exit(退出码),这个函数可以直接结束进程,注意在任何地方调用都会终止进程。

3._exit(退出码),和exit(退出码)的用法相似。

只是:

_exit 函数会直接终止程序

而exit会先执行析构函数,清空缓冲区(例如cout << "bznb" << endl)等操作再终止程序

(推荐使用exit())

其实exit就是封装的_exit( ),exit()是库函数,而_exit()则是系统调用接口。 

问:都说printf("xxx(没加\n)"),此时数据是在缓冲区的,想要写入到显示器文件中需要刷新缓冲区,那么这个缓冲区到底在哪里呢?谁来维护呢?

答:一定不是在操作系统内部的,C标准库来维护! 

(这个后面会学到:FILE是C标准库提供的结构体,内部有缓冲区的指针->每个FILE中都有对应的缓冲区->C程序代码又算作进程的一部分->task_struct中的“代码与数据”中有关于缓冲区的内容”->每个进程都有对应的pcb->每个进程都有对应的缓冲区 )

三、进程等待

1.进程等待的必要性

(1)子进程退出,父进程如果一直读取子进程状态回收,那子进程就要处于“Z”状态——占用内存。

(2)注意,kill -9 也杀不掉僵尸进程,因为kill -9 是用来强制结束进程的,而僵尸进程已经退出了。换句话说,谁也没有办法杀死一个已经死去的进程。

(3)父进程创建子进程,是要让子进程办事的,那么子进程把任务完成的怎么样、结果正确还是错误,父进程肯定是需要关心的,而父进程关心子进程的方式就是进程等待!

 总结:进程等待的原因:父进程通过进程等待的方式,回收子进程的资源,获取子进程的退出信息。

 2.进程等待的方法

wait 方法

#include <sys/types.h>

#include <sys/wait.h>



pid_t wait(int* status);//错误码

wait会等待一个进程,直到该进程状态发生改变。

wait的返回值有两种情况:1.wait成功 会返回子进程的pid 2.wait失败会返回 -1

 例如:


int main()
{
int id = fork();
if(id == -1)
{    
    perror("fork);
    exit(1);
}
else if(id == 0)
{
    //子进程正常运行,可以正常结束
    //...
}
else
{
    //父进程等待子进程结束
    int ret = wait(NULL);
    if(ret > 0)
    {
        //当ret > 0 时,就是等待成功了,返回的是子进程的pid
    }
    //等待完成之后,再执行父进程后面的代码。
}
return 0;
}

注意:

1.当进程等待时,进程处于阻塞态。

2.wait是系统调用接口

3.wait的参数是输出型参数,用来获取子进程的退出状态的,不关心的话可以设置成为NULL。

waitpid方法

//头文件和wait相同

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

pid 是被等待进程的pid
status 是错误码(输出型参数,用来获取被等待进程的退出状态)
options 是表示等待状态 ,默认为0,表示阻塞等待

waitpid同样也会等待一个进程直到它的状态发生改变

waitpid的返回值有三种:

1.当等待成功时,能返回收集到的子进程的pid

2.如果设置了选项WNOHANG,而调用中waitpid发现没有已经退出的子进程可以收集,就返回0(即等待成功,但是子进程未退出)

3.如果调用出错,就会返回-1.

对于waitpid的参数:

对于pid

也分为两种情况:

1. pid 为 -1 ,就说明waitpid等待任意一个子进程,此时waitpid的功能和wait一样。

2.pid大于 0 时,等待进程ID和pid相等的子进程。  

对于status

它是一个输出型参数,可以设为NULL来表示不关心输出的结果。如果关心输出的结果,可以通过设立一个变量来显示退出状态,例如:

//定义一个变量status
int status;

waitpid( ,&status, )//会把子进程的退出状态给到status变量
另外,需要注意的是,status并不是按照整型来整体使用的,而是按照比特位的方式。将32个比特位进行划分,只看低16位的。 在上面这张图中,waitpid所等待进程正常终止情况下的status的低16位中的次低16位(也就是8到15位),表示的就是退出码。

而如果进程没有正常终止而是被杀掉了,那status的低16位中的最低0到7位,所代表的就是子进程收到的信号(如果我们想查看,可以 status& 0x7f)——如果位运算出来的数不是0,那就代表进程崩溃、异常退出了,而第8位就代表core dump标志。

(0x7f = 16乘7+15 = 127 = 01111111二进制 ,配合位运算中的与&运算——同时为1才为1,可以得到最低七位的结果)

注意:程序异常不光光只有可能是内部代码有问题,也有可能是外力直接杀掉-->这导致无法确定子进程代码是否跑完了。

但我们可以得出结论:进程异常退出或者崩溃,本质上就是OS杀掉了进程。

那OS是怎么杀进程的呢?发射信号! 

我们kill -1 可以查看信号列表 

如果我们想要看到子进程的退出码,可以:(status>>8)&0xff)

//...
int status = 0;
pid_t ret = waitpid(fork(),&status,0);
if(ret>0)
{
    printf("等待结束,退出码为:%d\n",(status>>8)&0xff));
}

(0xff = 15乘16+15 = 255 = 11111111) 

另外,我们还能通过将status传参函数方式来获得退出码:

bool WIFEXITED(status):若status为正常终止的子进程返回的状态,那结果就为真(查看进程是否正常退出)

int WEXITSTATUS(status): 若WIFEXITED,非0,则提取出这个进程的退出码(就是封装了WIFEXITED和(status>>8)&0xff)方法的函数)(查看进程的退出码)

对于options

默认为0,表示阻塞式等待

 3.模拟实现

我们试着用代码去实现一个进程的阻塞等待的例子:

int main()
{
 pid_t pid;
 pid = fork();
 if(pid < 0)
 {
     printf("%s fork error\n",__FUNCTION__);//__FUNCTION__是一个宏,可以获得当前函数名。
     return 1;
 } 
 else if( pid == 0 )
 { //child
     printf("child is run, pid is : %d\n",getpid());
     sleep(5);
     exit(257);
 } 
 else
 {
     int status = 0;
     pid_t ret = waitpid(-1, &status, 0);//阻塞式等待,等待5S
     printf("this is test for wait\n");
     if( WIFEXITED(status) && ret == pid )
     {
       printf("wait child 5s success, child return code is :%d.\n",WEXITSTATUS(status));
     }
     else
     {
       printf("wait child failed, return.\n");
       return 1;
     }
 }
 return 0;
}

运行结果:
[root@localhost linux]# ./a.out
child is run, pid is : 45110
this is test for wait
wait child 5s success, child return code is :1.

4.为什么要进行进程等待?

 1.问:父进程通过wait/waitpid 可以间接拿到子进程的退出码,那为什么要用wait和waitpid函数呢?直接全局变量不行吗?

答:全局变量肯定是不行的,因为两个进程之间的数据是无法共通的(进程具有独立性),当子进程全局变量做修改时,会发生写时拷贝。况且异常信号用全局变量是拿不了的。

2.问:既然进程有独立性,进程退出码不也是进程的数据吗?父进程为啥能通过wait和waitpid拿到呢?

答:因为就算是僵尸进程,他也保留了自己的PCB,tast_struct里面保留了任何进程退出时的结果信息。本质上,wait和waitpid就是读取子进程的task_struct(中的int exit_code,int exit_signed成员),由于wait和/waitpid是系统调用接口,所以可以访问内核数据结构task_struct。

总结:为什么要进行进程等待:

1.只有子进程退出,父进程才会waitpid执行返回(在这期间父进程还没死),这使得进程退出具有了一定的顺序性,将来可以让父进程做更多的收尾工作。

2.防止内存泄露,因为僵尸状态的PCB会占用内存。

3.关心到子进程状态。

5.非阻塞式等待

pid_t waitpid(pid_t pid,&status,int options)

我们知道默认情况下options为0,代表阻塞式等待。如果我们想要非阻塞式等待,就要修改options的值。

options有两种情况: 1. 0  阻塞等待  2. WNOHANG 如果pid子进程没有退出,那waitpid立刻返回0。(WNOHANG就是wait no hang的意思)

(Linux自己用C语言写的——系统调用接口——OS自己提供的接口——就是C语言函数)

系统提供的一些大写的标记位WNOHANG,其实就是宏。

我们常说“夯住了”,其实就是没被CPU调度的进程要么在阻塞队列中,要么就是等待被调度。(CPU太忙了)。

阻塞等待:一般都是在内核中阻塞,等待被唤醒(把PCB放在等待队列中)(一般一个进程阻塞了,他的代码和数据就会被切换下去)

非阻塞等待:可以通过不断地调用来检测子进程的状态。

我们未来的编写代码的内容,网络代码,大部分都是IO类别的,会不断面临阻塞和非阻塞的接口。

小插曲:C++源文件可以写成.cpp/.cc/.cxx

非阻塞等待可让父进程执行别的任务(在等待过程中)

阻塞等待的例子:

int main()
{
    pid_t pid;
    pid = fork();
    if(pid<0)
    {
        perror("fork");
        return 1;
    }
    else if(pid == 0)
    {
        printf("我是子进程\n");
        sleep(5);
        exit(257);
    }
    else 
    {
        int status = 0;
        pid_t ret = waitpid(-1(或者吧pid写出来),&status,0);//阻塞等待五秒
        prinf("我在等\n");
        if(WIFEXITED(status)&&ret = pid)
        {
            prinf("等待5s之后子进程正常退出,退出码为%d\n",WEXITSTATUS(status));
        }
        else
        {
            printf("等待失败\n");
            return 1;
        }
    }
    return 0;
}

 将这串代码改为非阻塞式等待只需要:

四、进程替换

1.进程替换的原理

 我们前面已经说了,fork()之后。父子各自执行父进程代码的一部分。那如果子进程想执行一个全新的程序呢?

即:子进程想要有自己的代码呢?

——这就需要用到进程替换

进程替换是通过特定的接口,加载磁盘上的一个权限的程序(代码和数据),加载到调用进程的地址空间中,从而让其执行另一个程序。

进程替换:1.将新的磁盘程序加载到内存 2.和当前进程页表重新建立映射关系

问:进程替换之后,创建了新的进程吗?

答:没有,只是重新建立了映射关系,其他东西都没变的 

问:如何理解所谓的“将程序放入到内存中”?

答:就是程序加载到内存,exec*函数本质上就是如何加载程序的函数。

我们使用进程替换的方式就是exec函数,当进程调用一种exec函数时,该进程的用户空间的代码和数据完全被新的程序替换,从程序的启动例程开始执行。调用exec并不创建新的进程,所以调用exec前后该进程的pid不会改变。

2.初识exec 

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

path就是一个字符串,对应文件的路径,传这个参数用来找到路径

path后面的参数就是把命令行上的参数一个一个填进,最后一个参数必须是NULL表示参数传递完毕。

例如:

execl("usr/bin/ls","ls",NULL);

如果我们用的是ls -l 命令
那就得写成:execl("usr/bin/ls","ls","-l",NULL);

同理,如果我们用的是 ls -l -a 命令
那就得写成:execl("usr/bin/ls","ls","-l","-a",NULL);

现在我们有个例子:

int main()
{
    printf("bznb\n");
    execl("usr/bin/ls","ls",NULL);
    printf("bzbnb\n");
    return 0;
}
我们会发现,下面的printf函数没有打印处bzbnb,这是为什么呢?(因为bznb)

 execl是程序替换,调用该函数成功之后,会将当前进程的所有的代码和数据都进行替换,包括已执行的和没执行的。(所以,一旦调用成功,后续的所有代码,全部都不会执行)

如果execl调用失败(例如路径错误),那后面的代码会接着执行

execl调用失败的话会返回-1,调用成功的话没有返回值(也不需要有,因为我进程都替换了,还要返回值干嘛呢)因此execl不需要进行返回值的判定,可以直接在后面跟个exit(1)就好了,这样如果调用execl失败就会执行exit(1),如果调用成功就不会执行,而是去执行别的程序。

问:进程替换之前,父子进程数据和代码的关系?

答:代码共享,数据写时拷贝。(指向一个物理地址,需要修改的数值映射到别的物理地址上面)

问:当子进程加载新程序的时候,不就是一种“写入”吗?那代码要不要写时拷贝将父子的代码分离呢?

答:必须分离,这页导致了父子进程在代码和数据上面彻底分开了。

 3.进程函数

Linux下的进程替换函数主要包括这几种

#include <unistd.h>
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 execve(const char *path, char *const argv[], char *const envp[]);则是操作系统系统的系统调用接口,上面的这些函数底层都会调用同一个execve接口,使参数变成符合execve要求。

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

在这个函数中,参数path是路径,这个我们知道,后面这个argv[ ]指针数组是干啥的呢?
在exec族的函数中,函数名中带v的,第二个参数都是传指针数组的。
其实argv就是当命令行参数过多时,把所有参数(字符串)放在一个数组中而已。
例如:

所谓指针数组,其实就是指向各个字符串的指针。

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

在exec族的函数中,所有函数名带p的函数的第一个参数都是file,意为:

我自己会在环境变量PATH中进程查找,你不用告诉我你要执行的程序的路径 

因此我们在使用execlp时,可以这样写:

 

 

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

 我们先观察一下该函数的函数名,我们发现他又有v又有p。

其实他就是(1)和(2)的结合体——第一个参数可直接写指令名称,第二个参数传指针数组。

我们使用一下:

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

观察函数名我们发现,这次多了一个e,观察参数列表。我们发现,后面又多了一个指针数组。

其实,前两个参数我们知道,分别是路径和选项(命令行参数),第三个参数则是环境变量 。

例:

因为没有MYENV环境变量,所以我们打印它的路径就是空指针。

我们另起一个进程,在其中导入一个环境变量MYENV即可。
 

(5)int execve(const char *path, char *const argv[], char *const envp[]);

execve是系统提供的接口,上面的那些底层全部都会调用这唯一一个接口,他们内部所做的事就是让参数符合这个接口的参数列表,(也就是系统提供的封装,用来满足不同的调用场景)

(6)总结

函数名参数格式是否自带路径是否使用当前环境变量
execl列表 (有l)不是 (没有p) 是 (没有e)
execlp列表 (有l)是 (有p)

是(没有e)

execle列表 (有l)不是(没有p)不是(有e)
execv数组(有v)不是(没有p)是(没有e)
execvp数组(有v)是(有p)不是(有e)
execve数组(有v)不是(没有p)不是(有e)

总结:

第一个参数由是否有p来决定是否要写绝对路径(有p就不用写)

第二个参数由v和l决定,v就传数组,l就一个一个传

第三个参数由是否有e决定,有e就要传环境变量,没有e就用当前的环境变量

五、写一个简易的SHELL(命令行解释器) 

 我们可以综合上面的知识,写一个简单的SHELL。

首先我们要有SHELL的运行原理:子进程来执行命令,父进程等待子进程执行完成并且解析命令。

0.首先我们需要知道的是SHELL进程肯定是一个常驻的进程——不退出

因此我们把整个程序在while(1)死循环中进行。

1.我们平时在使用shell时,会有提示行,比如这样 

所以我们也可以拟写一份这样的提示行

 2.我们需要获取键盘上输入的指令和选项

我们讲这些指令和选线视为字符串,那事情就变得简单了。

3.命令行解析 

 我们使用strtok函数来执行字符串切割的工作:

strtok (char* str,const char* delim)

str:要切割的字符串,第一次需要传原始字符串,后续如果还是对该字符串进行切割操作,只需要传NULL即可。

delim:分割符

5.父进程解析,子进程执行

4.TODO内置命令 

如果没有这一部分的内置命令,我们的SHELL功能是不完整的,cd命令如果交由子进程来执行,那子进程切换了路径之后就退出了,没有任何的意义。因此,对于切换路径的命令的需要交给父进程来完成。

在切换路径操作上,我们使用的是chdir函数。

chdir函数是个系统能够调用接口,可以更改当前的工作目录 

5.代码完善

我们稍作修饰,就完成了这样一个shell程序:

#include <stdio.h>
    2 #include <stdlib.h>
    3 #include <unistd.h>
    4 #include <string.h>
    5 
    6 char* g_argv[32];
    7 char cmd_line[1024];//全局变量字符串,用来包含指令行
    8 int main()
    9 {
   10   while(1)//shell程序是一个不会退出的程序,因此我们把他放在这样的一个死循环中
   11   {
   12     //1.打印出提示信息,我们要在提示信息后面输入我们的指令。
   13     printf("[bz@localhost myshell]#");//注意这里我们没有加\n,是因为我们需要在提示信息后面输入指令
   14     fflush(stdout);//由于没有\n,因此我们必须用fflush函数来刷新缓冲区
   15     memset(cmd_line,'\0',sizeof(cmd_line));//先把我们要输入的指令字符串给初识化一下
   16     
   17     //2.获取用户键盘输入的指令和选项(例如 ls -l -a)
   18     if(fgets(cmd_line,sizeof(cmd_line),stdin) == NULL)
   19     {
   20       continue;//重新输入(基本不会出错),
   21     }
   22     //一般我们输入指令之后会回车一下,这个回车需要去掉。
   23     cmd_line[strlen(cmd_line)-1]='\0';//strlen是不会算入'\o'的,因此strlen-1一定就是最后一个字符也就是'\n'的下标。
   24 
   25     //3.命令行字符串解析(例如将 ls -l -a 解析成为 "ls" "-l" "-a" 这些命令和选项
   26     //我们进行分割输入的字符串,以 空格 为分隔符即可
   27     g_argv[0] = strtok(cmd_line," ");
   28     int index = 1;
W> 29     while(g_argv[index++]=strtok(NULL," "));
   30 
   31     //4.TODO内置命令
   32     if(strcmp(g_argv[0],"cd")==0)//不想让子进程进行,而是想让父进程进行的命令
   33     {
   34       if(g_argv[1]!=NULL)
   35       {                                                                                                                                                                                       
   36         chdir(g_argv[1]);
   37       }
   38       continue;
   39     }
   40     //这种想让父进程(shell进程)自己执行的命令,我们就叫做内置命令,内建命令,内建命令其实本质就是shell中的一个函数调用
   41
   42  //5.fork()
   43     pid_t id = fork();
   44     if(id == -1)
   45     {                                                                                                                                                                                         
   46       perror("fork");
   47       exit(1);
   48     }
   49     else if(id == 0)
   50     {
   51       //子进程
   52       execvp(g_argv[0],g_argv);//ls -a -l
   53       //这里无论什么命令进来,都会执行,但是对于子进程,如果让他执行cd命令这中内置命令,它执行完了之后就退出了,没有意义
E> 54       exit(1)
   55     }
   56     //父进程
   57     int status = 0;
E> 58     pid_t ret = waitpid(id,&status,NULL);
   59     if(ret>0)
   60       printf("exit cod: %d\n",WEXITSTATUS(status));
   61   }
   62   return 0;
   63 }

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值