文章目录
一、自定义shell命令行解释器
学习了前面进程概念,进程控制的相关知识,我们对进程已经有了理性的认识,下面我们一起来实现一个自定义shell把这些知识串联起来,能对进程概念及进程相关各种用法,函数调用接口有一个更深刻是理解和记忆。
实现自定义shell的目标:能处理普通命令、能处理内建命令、能帮助我们理解内建命令/本地变量/环境变量这些概念、能帮助我们理解shell的运行原理。
构建框架
首先我们把要用到的所有文件创建出来,采用头源分离。未来方便,主要用C++编写。
cpp
//myshell.h
#ifndef __MYSHELL_H__
#define __MYSHELL_H__
#include <iostream>
void Debug();
#endif
cpp
//myshell.cc
#include "myshell.h"
void Debug()
{
printf("hello shell!\n");
}
cpp
//makefile
myshell:main.cc myshell.cc
g++ -o $@ $^
.PHONY:clean
clean:
rm -f myshell
输出命令行提示符

实现shell第一步是打印命令行提示符,我们先看xshell的命令行提示符,如上图所示,除了一些符号外有三个主要变量,这三个变量可以直接通过系统调用获取,但是这里我们为了复习一下学过的知识,故采用从环境变量中间接获取,系统的最后一个字符是$,为了区分我们用#。
由于这里我们是C语言和C++混编,所以需要注意一些细节,比如C语言printf字符串的时候不能直接打印string变量,因为它的字符串格式说明符 %s 要求传入的参数是C 风格字符串------ 即一个指向以空字符 '\0' 结尾的字符数组的指针(const char* 类型,所以需要用c_str把string类型变量装换为C 风格字符串再打印。下面是代码示例:
cpp
//myshell.cc
static std::string GetUserName()
{
string username = getenv("USER");
return username.empty() ? "None" : username;
}
static std::string GetHostName()
{
string hostname = getenv("HOSTNAME");
return hostname.empty() ? "None" : hostname;
}
static std::string GetPwd()
{
string pwd = getenv("PWD");
return pwd.empty() ? "None" : pwd;
}
void PrintCommandPrompt()
{
std::string username = GetUserName();
std::string hostname = GetHostName();
std::string pwd = GetPwd();
printf("[%s@%s %s]# ", username.c_str(), hostname.c_str(), pwd.c_str());
}
GetUserName、GetHostName、GetPwd这三个接口我们不想暴露给外部使用,所以可以加static修饰使其只能在myshell.cc文件内部使用。
读取用户输入
因为我们要读取用户输入的整个字符串,所以还需要把空格字符串的空格读进去,所以不能用cin和scanf,因为它们遇到空格都会停止读取,这里我们选用C语言接口fgets,getline也可以,但fgets对于后续一些操作更友好,fgets具体使用介绍如下:

首先需要在main函数里创建一个字符数组commandstr作为参数存放读取用户输入的字符串。
cpp
//main.cc
#include "myshell.h"
#define SIZE 1024
int main()
{
char commandstr[SIZE];
while(true)
{
// 1、打印命令行提示符
PrintCommandPrompt();
// 2、读取用户输入的命令
GetCommandString(commandstr, SIZE);
//测试
printf("%s\n", commandstr);
}
return 0;
}
然后实现GetCommandString的内部逻辑,首先要判断传入的参数是否合法。接着通过fgets读取字符串,读取失败返回false,因为至少会读取一个回车键('\n'),所以不会读取为空。最后把读取到的最后一个字符'\n'置为'\0',因为长度为10的字符串最后一个字符下标为9,所以需要strlen(cmdstr_buff) - 1。若删掉'\n'后字符长度为0说明只读取到了'\n',返回false。
cpp
//myshell.cc
bool GetCommandString(char cmdstr_buff[], int len)
{
if(cmdstr_buff == NULL || len <= 0)
{
//参数不合法
return false;
}
char* res = fgets(cmdstr_buff, len, stdin);
if(res == NULL)
{
//读取字符串失败
return false;
}
//把输入的回车也就是'\n'置为'\0'
cmdstr_buff[strlen(cmdstr_buff) - 1] = 0;
return strlen(cmdstr_buff) == 0 ? false : true;
}
解析命令字符串
经过前面两步后下一步需要解析用户输入的命令字符串,解析命令字符串本质就是创建命令行参数表,并把用户输入的字符串按空格分开,依次放入命令行参数表中。而且系统中的命令行参数表原本就是由bash创建并维护的,我们自定义shell其实也是在一定程度上在模拟实现一个bash。
1、我们知道系统的命令行参数表是main函数的局部变量,而这里我们自定义shell时希望子进程能继承父进程的命令行参数表,所以我们这里需要把命令行参数表定义在全局。
2、有许多函数可以分割字符串,我们选取一个最简单的来使用:strtok

首次调用时第一个参数传入待分割的字符串,后续调用时传NULL,第二个参数传分隔符字符串,比如空格。切割成功返回分割出的子串的首元素地址,切割失败或者切割完毕返回NULL。它的底层原理是把原字符串里的分隔符字符串全部替换成'\0'。
3、开始提取子串写入命令行参数表gargv,第一次调用strtok得到的子串放入gargv[0],然后循环取子串放入,最后提取子串完毕strtok返回NULL写入gargv最后一个位置,正好命令行参数表要以NULL结尾。
4、但是目前解析命令字符串逻辑还有两个bug,其一因为gargv和gargc都是全局变量,所以在main函数死循环逻辑的开头需要初始化全局变量,gargc直接置为0就行了,gargv数组可以用memset初始化更方便:
void * memset ( void * ptr, int value, size_t num );
第一个参数传待设置的内存空间,因为memset是以字节为单位初始化的,第二个参数是要设置的数值(本质是ASCII码值),第三个参数是要设置的长度。
5、其二是如果用户啥都不输入直接按回车键,那么第二步读取用户命令什么都读取不到,commandstr数组将为空,第三步提取时会把strtok返回的NULL写入gargv[0],所以我们在主逻辑main函数的第二步多加一个判断,如果用户没有输入,直接回车,此时直接continue跳过此轮循环的后续逻辑。
cpp
//myshell.cc
//全局定义命令行参数表
char* gargv[ARGS] = {NULL};
int gargc = 0;
void InitGlobal()
{
gargc = 0;
memset(gargv, 0, sizeof(gargv));
}
bool ParseCommandString(char cmd[])
{
if(cmd == NULL)
{
//安全检查
return false;
}
//可以在函数内部定义,SEP表示分隔符
#define SEP " "
//"ls -a -l" -> "ls" "-a" "-l"
//把第一个子串写入gargv[0],然后gargc++
gargv[gargc++] = strtok(cmd, SEP);
//把子串全部写入gargv数组里,并且以NULL结尾
while(gargv[gargc++] = strtok(NULL, SEP))
;//循环空语句
//回退一次命令行参数的个数
--gargc;
//条件编译,测试代码
//因为gargv,gargc定义在该文件,无法在main.cc里debug
//#define DEBUG
#ifdef DEBUG
printf("gargc: %d\n", gargc);
printf("--------------------------\n");
for(int i = 0; i < gargc; i++)
{
printf("gargv[%d]: %s\n", i, gargv[i]);
}
printf("--------------------------\n");
for(int i = 0; gargv[i]; i++)
{
printf("gargv[%d]: %s\n", i, gargv[i]);
}
#endif
return true;
}
cpp
//main.cc
#include "myshell.h"
#define SIZE 1024
int main()
{
char commandstr[SIZE];
while(true)
{
// 0、初始化全局变量
InitGlobal();
// 1、打印命令行提示符
PrintCommandPrompt();
// 2、读取用户输入的命令
//如果用户没有输入,直接回车
//会返回false,此时直接continue
if(!GetCommandString(commandstr, SIZE))
continue;
// 3、解析命令行字符串
ParseCommandString(commandstr);
}
return 0;
}
cpp
//main.cc
#include "myshell.h"
#define SIZE 1024
int main()
{
char commandstr[SIZE];
while(true)
{
// 0、初始化全局变量
InitGlobal();
// 1、打印命令行提示符
PrintCommandPrompt();
// 2、读取用户输入的命令
//如果用户没有输入,直接回车
//会返回false,此时直接continue
if(!GetCommandString(commandstr, SIZE))
continue;
// 3、解析命令行字符串
ParseCommandString(commandstr);
// 4、执行命令
ForkAndExec();
}
return 0;
}
执行命令
首先我们要知道执行命令不能由shell本身来做,因为执行命令会发生程序提花,一但shell被替换那么就无法继续输出命令行提示符和读取用户输入了,所以执行命令需要交由子进程来做。
大体思路是先在main函数逻辑里fork一个子进程,子进程执行程序替换并运行命令,执行完毕后子进程直接退出。父进程等待子进程,不论等待成功还是失败都会继续循环执行shell主逻辑。
然后来选择使用哪个程序替换接口,因为我们要执行的命令没带路径所以要有p,命令行参数已经被我们维护成了表结构,所以要有v,子进程可以通过虚拟地址空间继承到环境变量,所以程序替换时可以不传,那么我们的最佳选择就是execvp。
cpp
void ForkAndExec()
{
pid_t id = fork();
if (id < 0)
{
//fork失败
perror("fork");
return;
}
else if (id == 0)
{
//子进程
execvp(gargv[0], gargv);
exit(0);
}
else {
//父进程
pid_t rid = waitpid(id, nullptr, 0);
}
}
内建命令
cd
我们目前已经实现了一个最基本的shell,还有许多优化工作需要我们做。首先当前的shell运行cd指令时无法切换shell进程的当前工作路径,因为cd命令交给子进程去执行了,改变的也只是子进程的工作路径,运行cd指令的子进程退出后不会对父进程有影响,再执行pwd时负责执行pwd的子进程还是继承原先父进程的工作路径,所以我们肉眼看到路径没有变化。而系统的cd路径切换本质是bash自己在切换,切换后创建的子进程继承了父进程的路径,再pwd就会看到切换后的路径。
1、下面我们来实现cd命令的运行逻辑,首先在主逻辑执行命令步骤之前添加一个检查内建命令、若为内建命令则执行的步骤(BuildInCommandExec),如果是内建命令,则执行完该步骤后直接continue,若不是则继续执行后续逻辑。
2、然后编写BuildInCommandExec的内部逻辑,首先判断gargv[0]是不是"cd",注意不能直接比较,直接比gargv[0]和"cd"是比的两个指针是否相同,我们需要比两个字符串是否相同。需要先将其中一方转换为string,然后再比较,这时另一方就会被隐式转换为string,然后就可以调用string的operator==比较两个字符串内容了。
3、接着通过父进程调用chdir改变当前工作路径,chdir 系统调用是 cd 指令的底层实现的一部分,我们要自己实现cd功能就需要让父进程自己调用chdir来切换自己的工作路径。下面是chdir的文档和使用介绍:

参数 path 是目标目录的路径,绝对路径或相对路径均可,调用成功返回 0,调用失败返回 -1。
4、这里小编补充一点,当我们只输入 "cd" 时功能和 "cd ~" 一样,会使当前工作路径返回家目录。所以我们实现时要考虑这两种情况,若为这两种情况,则需要从环境变量中获取家目录并跳转,若不是则跳转到gargv[1]指定的目录下,绝对路径、相对路径均可。
5、最后处理返回值,该接口默认认为提取到的命令不是内建命令返回false,只有是内建命令并且父进程执行了该指令后才返回true。
下面是示例代码:
cpp
//myshell.cc
static std::string GetHostName()
{
string hostname = getenv("HOSTNAME");
return hostname.empty() ? "None" : hostname;
}
bool BuildInCommandExec()
{
//不能:gargv[0] == "cd" 这样比
//这样比是比较指针是否相同,而非字符内容
std::string cmd = gargv[0];
bool ret = false; //默认不是内建命令
if(cmd == "cd")//这里"cd"会被隐式类型转换为string
{
if(gargc == 2)
{
std::string target = gargv[1];
if(target == "~")
{
//"cd ~"返回家目录
chdir(GetHomePath().c_str());
ret = true;
}
else{
chdir(gargv[1]);
ret = true;
}
}
else if(gargc == 1)
{
chdir(GetHomePath().c_str());
ret = true;
}
else{
//错误
}
}
return ret;
}
echo
echo命令也是一个内建命令,因为"echo ?"可以打印出上一个子进程的退出码,而退出码不是环境变量是本地变量,子进程是拿不到父进程的本地变量的,所以echo是由父进程直接执行的。所以echo指令也需要进BuildInCommandExec接口。首先定义一个全局变量lastcode存子进程的退出码,在执行命令接口ForkAndExec的子进程逻辑中获取子进程的退出码写入lastcode中。当用户输入"echo ?"指令时就把lastcode的值打印出来,lastcode里存的就是上一个子进程的退出码,所以InitGlobal不用初始化lastcode。当输入"echo $(环境变量)"时就通过getenv(const char* name)接口查找环境变量并打印。 当打印其它字符串时就把字符串原封不动的打印出来。
cpp
//myshell.cc
//用于存储上一个子进程的退出码
int lastcode;
void ForkAndExec()
{
pid_t id = fork();
if(id < 0)
{
//fork失败
perror("fork");
return;
}
else if(id == 0)
{
//子进程
execvp(gargv[0], gargv);
exit(0);
}
else{
//父进程
int status = 0;
pid_t rid = waitpid(id, &status, 0);
if(rid > 0)
{
//获取子进程退出码
lastcode = WEXITSTATUS(status);
}
}
}
bool BuildInCommandExec()
{
//不能:gargv[0] == "cd" 这样比
//这样比是比较指针是否相同,而非字符内容
std::string cmd = gargv[0];
bool ret = false; //默认不是内建命令
if(cmd == "cd")//这里"cd"会被隐式类型转换为string
{
if(gargc == 2)
{
std::string target = gargv[1];
if(target == "~")
{
//"cd ~"返回家目录
chdir(GetHomePath().c_str());
lastcode = 0;
ret = true;
}
else{
chdir(gargv[1]);
lastcode = 0;
ret = true;
}
}
else if(gargc == 1)
{
chdir(GetHomePath().c_str());
lastcode = 0;
ret = true;
}
else{
//错误
}
}
else if(cmd == "echo")
{
if(gargc == 2)
{
std::string args = gargv[1];
if(args[0] == '$')
{
if(args[1] == '?')
{
printf("%d\n", lastcode);
lastcode = 0;
ret = true;
}
else{
//char *getenv(const char *name);
const char* name = &args[1];
printf("%s\n", getenv(name));
lastcode = 0;
ret = true;
}
}
else{
printf("%s\n", gargv[1]);
lastcode = 0;
ret = true;
}
}
}
return ret;
}
更新命令行提示符中的当前路径
代码写到这里还有问题,我们cd后再pwd确实看到当前工作路径已经变了,但是为什么输出的命令行提示符的当前路径一直没变呢?为什么pwd后看到的路径却变了呢?

我们一步一步来,先解决命令行提示符的当前路径一直不变的问题。我们在讲环境变量时提到过,环境变量有两个来源,一个是从bash从配置文件中获取,一个是bash启动后自己动态获取并创建,就比如PWD,当用户执行 cd 命令切换目录时,bash 会先通过 chdir() 系统调用修改自身的 cwd,然后立即调用 getcwd() 获取新的 pwd路径,更新到 PWD环境变量中。到目前为止我们自定义的bash已经实现了chdir()的功能,接下来还需要我们实现getcwd()的功能。(getcwd的使用说明:请点击)
命令行提示符的当前路径是通过GetPwd接口获取的,所以我们需要修改原来的GetPwd接口,不再直接getenv获取当前工作路径。
下面是初版代码,并没有更新当前的进程的环境变量中的PWD。
cpp
static std::string GetPwd()
{
//string pwd = getenv("PWD");
//return pwd.empty() ? "None" : pwd;
char pwd[1024];
getcwd(pwd, sizeof(pwd));
return pwd;
}
接下来我们需要更新环境变量表中的PWD,首先需要在全局定义一个字符数组pwd用来存储环境变量表的内容,因为我们知道环境变量表是一个字符指针数组,指向一个一个的字符串或者字符数组,而snprintf可以把拿到的tmp数组格式化输出到字符串中,用法和printf类似,只不过printf是往显示器上输出,而snprintf是往字符串中输出。然后通过putenv环境变量表修改环境变量表,我们之前已经介绍过了。两个接口如下所示:


优化后的代码:
cpp
char pwd[1024];
static std::string GetPwd()
{
char tmp[1024];
getcwd(tmp, sizeof(tmp));
//顺便更新一下自己shell的环境变量
snprintf(pwd, sizeof(pwd), "PWD=%s", tmp);
putenv(pwd);
return pwd;
}
现在我们已经把基本功能实现完毕,还有最后一步,我们看到xshell的命令行提示符中只打印了一个类似"myshell"的路径,而不是 "/home/fdb/lesson21/myshell" 这样的长路径,所以需要截取子串,步骤如下:
cpp
static std::string GetPwd()
{
char temp[1024];
getcwd(temp, sizeof(temp));
//顺便更新一下自己shell的环境变量
snprintf(pwd, sizeof(pwd), "PWD=%s", temp);
putenv(pwd);
//命令行提示符中输出单个路径(截取子串)
std::string pwd_label = temp;
const std::string pathsep = "/"; //路径分隔符
//查找长路径中最后一个'/'的位置
size_t pos = pwd_label.rfind(pathsep);
if(pos == std::string::npos)
{
//整个路径都没有'/',返回None
return "None";
}
//从pos位置的下一个位置开始截取,相当于跳过pathsep截取后续子串
pwd_label = pwd_label.substr(pos + pathsep.size());
//如果此时size为0说明什么都没截取到,说明截取前pwd_label中只有"/"
//则返回"/"
return pwd_label.size() ? pwd_label : "/";
}
自定义shell源码
cpp
#include "myshell.h"
#define SIZE 1024
int main()
{
char commandstr[SIZE];
while(true)
{
// 0、初始化全局变量
InitGlobal();
// 1、打印命令行提示符
PrintCommandPrompt();
// 2、读取用户输入的命令
//如果用户没有输入,直接回车
//会返回false,此时直接continue
if(!GetCommandString(commandstr, SIZE))
continue;
// 3、解析命令行字符串
ParseCommandString(commandstr);
// 4、检查命令,若为内建命令由父进程运行
if(BuildInCommandExec())
continue;
// 5、执行命令
ForkAndExec();
}
return 0;
}
cpp
#include "myshell.h"
//using namespace std不放在头文件中,会污染命名空间
using namespace std;
//全局定义命令行参数表
char* gargv[ARGS] = {NULL};
int gargc = 0;
//用于存储环境变量PWD
char pwd[1024];
//用于存储上一个子进程的退出码
int lastcode;
void Debug()
{
printf("hello shell!\n");
}
void InitGlobal()
{
gargc = 0;
memset(gargv, 0, sizeof(gargv));
}
static std::string GetUserName()
{
string username = getenv("USER");
return username.empty() ? "None" : username;
}
static std::string GetHostName()
{
string hostname = getenv("HOSTNAME");
return hostname.empty() ? "None" : hostname;
}
static std::string GetPwd()
{
//string pwd = getenv("PWD");
//return pwd.empty() ? "None" : pwd;
char temp[1024];
getcwd(temp, sizeof(temp));
//顺便更新一下自己shell的环境变量
snprintf(pwd, sizeof(pwd), "PWD=%s", temp);
putenv(pwd);
//命令行提示符中输出单个路径
std::string pwd_label = temp;
const std::string pathsep = "/"; //路径分隔符
//查找长路径中最后一个'/'的位置
size_t pos = pwd_label.rfind(pathsep);
if(pos == std::string::npos)
{
//整个路径都没有'/',返回None
return "None";
}
//从pos位置的下一个位置开始截取,相当于跳过pathsep截取后续子串
pwd_label = pwd_label.substr(pos + pathsep.size());
//如果此时size为0说明什么都没截取到,说明截取前pwd_label中只有"/"
//则返回"/"
return pwd_label.size() ? pwd_label : "/";
}
static std::string GetHomePath()
{
std::string home = getenv("HOME");
//若环境变量缺失或被篡改home为空,为空则回退到家目录
return home.empty() ? "/" : home;
}
void PrintCommandPrompt()
{
std::string username = GetUserName();
std::string hostname = GetHostName();
std::string pwd = GetPwd();
printf("[%s@%s %s]# ", username.c_str(), hostname.c_str(), pwd.c_str());
}
bool GetCommandString(char cmdstr_buff[], int len)
{
if(cmdstr_buff == NULL || len <= 0)
{
//参数不合法
return false;
}
char* res = fgets(cmdstr_buff, len, stdin);
if(res == NULL)
{
//读取字符串失败
return false;
}
//把输入的回车也就是'\n'置为'\0'
cmdstr_buff[strlen(cmdstr_buff) - 1] = 0;
return strlen(cmdstr_buff) == 0 ? false : true;
}
bool ParseCommandString(char cmd[])
{
if(cmd == NULL)
{
//安全检查
return false;
}
//可以在函数内部定义,SEP表示分隔符
#define SEP " "
//"ls -a -l" -> "ls" "-a" "-l"
//把第一个子串写入gargv[0],然后gargc++
gargv[gargc++] = strtok(cmd, SEP);
//把子串全部写入gargv数组里,并且以NULL结尾
while(gargv[gargc++] = strtok(NULL, SEP))
;//循环空语句
//回退一次命令行参数的个数
--gargc;
//条件编译,测试代码
//#define DEBUG
#ifdef DEBUG
printf("gargc: %d\n", gargc);
printf("--------------------------\n");
for(int i = 0; i < gargc; i++)
{
printf("gargv[%d]: %s\n", i, gargv[i]);
}
printf("--------------------------\n");
for(int i = 0; gargv[i]; i++)
{
printf("gargv[%d]: %s\n", i, gargv[i]);
}
#endif
return true;
}
void ForkAndExec()
{
pid_t id = fork();
if(id < 0)
{
//fork失败
perror("fork");
return;
}
else if(id == 0)
{
//子进程
execvp(gargv[0], gargv);
exit(0);
}
else{
//父进程
int status = 0;
pid_t rid = waitpid(id, &status, 0);
if(rid > 0)
{
//获取子进程退出码
lastcode = WEXITSTATUS(status);
}
}
}
bool BuildInCommandExec()
{
//不能:gargv[0] == "cd" 这样比
//这样比是比较指针是否相同,而非字符内容
std::string cmd = gargv[0];
bool ret = false; //默认不是内建命令
if(cmd == "cd")//这里"cd"会被隐式类型转换为string
{
if(gargc == 2)
{
std::string target = gargv[1];
if(target == "~")
{
//"cd ~"返回家目录
chdir(GetHomePath().c_str());
lastcode = 0;
ret = true;
}
else{
chdir(gargv[1]);
lastcode = 0;
ret = true;
}
}
else if(gargc == 1)
{
chdir(GetHomePath().c_str());
lastcode = 0;
ret = true;
}
else{
//错误
}
}
else if(cmd == "echo")
{
if(gargc == 2)
{
std::string args = gargv[1];
if(args[0] == '$')
{
if(args[1] == '?')
{
printf("%d\n", lastcode);
lastcode = 0;
ret = true;
}
else{
const char* name = &args[1];
printf("%s\n", getenv(name));
lastcode = 0;
ret = true;
}
}
else{
printf("%s\n", gargv[1]);
ret = true;
}
}
}
return ret;
}
myshell.h:
cpp
#ifndef __MYSHELL_H__
#define __MYSHELL_H__
#include <stdio.h>
#include <iostream>
#include <string>
#include <cstdlib>
#include <cstring>
#include <cstdbool>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
//命令行参数表的大小
#define ARGS 64
void Debug();
//初始化全局变量
void InitGlobal();
//输出命令行字符串
void PrintCommandPrompt();
//读取用户输入字符串
bool GetCommandString(char cmdstr_buff[], int len);
//解析命令行字符串
bool ParseCommandString(char cmd[]);
//执行命令
void ForkAndExec();
//检查是否是内建命令,若为内建命令交由父进程运行
bool BuildInCommandExec();
#endif
二、子进程备份
我们前面实现的自定义shell创建子进程都是让它程序替换后执行与父进程完全不同的代码,下面小编再展示一份让父子进程分工合作的代码,让子进程运行父进程代码的一部分。
代码的业务逻辑是保存随机数据到全局数组并备份到文件中,让父进程负责保存数据,让子进程负责备份父进程的数据,这样就可以使保存数据和备份数据并发执行,提高效率。
因为有写时拷贝的存在,即使父子进程操作的是同一份全局数组,也互不影响。
cpp
#include <stdio.h>
#include <time.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int garray[100];
pid_t backup(const char* filename)
{
//交由子进程完成备份
pid_t id = fork();
if(id == 0)
{
FILE* pf = fopen(filename, "w");
for(int i = 0; i < 100; i++)
{
fprintf(pf, "%d ", garray[i]);
}
fclose(pf);
exit(0);
}
return id;
}
int main()
{
srand(time(NULL));
for(int i = 0; i <100; i++)
{
garray[i] = rand() % 10;
}
pid_t sub1 = backup("log1.txt");
for(int i = 0; i <100; i++)
{
garray[i] = rand() % 10;
}
pid_t sub2 = backup("log2.txt");
for(int i = 0; i <100; i++)
{
garray[i] = rand() % 10;
}
pid_t sub3 = backup("log3.txt");
waitpid(sub1, NULL, 0);
waitpid(sub2, NULL, 0);
waitpid(sub3, NULL, 0);
return 0;
}
以上就是小编分享的全部内容了,如果觉得不错还请留下免费的赞和收藏
如果有建议欢迎通过评论区或私信留言,感谢您的大力支持。
一键三连好运连连哦~~