进程控制详解

本文深入探讨了 Linux 进程管理,包括 fork 函数用于创建子进程,wait 和 waitpid 函数等待子进程退出并获取状态,以及 exit 和 _exit 的区别。还详细介绍了进程替换的概念,通过 exec 系列函数(如 execl、execv、execle、execvp 和 execve)实现程序的动态替换。最后,通过示例展示了如何构建一个简单的 shell。
摘要由CSDN通过智能技术生成

一、进程创建

1.1 fork函数认识

在Linux中fork函数非常的重要,它的作用是在一个已经存在的进程中创建一个新进程。新进程叫做子进程,原来的进程叫做父进程。

函数名称fork
函数功能创建子进程
头文件#include<unistd.h>
函数原型pid_t fork(void);
参数
返回值>-1:成功(其中子进程返回0,父进程返回子进程的id)
=-1:失败

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

  • 分配新内存和数据结构给子进程
  • 将父进程部分数据结构内容拷贝至子进程中
  • 添加子进程到系统进程列表中
  • fork返回,调度器开始调度

当一个进程调用了fork之后,父子进程代码是共享的,虽然他们都运行到了相同的地方,但是每个进程都可以开始自己的旅程:

#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>
int main()
{
  printf("before:pid is: %d\n",getpid());
  pid_t id=fork();
  
  if(id==-1)
  {
    printf("perror fork()!\n");
  }
  printf("after:pid is: %d,return is %d\n",getpid(),id);
  sleep(1);
  return 0;
}

结果展示:
在这里插入图片描述
我们可以看到,第一行输出是fork之前,只有父进程在执行,打印了before信息,fork创建子进程后,打印了两行after信息,分别由父子进程打印,注意到,进程29404打印了before的pid,而另外一个after却没有打印,这是为啥呢?
在这里插入图片描述
所以,fork之前父进程独立执行,fork之后,两个父子进程执行流分别执行。
注意:fork之后谁先执行,完全由调度器决定。(父子都有可能先执行)

写时拷贝
通常,父子代码共享,父子不再写入时,数据也是共享的,当任意一方试图写入时,便以写时拷贝的方式各自一份副本(在物理内存中)。
在这里插入图片描述

1.2 fork常规用法

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

1.3 fork调用失败的原因

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

二、进程终止

2.1 进程退出场景

  • 代码运行完毕,结果正确
  • 代码运行完毕,结果不正确
  • 代码异常终止(vs下叫做程序崩溃)

2.2 退出码

main函数的return值就是进程的退出码:

  1. 0:success
  2. 非0:failed

查看退出码的指令
echo $?
输出最近一次进程退出时的退出码(说的简单点就是上一条指令执行完毕后的退出码)
在这里插入图片描述
我们一般写程序时,main函数都是return 0,表示结果正确,但是如果我们不return 0呢?

#include<stdio.h>
int main()
{
  printf("hello!\n");
  return 123;//非0
}

我们可以运行此程序,观察退出码。
在这里插入图片描述
这里就可以提出我们退出码的意义

它能够表示结果的正确与否,正确用0表示,因为那么多个数字,0只有一个,但是错误却有多个,用非0数字表示,错误的原因也是有多种可能的

退出码也是不能够随意乱写的,每一个退出码对应的数字,代表不同的错误,我们可以利用函数接口strerror观察有哪些错误码

#include<stdio.h>
#include<string.h>
int main()
{
  for(int i=0;i<100;i++)
  {
    printf("%d:%s\n",i,strerror(i));
  }
  return 0;
}

结果展示部分错误码对应的错误原因:
在这里插入图片描述

2.3 进程退出的方式

  1. main函数中return返回,代表退出进程!那么非main函数呢?它表示的其实就是普通的函数返回
  2. 调用exit函数,它在程序的任意地方调用都是代表终止进程,参数是退出码,exit函数会完成一些收尾工作,例如资源的清理和释放,刷新缓冲区等。
  3. 调用_exit函数,它的作用是强制终止进程,不要进行后续收尾工作,比如刷新缓冲区(用户级别的缓冲区!)

介绍return退出、exit函数和_exit函数
1️⃣ return退出
return退出是一种最为常见的一种退出进程的方法,执行return n等于执行exit(n),因为调用main函数运行时,会将main的返回值当做exit的参数。
在这里插入图片描述
执行并查看退出码:
在这里插入图片描述
2️⃣ exit函数是标准C库中的一个库函数。

函数名称exit
函数功能正常终止一个进程
头文件#inlcude<stdlib.h>
函数原型void exit(int status)
参数status:程序退出的状态
返回值

在这里插入图片描述
执行并查看退出码:
在这里插入图片描述
在调用exit之前,还会做一些其他的工作,

  1. 执行用户通过atexit或者on_exit定义的清理函数。
  2. 关闭所有打开的流,所有的缓冲数据均被写入。
  3. 调用_exit函数。

在这里插入图片描述
结果展示:
在这里插入图片描述
3️⃣ _exit函数
_exit也是标准C库中的一个库函数,它和_Exit函数调用同义。

函数名称_exit
函数功能正常终止一个进程
头文件#include<unistd.h>
函数原型void _exit(int status)
参数status:程序退出时的状态
返回值

_exit是强制退出进程,并不进行后续的收尾工作!
在这里插入图片描述
结果演示:
在这里插入图片描述
注意status定义了进程的终止状态,父进程通过wait来获取该值,虽然status是int,但是仅有低八位可以被父进程所用。所以_exit(-1)时,在终端执行echo $?时,发现返回值是255。

exit_exit函数的区别:

exit函数退出进程前,exit函数会执行用户定义的清理函数、刷新缓冲区,关闭流等操作,然后再终止进程,而_exit函数会直接终止进程,不会做任何收尾工作。

在这里插入图片描述

三、进程等待

3.1 进程等待是什么?

让父进程fork之后,需要通过wait或者waitpid等待子进程退出,父进程想要知道子进程完成的任务情况如何了。

3.2 为什么要让父进程等待?

  1. 通过获取子进程的退出信息(status),能够得知子进程的执行结果
  2. 可以保证“时序问题”,那就是保证子进程先退出,父进程后退出(避免孤儿进程,这样会导致资源泄露和进程管理混乱)
  3. 子进程退出的时候,会先进入僵尸状态,会造成内存泄漏的问题,需要通过父进程wait,释放子进程占用的资源

3.3 如何等待?

利用系统级别的函数waitwaitpid

3.3.1 wait函数

函数名称wait
函数功能暂停当前进程,直至子进程结束,并取回子进程结束时的状态
头文件#include<sys/wait.h>
函数原型pid_t wait(int *status)
参数status:子进程终止状态的地址
返回值>0:成功
<0:失败

说明:输出型参数status,获取子进程状态,不关心时可以设置为NULL

我们写一段代码来看一看,fork之后我们先让子进程运行5秒,之后子进程退出,而让父进程一直在等待(调用wait函数),我们就能看到进程等待的现象。

#include<stdio.h>
#include<string.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<unistd.h>
#include<stdlib.h>
int main()
{
	pid_t id = fork();
	if (id == 0)
	{
		//child
		int cnt = 5;
		while (cnt)//5秒后子进程退出
		{
			printf("child[%d] is running!,cnt is %d\n", getpid(), cnt);
			cnt--;
			sleep(1);
		}
		exit(0);//退出子进程
	}
	printf("father wait begin!\n");
	sleep(10);//休眠10秒
	pid_t ret = wait(NULL);
	if (ret > 0)
	{
		printf("father wait:%d,sucess\n", ret);
	}

	else
	{
		printf("father wait failed!\n");
	}
	sleep(10);//子进程被回收之后,让父进程再活上10秒钟
	return 0;
}

3.3.2 waitpid函数

函数名称waitpid
函数功能获取子进程结束时的状态
头文件#include<sys/wait.h>
函数原型pid_t waitpid(pid_t pid,int *status,int options)
参数pid:指定的子进程PID
status:子进程终止状态的地址
options:控制操作方式的选项
返回值>0:成功
<0:失败

3.3.3 对于三个参数进一步说明:

1️⃣ pid:
1、pid<-1等待进程组识别码为pid绝对值的任何子进程.
2、pid=-1 等待任何子进程,相当于wait().
3、pid=0 等待进程组识别码与目前进程相同的任何子进程.
4、pid>0 等待任何子进程识别码为pid的子进程.

2️⃣ status:
用下面的常用的两个宏:

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

3️⃣ options:

  • 0:默认行为,阻塞等待(父进程什么都不做,就是等待子进程退出)
  • WNOHANG:设置等待方式为非阻塞等待

注意:当一个进程非正常退出时,说明该进程是被信号所杀,那么该进程的退出码也就没有意义了

3.4 如何获取子进程status

3.4.1 如何理解status这个参数呢?

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

在这里插入图片描述
其中进程若是正常终止,高8位表示退出状态,即退出码。若是异常终止,则会被信号所杀,低7位表示终止信号,第8位表示core dump位。

3.4.2 获取退出码和退出信号

我们通过位运算可以,根据status得到退出码和退出信号:

status wait code=(status >> 8) & 0xFF;	//退出码
status exit signal=status & 0x7F; 		//退出信号
#include<stdio.h>
#include<string.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<unistd.h>
#include<stdlib.h>
int main()
{
  pid_t id=fork();
  if(id==0)
  {
    //child
    int cnt=5;
    while(cnt)
    {
      printf("child[%d] is running! cnt is %d\n",getpid(),cnt);
      cnt--;
      sleep(1);
    }
    exit(11);
  }
  printf("father wait begin!\n");
  sleep(10);
  //pid_t ret =wait(NULL);
  int status=0;
  pid_t  ret =waitpid(id,&status,0);
  if(ret>0)
  {
    printf("father wait:%d sucess,status wait code:%d,status exit signal:%d\n",ret,(status>>8)&0xFF,status&0x7F);
  }

  else
  {
    printf("father wait failed!\n");

  }
  sleep(10);
  return 0;
}

结果展示:
在这里插入图片描述
我们对于位操作是不是有点太过复杂和麻烦了,我们可以用上文提到的来代替位操作:

#include<stdio.h>
#include<string.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<unistd.h>
#include<stdlib.h>
int main()
{
	pid_t id = fork();
	if (id == 0)
	{
		//child
		int cnt = 5;
		while (cnt)
		{
			printf("child[%d] is running! cnt is %d\n", getpid(), cnt);
			cnt--;
			sleep(1);
		}
		exit(11);
	}
	printf("father wait begin!\n");
	sleep(10);
	//pid_t ret =wait(NULL);
	int status = 0;
	pid_t  ret = waitpid(id, &status, 0);
	if (ret >0)
	{
		if (WIFEXITED(status))//没有收到任何退出信号
		{	//正常结束,获取对应的退出码
			printf("exit code:%d\n", WEXITSTATUS(status));
		}
		else
		{
			printf("error get a signal!\n");
		}
	}
	/*if (ret > 0)
	{
		printf("father wait:%d sucess,status wait code:%d,status exit signal:%d\n", ret, (status >> 8) & 0xFF, status & 0x7F);
	}

	else
	{
		printf("father wait failed!\n");

	}*/
	sleep(10);
	return 0;
}

达到的效果是一样的:
在这里插入图片描述

3.5 阻塞等待和非阻塞等待

3.5.1 概念

都是等待的一种方式!
阻塞等待:死等,就是上述的情况,父进程一直等待子进程,父进程不做任何事情。
非阻塞等待:我们可以不要让父进程死等,而是在等待期间,父进程去做自己的事情,等子进程退出时再来检测子进程的运行状态
等待中可能需要多次检测,这里就提出来了一个方案:

3.5.2 基于非阻塞等待的轮询检测方案

问题:阻塞了是不是意味着父进程不被调度执行了?

阻塞的本质:其实就是进程的PCB被放入了等待队列中,并将进程的状态从R切换到S状态(不再分配CPU资源,从上层看就是卡住了!)
 返回的本质:进程的PCB从等待队列切换到了运行队列,从而被CPU调度。

我们平时看到某些程序或者OS本身卡住了,长时间不动,我们就叫它hang住了。
wait/waitpid函数中有个参数是WNOHANG(wait no hang):非阻塞等待
返回值结果:

  1. 子进程根本没有退出
  2. 子进程退出来(调用waitpid失败或者成功)

基于阻塞的等待轮询方案代码:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
	pid_t id = fork();
	if (id == 0)
	{
		//child
		int count = 3;
		while (count--){
			printf("child do something PID:%d, PPID:%d\n", getpid(), getppid());
			sleep(3);
		}
		exit(0);
	}

	//father
	while (1)
	{
		int status = 0;
		pid_t ret = waitpid(id, &status, WNOHANG);
		if (ret == 0)
		{
		//子进程没有退出,但是waitpid是成功的,需要父进程重复等待!
			printf("father do nothing!\n");
		}
		else if (ret>0)
		{
		//子进程退出来,waitpid也成功了,获取到了退出结果!
			printf("exit code:%d\n", WEXITSTATUS(status));
			break;
		}
		else//ret<0
		{
		//等待失败!
			perror("waitpid");
			break;
		}
		sleep(1);//让父进程每隔一秒据监测一次
	}
	return 0;
}

过程描述:父进程每隔一秒就去看看自己进程退出没有,若没有就继续等待,若退出了,就获取退出码。
运行结果:
在这里插入图片描述

我们再来看看,就只有这一行简单的代码:
在这里插入图片描述
我们运行起来:
在这里插入图片描述
我们再查看一下ppid:13215
在这里插入图片描述
 这里就有一个我们之前提到过的概念,我们再次明确一下:我们发现这里的13215就是我们的bash。
 bash是我们的命令行启动的所有进程的父进程,那么我们的所有程序都是要子进程来执行的。

那么bash是怎么获得这些进程的退出结果的呢?

一定是通过wait等待的方式得到子进程的退出结果,所以我们能够通过echo $?查到子进程的退出码!

 目前我们创建子进程的目的:通过if else分流来让子进程执行父进程代码的一部分,现在我想让子进程执行一个“全新的程序”呢?——那就是进程的程序替换!

四、进程替换

4.1 替换原理

在这里插入图片描述

 进程的程序替换:进程不变,仅仅替换当前进程的代码和数据的技术。用老的进程的外壳,去执行新进程的代码和数据,其中并没有创建新的进程。

代码演示:

#include<stdio.h>
#include<unistd.h>
int main()
{
  printf("I am a process! pid:%d\n",getpid());

  execl("/usr/bin/ls","ls","-a","-l",NULL);
  //本该执行下面的hello程序的,但是调用execl替换了代码和数据,将会执行ls等命令
  
  printf("hello!\n");
  printf("hello!\n");
  printf("hello!\n");
  printf("hello!\n");
  printf("hello!\n");
  printf("hello!\n");
  return 0;
}

结果展示:
在这里插入图片描述
程序替换的本质:就是把程序的代码和数据,加载到特定的进程的上下文中!
我们平时写的C/C++程序要运行,必须先加载到内存中!如何加载呢?——加载器!(封装的底层就是exec系列的程序替换函数)

为什么要加载到内存中呢?

因为数据是存储在外设磁盘上的,CPU举例内存最近,读取是最方便的!

现在我们想让子进程也进行程序替换:

#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/wait.h>
int main()
{
  if(fork()==0){//child
    printf("command begin...\n");
    execl("/usr/bin/ls","ls","-a","-l","-i",NULL);
    printf("command end...\n");
    exit(1);
  }

  //father
  waitpid(-1,NULL,0);//等待任意一个子进程
  printf("father wait success!\n");
  return 0;
}

结果展示:
在这里插入图片描述
 当我们在执行子进程的替换时,我们的父进程照常执行自己的任务,两者之间互不影响呢?那是因为进程之间具有独立性!

可是父子代码不是共享的吗?

(我们之前提到的只是代码不修改的情况,现在是代码会被修改)进程替换会更改代码区的代码,同时会发生写时拷贝,所以父进程继续执行它本身的代码,而子进程转而去执行替换后的代码。

4.2 替换函数

其实有6种以exec开头的函数,统称为exec函数

#include<unistd>
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[]);

4.3 函数解释

 只要程序替换成功,就不会执行后续的代码,意味着exec* 系列的函数,执行成功的时候就不需要返回值检测!只要exec* 返回了,那就意味着替换失败,调用函数也失败了!(返回-1)
所以exec函数只有出错的返回值而没有成功的返回值!

4.4 exec*接口介绍

函数名参数格式是否带路径是否使用当前环境变量
execl列表不带
execlp列表
execle列表不带不是,需要自己组装环境变量
execv数组不带
execvp数组带
execve数组不带不是,需要自己组装环境变量

4.5 命名解释

这些函数看似容易混淆,其实隐藏着见名知意的规律

  • l(list):表示采用参数列表,比如"ls",“-a”,“-l”,"-i"这样传参
  • v(vector):表示参数采用数组
  • p(path):有p自动搜索环境变量的PATH
  • e(env):表示自己维护的环境变量

所有接口没有本质差别,只是参数不同

为什么有那么多接口?

是为了满足不同的应用场景!

execl函数的使用

见前面的引出小例子

execv函数的使用

#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/wait.h>
int main()
{
  if(fork()==0){//child
    printf("command begin...\n");
   // execl("/usr/bin/ls","ls","-a","-l","-i",NULL);
   
    char* argv[]={
      "ls",
      "-a",
      "-l",
      "-i",
      NULL//必须以NULL结尾
    };
    execv("/usr/bin/ls",argv);
    printf("command end...\n");
    exit(1);
  }
  //father
  waitpid(-1,NULL,0);//等待任意一个子进程
  printf("father wait success!\n");
  return 0;
}

结果展示:
在这里插入图片描述

相当于我们之前在命令行上传入的参数,我们现在将它打包成一个数组,我们将该数组的地址传入execv函数之中。本质上和execl函数是一样的!

execlp函数的使用

直接传入文件的名字即可,不用再带上路径了,因为有环境变量PATH的存在,它会去环境变量里面自动搜索文件路径。

#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/wait.h>
int main()
{
  if(fork()==0){//child
    printf("command begin...\n");
   // execl("/usr/bin/ls","ls","-a","-l","-i",NULL);
  // 
  //  char* argv[]={
  //    "ls",
  //    "-a",
  //    "-l",
  //    "-i",
  //    NULL//必须以NULL结尾
  //  };
  //  execv("/usr/bin/ls",argv);
  //
  //
  
    execlp("ls","ls","-a","-l","-d",NULL);
    //两个 ls 含义完全不同
    printf("command end...\n");
    exit(1);
  }

  //father
  waitpid(-1,NULL,0);//等待任意一个子进程
  printf("father wait success!\n");
  return 0;
}

结果展示:
在这里插入图片描述

execvp函数的使用

传入参数是文件名和命令选项的指针数组的地址

#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/wait.h>
int main()
{
  if(fork()==0){//child
    printf("command begin...\n");
   // execl("/usr/bin/ls","ls","-a","-l","-i",NULL);
   
  //  char* argv[]={
  //    "ls",
  //    "-a",
  //    "-l",
  //    "-i",
  //    NULL//必须以NULL结尾
  //  };
  //  execv("/usr/bin/ls",argv);
  //
  //
  
  //  execlp("ls","ls","-a","-l","-d",NULL);
  
   char* argv[]={
     "ls",
     "-a",
     "-l",
     "-i",
     NULL//必须以NULL结尾
   };
     execvp("ls",argv);
     printf("command end...\n");
     exit(1);
  }

  //father
  waitpid(-1,NULL,0);//等待任意一个子进程
  printf("father wait success!\n");
  return 0;
}

结果展示:
在这里插入图片描述

execle函数的使用

 相较之前的函数,不过多了一个e参数,表示自己维护的环境变量,意思就是你可以把指定的环境变量传给被替换的程序。
我们接下来验证的是,不再替换系统的指令,而是替换我我们自己写的程序了。

手动写的程序:

//myexe.c
#include<stdio.h>
int main()
{
  printf("hello! I am your exe!\n");
  extern char** environ;
  for(int i=0;environ[i];i++)
  {
    printf("%s\n",environ[i]);
  }
  printf("my exe running....done!\n");
  return 0;
}

我们修改名字:将myexec.c修改为myload.c(我们要在myload中替换myexe程序,意思就是利用myload运行myexe)

现在要求利用Makefile一次形成两个可执行程序
我们需要修改一下:

//Makefile
.PHONY:all
all:myexe myload
//设定一个伪目标all
myload:myload.c
	gcc -o $@ $^ -std=c99
myexe:myexe.c
	gcc -o $@ $^ -std=c99
.PHONY:clean
clean:
	rm -f myload myexe

因为想要形成all,但是没用依赖方法,所以Makefile向下扫描,找寻生成myexe和myload的依赖方法!如果要生成多个,只需依葫芦画瓢,在后面添加即可!

结果展示:
在这里插入图片描述
改进myload.c:

#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/wait.h>
int main()
{
  if(fork()==0){//child
    printf("command begin...\n");
    char* env[]={//这里就是自己维护的环境变量
        "MYENV=hello",
        "MYENV=hello",
        "MYENV=hello",
        "MYENV=hello",
        NULL
    };
      execle("./myexe","myexe",NULL,env);
     printf("command end...\n");
     exit(1);
  }

  //father
  waitpid(-1,NULL,0);//等待任意一个子进程
  printf("father wait success!\n");
  return 0;
}

现在我们来实验一下:
在这里插入图片描述
在这里插入图片描述
将myload.c里面的环境变量导入myexe.c,这就叫做带e的替换自定义的环境变量。

execve函数的使用

有了前面几个demo,这个函数用起来就更加简单了,我们只需将要运行的命令打包成一个数组即可。

#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/wait.h>
int main()
{
  if(fork()==0){//child
    printf("command begin...\n");
    char* env[]={
        "MYENV=hello",
        "MYENV=hello",
        "MYENV=hello",
        "MYENV=hello",
        NULL
    };

    char* argv[]={
        "myexe",
        NULL
    };
     execve("./myexe",argv,env);
     printf("command end...\n");
     exit(1);
  }

  //father
  waitpid(-1,NULL,0);//等待任意一个子进程
  printf("father wait success!\n");
  return 0;
}

运行起来:
在这里插入图片描述
总结
事实上,只有execve才是真正的系统调用,其他五个函数底层都是封装的execve函数。
在这里插入图片描述

五、做一个简单的shell

 用下图的时间轴来表示事件发生的次序。shell由标识方块代表,它随时间的流逝,从左向右读取用户输入的指令,shell从用户读取ls,shell建立一个新的进程,然后在那个进程中运行ls进程,并等待子进程退出,然后读取新的命令。
在这里插入图片描述
具体步骤
1️⃣不断打印提示符
2️⃣获取我们输入的字符
3️⃣解析字符串
4️⃣检测是否需要shell本身执行的内建命令
5️⃣执行第三方命令(就是子进程的程序替换)
然后循环上述步骤,就可以实现一个简易版本的shell了。

#include<stdio.h>
#include<unistd.h>
#include<string.h>
#include<sys/wait.h>
#include<stdlib.h>
#define NUM 128
#define CMD_NUM 64

int main()
{
  char command[NUM];//输入的命令行参数
  for(;;)
  { 
  	//1.死循环打印提示符
  	
    char*argv[CMD_NUM]={NULL};//将我们的命令行参数看做一个一个字符串,拆开放入数组中

    command[0]=0;//c语言中,字符串是以\0结尾的,这样做以O(1)的复杂度,将字符串清空
    printf("[sjj@my_mini_shell]# ");

    //2.获取命令行上的字符串
    fgets(command,NUM,stdin);//从标准输入上获取后,放入command数组中
    
    //因为我们输入的时候,会以回车作为结尾,回显出来的时候,会多出一个空行,这里处理一下
    command[strlen(command)-1]='\0';

    //printf("echo: %s\n",command);//回显刚刚获取到的字符串
    
    fflush(stdout);//将缓冲区刷新,不然的话就不会在屏幕上直接打印

    //3.解析字符串 以空格分隔解析
    const char* separate=" ";
    argv[0]=strtok(command,separate);//截取字符串
    
    int i=1;
    while(argv[i]=strtok(NULL,separate))//继续截取下一个字符时,传入NULL即可
    {
      i++;
    }
    //for(int i=0;argv[i];i++)
    //{
    //  printf("argv[%d]:%s\n",i,argv[i]);
    //}
  

    //4.检测命令是否需要shell本身执行?内建命令
    if(strcmp(argv[0],"cd")==0)
    {
      if(argv[1]!=NULL)
      {
        chdir(argv[1]);
      }
    }

    //5.执行第三方命令
    //让子进程替换程序
    //其实这里替换的就是的bash,用我们写的程序去
    if(fork()==0)
    {
      execvp(argv[0],argv);
      exit(1);
    }

    //父进程等待子进程结束
    waitpid(-1,NULL,0);
  }
  return 0;
}

运行效果展示:
在这里插入图片描述

谢谢观看!

评论 39
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值