进程控制【万字详解】进程创建 | 进程终止 | 进程等待 | 进程替换 | 实现简易minishell

目录

一、进程的创建

1.初识fork函数

a.fork函数的基本介绍:

b.fork函数返回值及其参数  和  头文件:

2.fork函数的原理 :

3.fork函数用法:​​​​​​​​​​​​​​

4.fork()调用失败的原因

二、进程终止(进程退出)

1.进程退出的场景

a.main()函数的返回值(有两种情况):

b.命令行中获取最近一个进程执行完毕的退出码

2.进程常见的退出方法

I.正常终止/用代码终止一个进程(可以用echo $? 查看退出码)

 II.异常退出

三、进程等待

1.进程等待的必要性(为什么要进程等待)

2.进程等待的方法

I.wait方法

II.waitpid()方法

3.获取子进程status

4.宏

5.阻塞等待 和 非阻塞等待(轮询式等待)

阻塞等待 (用wait()实现)

非阻塞等待 (用waitpid()实现)

6.深入理解阻塞等待和非阻塞等待

四、进程替换

1.进程替换的原理

2.进程替换的过程及其代码

过程图示:

代码: 

 3.exec族函数

五、实现简易minishell



一、进程的创建

1.初识fork函数

a.fork函数的基本介绍:

在linux中fork函数时非常重要的函数,它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程
 

b.fork函数返回值及其参数  和  头文件:

#include 
pid_t fork(void);//pid_t为整形 

返回值:子进程中的fork()返回0,父进程中的fork()返回子进程的id (pid),出错时返回 -1

2.fork函数的原理 :

  fork前后

进程控制【万字详解】进程创建 | 进程终止 | 进程等待 | 进程替换 | 实现简易minishell_第1张图片

pid_t fork(void)

{

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

}

fork内部

进程控制【万字详解】进程创建 | 进程终止 | 进程等待 | 进程替换 | 实现简易minishell_第2张图片

子进程的特点:

1.将父进程的所有数据 共享拷贝 到了子进程中。(若子进程不对父进程的数据进行修改的话,父子进程的数据也是共享的。若子进程对父进程的数据进行修改时,会发生写时拷贝,将父进程的数据进行拷贝一份到子进程中)

写时拷贝 :(是一种延时申请技术,可以提高整机内存的使用率)

进程控制【万字详解】进程创建 | 进程终止 | 进程等待 | 进程替换 | 实现简易minishell_第3张图片

2.子进程和父进程的所有代码共享

3.由于 程序计数器 和   CPU中存储上下文数据的寄存器 原因,子进程虽然可以共享父进程的所有代码,但是在子进程中是从fork()创建子进程之后的代码开始执行的,fork()创建子进程之前的代码默认已经执行过,不会重复执行,所以子进程会执行子进程创建之后的代码但是由于fork()进行返回值是在子进程创建之后进行返回的,所以子进程依然会执行fork()的返回值。

3.fork函数用法:

父进程用fork派生一个子进程,然后子进程进行进程替换,执行其他代码:

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

进程控制【万字详解】进程创建 | 进程终止 | 进程等待 | 进程替换 | 实现简易minishell_第4张图片  

终端2号: 

输入 while :; do ps axj | head -1 && ps ajx |grep myproc -w |grep -v grep; sleep 1; done

可对父子进程进行监控 

进程控制【万字详解】进程创建 | 进程终止 | 进程等待 | 进程替换 | 实现简易minishell_第5张图片

 makefile:

myproc : myproc.c


.PHONY : clean
clean:
	rm -f myproc 

myproc.c

#include 
#include 
int main()
{
  printf("i am father准备开始fork\n");
  pid_t id=fork();
  if(id<0)
  {
    perror("fork");
    return 1;
  }
  else if(id==0)
  {
    while(1)
    {
    //child
    printf("#############################################\n");
    printf("i am child,pid:%d,ppid: %d\n",getpid(), getppid());
    sleep(1);
    }
  }
  else if(id>0)
  {
    while(1)
    {
    //parent
    printf("i am father,pid:%d,ppid: %d\n",getpid(),getppid());
    printf("#############################################\n");
    sleep(1);
    }
  }
  return 0;
}

4.fork()调用失败的原因

·系统中有太多的进程
·实际用户的进程数超过了限制

二、进程终止(进程退出)

进程终止:进程终止时,操作系统释放进程中申请的内核数据结构和对应的数据代码本质就是释放系统资源,最主要是内存)

1.进程退出的场景

·代码运行完毕,结果正确 (main函数的返回值为0则为正确,0:success)
·代码运行完毕,结果不正确(main函数的返回值非0则为不正确,返回值:报错信息)
·代码异常终止(main函数的返回值不具有意义,此时应该去看退出信号)

a.main()函数的返回值(有两种情况):

(main函数的返回值实际上是 进程的退出码,用于表示进程是否是正确返回。)

第一种:若退出码是0,则表示进程正确返回,0:success

第二种:若退出码为非0,则表示进程不正确返回,并且每个退出码都对应一个报错信息,退出码:报错信息

意义:将返回值返回给上一进程,用于监控进程的退出状态。出错时方便定位错误。

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
7:Argument list too long
8:Exec format error
9:Bad file descriptor
10:No child processes
11:Resource temporarily unavailable
12:Cannot allocate memory
13:Permission denied
14:Bad address
15:Block device required
16:Device or resource busy
17:File exists
//以上只列出部分   
//可以用printf("%s",strerror(退出码));打印退出错误的信息,
//或者用perror直接打印最近一个函数执行完毕之后的错误的退出信息


b.命令行中获取最近一个进程执行完毕的退出码

(ls , kill等等都是进程,都具有退出码)

输入  echo $? 

 进程控制【万字详解】进程创建 | 进程终止 | 进程等待 | 进程替换 | 实现简易minishell_第6张图片

2.进程常见的退出方法

I.正常终止/用代码终止一个进程(可以用echo $? 查看退出码)

·从main返回 (用return 语句,返回值,并且终止。只有在main函数中return 才是真正的终止)


·调用exit() 

1.头文件:

是#include

2.返回值及其参数:

void exit(int status-退出码);

3.exit()和return :

和return 语句类似,都是返回值,并且终止;不一样的是,exit()不论在哪个函数中exit()进程都会返回值,并且终止。推荐使用exit()

·_exit() (_exit()和_EXIT()是等价的。)

1.头文件:

是#include

2.返回值及其参数:

void _exit(int status-退出码);

2._exit()和exit()的区别:

_exit()和exit()的区别在于,exit()终止进程时,会将缓冲区的数据先刷新到屏幕上来。而_exit()不会将缓冲区的东西刷新到屏幕上来,而是直接终止。

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

                                                  exit()和_exit()的区别详解 

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

i.使用exit()和_exit()对缓冲区的区别:

a. _exit() 不+ \n

进程控制【万字详解】进程创建 | 进程终止 | 进程等待 | 进程替换 | 实现简易minishell_第7张图片

b. _exit() + \n

进程控制【万字详解】进程创建 | 进程终止 | 进程等待 | 进程替换 | 实现简易minishell_第8张图片

 c.exit() 不+\n

进程控制【万字详解】进程创建 | 进程终止 | 进程等待 | 进程替换 | 实现简易minishell_第9张图片

d.exit() +\n

进程控制【万字详解】进程创建 | 进程终止 | 进程等待 | 进程替换 | 实现简易minishell_第10张图片

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

ii.exit()和_exit()的底层区别:

_exit():_exit()是系统调用接口的函数。

exit():而exit()是把_exit()封装在内,并且增加了其他的函数,共同组合成exit()——是一个库函数。

进程控制【万字详解】进程创建 | 进程终止 | 进程等待 | 进程替换 | 实现简易minishell_第11张图片

 II.异常退出

·ctrl + c,信号终止

三、进程等待

1.进程等待的必要性(为什么要进程等待)

没有进程等待的危害:(子进程变成无法杀死的“僵尸进程”,并造成内存泄漏)

a.子进程退出,父进程如果不管不顾,就可能造成‘僵尸进程’的问题,进而造成内存泄漏


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

僵尸进程:[此处链接正在努力加载中]

有进程等待的好处:(父进程可以获取子进程的退出信息,监控子进程)
c.最后,父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是不对,或者是否正常退出。父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息

2.进程等待的方法

I.wait方法--一般用于阻塞等待

II.waitpid()方法--用于轮询等待(非阻塞式等待),也可用于阻塞等待,与options传的参数有关

I.wait方法

1.头文件:

include
#include

2.返回值及其参数:pid_t wait(int*status);

wait()返回值(两种情况)

第一种:成功返回被等待进程pid(返回值>0时)

第二种:等待失败返回-1 (返回值==-1时)

参数

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

WIFEXITED(status)--子进程退出信号:

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

1.若WIFEXITED非零(status& 0x7F ==0为真),提取子进程退出码:WEXITSTATUS为0时则为正常退出;WEXITSTATUS>0时则为正常退出,但有错误信息。(查看进程的退出码)

2.若WIFEXITED为零时(status& 0x7F ==0为假),子进程的退出码将没有任何意义,转而应该去查看进程的退出信号。(查看进程的退出信号)

II.waitpid()方法

1.头文件:

#include
#include

2.返回值及其参数:pid_ t waitpid(pid_t pid, int *status, int options);

waitpid()返回值:(三种情况)
第一种:当正常返回的时候waitpid返回收集到的子进程的进程ID;(返回值>0时)


第二种:如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0;(返回值==0时)


第三种:如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;(返回值==-1时)


参数:

-------------------------------------------------------------------------------------------------------------------------
pid:
pid=-1,等待任一个子进程。与wait等效。
pid>0.等待其进程ID与pid相等的子进程。

-------------------------------------------------------------------------------------------------------------------------
status: (输出型参数,获取子进程退出状态,不关心则可以设置成为NULL)
WIFEXITED(status)--子进程退出信号:

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

1.若WIFEXITED非零(status& 0x7F ==0为真),提取子进程退出码:WEXITSTATUS为0时则为正常退出;WEXITSTATUS>0时则为正常退出,但有错误信息。(查看进程的退出码)

2.若WIFEXITED为零时(status& 0x7F ==0为假),子进程的退出码将没有任何意义,转而应该去查看进程的退出信号。(查看进程的退出信号)

-------------------------------------------------------------------------------------------------------------------------
options: (默认为0,是阻塞式等待)
WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以阻塞等待。若正常结束,则返回该子进程的ID。

若options传0的时候,则waitpid()和wait()一样,都是阻塞式等待

若options传的是WNOHANG的时候,waitpid()是进行非阻塞式等待,该等待是用于 轮询式 的等待
-------------------------------------------------------------------------------------------------------------------------

3.获取子进程status

·wait和waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充。
·如果传递NULL,表示不关心子进程的退出状态信息。
·否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程。
·status不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究statu低16比特位)

 

但是实际上status并不是直接将退出码或者是退出信号传出来的,而是在status上的一部分区域存储退出码,一部分区域存储退出信号。(只研究status的低16位比特位)

退出码:存放在status的低15位至低8位之间

( 退出码=(status>>8)&0XFF 或 退出码=(status>>8)& 二进制(1 1 1 1 1 1 1 1))

进程控制【万字详解】进程创建 | 进程终止 | 进程等待 | 进程替换 | 实现简易minishell_第12张图片

退出信号:存放在status的低7位( 退出信号=status& 0x7F 或者 退出码=status & 1 1 1 1 1 1 1 )

进程控制【万字详解】进程创建 | 进程终止 | 进程等待 | 进程替换 | 实现简易minishell_第13张图片

 当退出信号==0时,退出码对应退出错误的信息

 当退出信号!=0时,退出码将无意义

4.宏

·退出码:WEXITSTATUS(status) 等价于 (status>>8)&0XFF

·退出信号:WIFEXITED(status) 等价于 status& 0x7F

5.阻塞等待 和 非阻塞等待(轮询式等待)

阻塞等待 (用wait()实现)

代码: 

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

step1.fork()

pid_t pid=fork();

step2.返回失败的情况

if(pid<0)
  {
    printf("error\n");
  }


 step3.子进程

 

 if(pid==0)
  {
   int cnt=5; 
    while(cnt)
    {
    sleep(2);
    printf("我是子进程:%d,pid:%d,ppid:%d\n",cnt--,getpid(),getppid());
    }
    exit(12);
  }

step4.父进程
  

 if(pid>0)
  {
  int status=0;//用于接收退出码
  pid_t res=wait(&status);//res返回的是等待成功的子进程的pid
  if(res>0)
  {
    int st=WEXITSTATUS(status);
    printf("等待子进程退出成功,子进程pid:%d,子进程退出码为:%d:%s\n",res,st,strerror(st));
  }
  else 
  {
    printf("等待子进程失败\n");
  }
  sleep(3); 
  int cnt=5;
  while(cnt--)
    {
      sleep(1);
      printf("我是父进程:%d,pid:%d, ppid:%d\n",cnt,getpid(),getppid());
    }
  printf("父进程已退出\n");
  }

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

效果图:

终端1号(运行程序):

进程控制【万字详解】进程创建 | 进程终止 | 进程等待 | 进程替换 | 实现简易minishell_第14张图片

 

终端2号(监控进程):

进程控制【万字详解】进程创建 | 进程终止 | 进程等待 | 进程替换 | 实现简易minishell_第15张图片

 (中间不会产生僵尸进程,因为子进程一结束就会通过wait()函数,让父进程回收子进程,回收资源,释放空间。)

非阻塞等待 (用waitpid()实现)

(options传WNOHANG用于轮询等待,options传0用于阻塞等待

(除了if(pid>0)的父进程处代码有所修改,其他处均与阻塞等待相同)

代码:

//在轮询等待时给父进程分配的任务

typedef void (*handler_t)();
std::vector  handlers;//因为namespace 没展开
int arr[10]={0};
void fun_one()
{
  printf("这是一个给数组arr元素全部初始化为1的临时任务\n");
  int i=0;
  int sz=sizeof(arr)/sizeof(arr[0]);
  for(i=0;i

//父进程

if(pid>0)
 {
   int quit=0;
  while(!quit)
  {
   int status =0;
   pid_t res=waitpid(-1,&status,WNOHANG);//以非阻塞的方式等待
   if(res>0)
   {
     //等待成功&&子进程退出
     //printf("等待子进程退出成功,退出码:%d\n",WEXITSTATUS(status));
     printf("等待子进程退出成功,退出码:%d\n",WEXITSTATUS(status));
     quit=1;
   }
   else if(res==0)
   {
     sleep(1);
     //等待成功&&子进程没有退出
     printf("进程还在运行中,暂时还没有退出,父进程可以再等一等,处理一下其他事情??\n");
     if(handlers.empty())
     {
       LOAD();
     }
   for(auto& it : handlers) 
   {
     it();
   }
  }
   else if(res<0) 
   {
     //等待失败
       printf("wait失败\n");
       quit=1;
   }
  }
 }

 效果图:

进程控制【万字详解】进程创建 | 进程终止 | 进程等待 | 进程替换 | 实现简易minishell_第16张图片

 可以看到父进程在轮询等待子进程的过程中还完成了两个任务,这就是轮询等待用法

6.深入理解阻塞等待和非阻塞等待

阻塞等待:父进程会一直wait()子进程(一直被挂起:只有pcb在运行队列中,但没有代码和数据了),等到子进程退出(直到子进程退出才重新被唤醒,代码和数据才重新从磁盘中加载到pcb中),wait()才结束并返回值,若等待成功,则返回子进程pid,若等待失败则返回-1

举个例子:例如,你去坐飞机。飞机靠站机场等待乘客,而你在赶来的机场的路上,但飞机不能做其他事情,这就是 阻塞式等待。在到达起飞时间前,飞机一直在等待你。若最后你成功赶上了飞机,则说明飞机等待成功你成功。若最后你没赶上飞机,则说明飞机等待你失败。这就是 阻塞式等待的两种返回值结果。

非阻塞等待:也叫轮询式等待,若子进程并没有退出,父进程不会停下来等待子进程,而是直接跳过,此时waipid()返回的是0。若子进程退出了,则父进程等待子进程成功,此时waitpid()返回的是子进程的pid。若出现错误而等待失败,则返回-1。

举个例子:假如,国庆节放假,我打算约张三出来吃饭。我打了一通电话给张三,张三说还在化妆,说等一下。我等待5分钟。又过了5分钟,我又打电话给张三,张三说还在挑衣服,等一下。我又等待5分钟。......我不断打电话给张三,张三不断推迟。由我不断打电话询问张三,张三一直没有下楼。而张三在等待的过程中可以刷视频,或者打游戏 或 做其他事情。这也就是轮询式等待(非阻塞等待)。若最后张三做完所有事情,并下楼与我去吃饭,则说明我等待成功。若张三还一直在忙别的事,则我一直是在轮询等待。若B最后突然不想去了,放了我的鸽子,则说明我等待失败。这也就是 轮询式等待的三种返回值结果。

图示:

进程控制【万字详解】进程创建 | 进程终止 | 进程等待 | 进程替换 | 实现简易minishell_第17张图片

总结:阻塞vs非阻塞

我们未来编写的代码内容,网络代码,大部分都是IO类的,不断面临阻塞和非阻塞的接口。因为有些网络代码没有函数接口管理,所以都要等待前一个函数执行完之后才能进行下一个,这也相当于是一个阻塞等待。

四、进程替换

1.进程替换的原理

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


2.进程替换的过程及其代码

过程图示:

替换前:进程控制【万字详解】进程创建 | 进程终止 | 进程等待 | 进程替换 | 实现简易minishell_第18张图片

替换后: 

进程控制【万字详解】进程创建 | 进程终止 | 进程等待 | 进程替换 | 实现简易minishell_第19张图片

 回答两个问题:

1.进程替换,有没有创建新的子进程?

答:没有派生新的子进程,而是更改了子进程的映射关系。

2.如何理解所谓将程序放入内存中?

答:放入就是所谓的加载。运用加载器,从一个硬件(可以是磁盘,也可以是其他硬件),搬运到另一个硬件,由操作系统提供的接口来加载,即exec族函数--加载器,其本质就是加载程序的函数。

代码: 

首先是不创建子进程,在父进程中作进程替换

//file.cpp

进程控制【万字详解】进程创建 | 进程终止 | 进程等待 | 进程替换 | 实现简易minishell_第20张图片

//效果图

进程控制【万字详解】进程创建 | 进程终止 | 进程等待 | 进程替换 | 实现简易minishell_第21张图片

接下来是由父进程派生子进程,再由子进程来进程替换

//exec.c

进程控制【万字详解】进程创建 | 进程终止 | 进程等待 | 进程替换 | 实现简易minishell_第22张图片

//cmd.c 

进程控制【万字详解】进程创建 | 进程终止 | 进程等待 | 进程替换 | 实现简易minishell_第23张图片

 //效果图

进程控制【万字详解】进程创建 | 进程终止 | 进程等待 | 进程替换 | 实现简易minishell_第24张图片

 3.exec族函数

进程控制【万字详解】进程创建 | 进程终止 | 进程等待 | 进程替换 | 实现简易minishell_第25张图片

 头文件:#include

extern char**environ

种类:

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

解析:path必须传指定文件的路径。arg,...传的是不定个文件的选项,但最后最好要以nullptr结尾

如:execl("/usr/bin/ls","ls","-a","-l",nullptr);

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

解析:file可传指定文件路径,如果指定文件具有相应的环境变量PATH,也可直接传文件名。

arg,...传的是不定个文件的选项,但最后最好要以nullptr结尾

如:execlp("/usr/bin/ls","ls","-a","-l",nullptr);或execlp("ls","ls","-a","-l",nullptr);

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

解析:path必须传指定文件的路径。arg,...传的是不定个文件的选项,但最后最好要以nullptr结尾envp[]是自己定义的环境变量的数组,可以将环境变量传过去给替换成的进程。

如: char* const envp[]={"myval=233333";     

      execle("/usr/bin/ls","ls","-a","-l",nullptr,envp[]);

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

解析:path必须传指定文件的路径。argv传数组元素是不定个文件选项的数组,数组最后一个元素最好是nullptr。

如:const char* argv[]={"ls","-l","-a",nullptr};      execv("/usr/bin/ls","argv[]);

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

解析:file可传指定文件路径,如果指定文件具有相应的环境变量PATH,也可直接传文件名。argv传数组元素是不定个文件选项的数组,数组最后一个元素最好是nullptr。

如:const char* argv[]={"ls","-l","-a",nullptr};     

       execvp("ls","argv[]);

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

解析:file可传指定文件路径,如果指定文件具有相应的环境变量PATH,也可直接传文件名。argv传数组元素是不定个文件选项的数组,数组最后一个元素最好是nullptr。

envp[]是自己定义的环境变量的数组,可以将环境变量传过去给替换成的进程。

如:const char* argv[]={"ls","-l","-a",nullptr}; 

       char* const envp[]={"myval=233333";         

       execvpe("ls","argv[],envp[]);

-------------------------------------------------------------------------------------------------------------------------                     有了以上的知识,我们可以自行实现一个简易的minishell了

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

五、实现简易minishell

代码:

#include 
#include 
#include 
#include 
#include 
#include 

#define NUM 1024
#define SIZE 32
#define SEP " "

//保存完成的命令行字符串
char cmd_line[NUM];
//保存打散之后的命令行字符串
char *g_argv[SIZE];

// shell 运行原理 :通过让子进程执行进程命令,父进程等待&&解析命令
int main()
{
  //0.命令行解释器,一定是一个常驻内存的进程,不退出
  while(1)
  {
    //1.打印出提示信息 [LX@localhost minishell]#
    printf("[LX@localhost myshell]#");
    
    //由于前面的printf没有\n刷新缓冲区
    //,而又进入下一次循环了,来不及打印,需要用fflush强制将缓冲区的数据刷出来到屏幕上
    fflush(stdout);
    memset(cmd_line,'\0',sizeof cmd_line);

    //2.获取用户的键盘输入[输入的是各种指令和选项]: "ls -a -l -i"
    if(fgets(cmd_line,sizeof cmd_line, stdin)==NULL)
    {
      continue;
    }


    //"ls -a -l -i\n\0"
    //将输入完成指令和选项之后敲的确认键换车给清掉
    //,因为他也会被当成选项和指令输入cmd_line数组中
    //printf("echo: %s\n",cmd_line);
    cmd_line[strlen(cmd_line)-1]='\0';


    //3.命令行字符串解析: "ls -a -l -i"->"ls" "-a" "-i"
    g_argv[0]=strtok(cmd_line,SEP);//第一次调用,要传入原始字符串
    int index=1;
    if(strcmp(g_argv[0],"ls")==0)
    {
      g_argv[index++] = "--color=auto";
    }
    if(strcmp(g_argv[0],"ll")==0)
    {
      g_argv[0]= "ls";
      g_argv[index++]= "-l";
      g_argv[index++]= "--color=auto";
    }
    
    while(g_argv[index++]=strtok(NULL,SEP));//第二次,如果还要解析原始字符串,传入NULL,strtok是分割字符
    
    //for debug
    //for(inedx=0;g_argv[index];index++)
    //printf("g_argv[%d]: %s\n",index,g_argv[index]);
    
    //4.TODO,内置命令,让父进程(shell)自己执行命令,我们叫做内置命令,内建命令
    //内建命令本质其实就是shell中的一个函数调用
    if(strcmp(g_argv[0],"cd")==0)//not child execute, father execute
    {
      if(g_argv[1]!=NULL)
        chdir(g_argv[1]);//cd path,cd ..//chdir是将路径进行更改到g_argv[1]指定的路径中
    }


    //5.fork()
    pid_t id =fork();

    //child
    if(id==0)
    {
      printf("下面功能让子进程进行的\n");
      //cd cmd, current child path
      execvp(g_argv[0],g_argv);//ls -a -l -i
      exit(1);
    }
   
    //father
    int status=0;
    pid_t ret =waitpid(id,&status,0);
    if(ret>0)printf("exit code: %d",WEXITSTATUS(status));
  }
  return 0;
}

效果图:

进程控制【万字详解】进程创建 | 进程终止 | 进程等待 | 进程替换 | 实现简易minishell_第26张图片

你可能感兴趣的:(Linux,c++,开发语言,linux)