Linux系统编程:进程控制

目录

1.进程创建

1.1依旧鞭尸fork

1.1.1fork做什么

1.1.2fork的返回值

1.1.3写时拷贝

1.1.4fork的意义

1.1.5fork失败原因

2.进程终止

2.1进程退出码

2.1.1为什么main函数要返回0

2.1.2退出码概念

2.1.2.1查看退出码echo $?

2.1.2.2翻译错误退出码strerror

2.2进程正常退出

2.2.1 return退出

2.2.2 exit退出

2.2.3 _exit退出

2.2.4 return exit _exit 区别与联系

2.3进程异常退出

3.进程等待

3.1如何理解

3.1.1是什么,怎么做

3.1.2必要性

3.2等待方法

3.2.0获取子进程status

3.2.1wait

3.2.2waitpid

3.3非阻塞轮询

3.4多进程创建与等待 代码模型

4.进程替换

4.1替换原理

4.2替换函数

4.2.1命名解释

4.2.2函数解释

5.实现Shell命令行解释器


1.进程创建

1.1依旧鞭尸fork

1.1.1fork做什么

众所周知,在Linux中fork函数是非常重要的函数,它从已存在进程中创建一个新进程。新进程为子进程,原进程为父进程。

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

1——>分配新的内存块和内存数据给子进程。

2——>将父进程部分数据结构内容拷贝给子进程。

3——>添加子进程到系统进程列表当中。

4——>fork返回,开始调度器调度。

当进程调用fork之后,就会有二进制代码完全相同的两个进程。而且他们都运行到相同的地方。但每个进程都可以开始他们自己的旅程,看下面这段代码

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;
}
运⾏结果:
[root@localhost linux]# ./a.out
Before: pid is 43676
After:pid is 43676, fork return 43677
After:pid is 43677, fork return 0

可以看到有三行输出,一行before和两行after,43676打印了before接着打印了after,但是43677没有打印before这是为什么?看下图

所以fork之前父进程独立执行,fork之后,父子进程执行流轮流进程。执行的顺序完全由调度器决定。

1.1.2fork的返回值

返回给父进程 子进程的PID,返回给子进程 0。

1.为什么要这样?! 

一个父进程可以创建多个子进程,但一个子进程只有一个父进程。对于子进程来说,父进程不需要被识别,通过PPID就能找到父进程。对于父进程来说 子进程才需要被失败,因为父进程创建子进程目的是为了让子进程工作,父进程只有知道子进程的PID才能很好的对该子进程委派任务。

2.为什么有两个返回值?

fork函数内部执行retrun之前,子进程就已经创建完毕了,所以return这个语句,父进程和子进程都会执行一次,这就是为啥会有两个返回值。

1.1.3写时拷贝

通常,父子代码共享,父子在不写入时,数据也是共享的。当任意一方试图写入,就会以写时拷贝的方式拷贝一份副本然后进行修改。具体见下图

因为写时拷贝,所以父子进程得以彻底分离,完成了进程独立性的技术保证。

1.1.4fork的意义

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

2.一个进程要执行一个不同的程序。例如子进程从fork返回后,调用exec函数(进程替换)。

1.1.5fork失败原因

fork函数创建子进程可能也会失败,具体原因如下:

1.系统中有太多进程,内存资源不足,创建子进程失败。

2.实际用户的进程数目超过了显示,子进程创建失败,比如PID就是有限的。

2.进程终止

进程退出的场景有三:

我们最希望的:代码运行完毕,结果正确。

帮助我们提升的:代码运行完毕,结果错误。

我们不想看到的:代码异常终止(进程崩溃)。

2.1进程退出码

2.1.1为什么main函数要返回0

众所周知,main函数是代码的入口。实际上,main函数只是用户级别代码的入口,main函数也是被其他代码调用的。比如在VS2013中main函数就是被一个名为__tmainCRTStartup的函数所调用,而__tmainCRTStartup又是通过加载器被操作系统调用。

既然main函数是被操作系统间接调用的,那么main函数调用结束后就应该给操作系统返回相应的退出信息。这个就是退出码,在main函数中就是返回值0,返回0了就代表代码运行完毕,结果正确。返回其他的就是出问题了。

2.1.2退出码概念

2.1.2.1查看退出码echo $?

进程运行结束后,可以通过 echo $? 命令查看进程最近一次退出码

翻了一段陈年老代码,运行!

查看!退出码是0,说明main函数也是顺利进行了。

2.1.2.2翻译错误退出码strerror

C语言中我们可以通过strerror函数,利用错误退出码,找到错误信息。

通过下面代码我们可以看到不同退出码对应的退出信息。可以发现退出码非0,就代表着各种错误。

ps:退出码都有对应的 字符串解释,帮助用户确定执行失败的原因。而这些退出码具体代表什么含义都是人为规定的,不同环境下相同退出码代表的执行失败原因可能不同。

Linux Shell主要退出码

2.2进程正常退出

2.2.1 return退出

参照2.1.1

2.2.2 exit退出

exit退出进程也是常用的方法,exit可以在代码的任何地方退出进程,并在退出前做一堆准备工作:

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

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

3.调用_exit函数终止进程。

2.2.3 _exit退出

使用_exit函数并不常用,但也可以作为终止进程的一种方法,但相较于exit ,_exit并不会做任何收尾工作。

比如将上面代码exit替换成_exit进程终止后,缓冲区信息不会被打印,

代码演示exit and _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]#

2.2.4 return exit _exit 区别与联系

区别:

只有在main函数中的return起到退出进程的作用,其他任何函数的return都不会终止进程,而exit和_exit可以在代码任何位置退出进程。

exit相较于_exit会处理进程退出的收尾工作。

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

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

3.调用_exit函数终止进程。

联系:

return num ==调用 exit(num)

调用main函数运行结束后,会将返回值传给exit,然后调用exit函数。

2.3进程异常退出

异常退出的进程不会有退出码,所有进程结束后应该优先判断退出是否异常,再确定用不用退出码。

向进程发送信号导致进程异常退出:

例如,在进程运行过程中向进程发生kill -9信号使得进程异常退出,或是使用Ctrl+C使得进程异常退出等。

代码错误导致进程运行时异常退出:

比如除0错误,和访问野指针,会触发硬件级别的错误,除0:CPU状态寄存器会出现溢出,访问野指针:即将访问的虚拟地址在页表中找不到对应的映射,或者是建立的映射关系只有只读权限。像这样的问题,最终都是转换成一些硬件级别的信号来给操作系统。

3.进程等待

3.1如何理解

3.1.1是什么,怎么做

通过系统调用接口 wait/waitpid ,来对子进程进行状态检查和回收的功能,避免产生僵尸进程。

3.1.2必要性

1.避免内存泄漏

子进程退出,父进程如果不读取子进程的退出信息,子进程会变成僵尸进程。僵尸进程是一个死掉的进程,即使使用kill -9命令也无法将其杀死,因为无法杀死一个死掉的进程。所以必须通过进程等待解决问题。

2.父进程通过进程等待的方式,回收子进程资源,获取子进程退出情况。

如此父进程才能知道,子进程运行是否完成,结果对还是不对,或者是否正常退出。

3.2等待方法

3.2.0获取子进程status

下面提到的两种方法都会有一个输出型参数:status.

status 参数是 wait() 系统调用的一个 输出型参数,用于获取子进程的退出状态信息。它是一个指向 int 的指针(int*),如果传递NULL,表示不关心子进程的退出状态信息。否则系统会将子进程的终止状态写入该地址,将子进程退出信息反馈给父进程。

status不能简单当作整型看待,可以看作位图看待,具体如下图(只研究低16比特位):

正常退出,高8位表示进程退出状态,也就是退出码。

被信号所杀,低7位表示终止信号,第8位是core dump标志。

我们可以通过位操作,获得这些信号,比如:
exitCode = (status >> 8) & 0xFF; //退出码

exitSignal = status & 0x7F; //退出信号

系统中定义了两个宏,来获取退出码和退出信号

WIFEXITED(status):判断子进程是否正常退出。

WEXITSTATUS(status):当WIFEXITED(status)为真时,获取子进程退出码。

3.2.1wait

阻塞父进程,等待任意一个子进程终止。

#include<sys/types.h>
#include<sys/wait.h>
pid_t wait(int* status);

返回值:
成功返回被等待进程pid,失败返回-1。
参数:
输出型参数,获取⼦进程退出状态,不关⼼则可以设置成为NULL

例如父进程可以一直等待子进程,等待到子进程退出后读取退出信息。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
int main()
{
	pid_t id = fork();
	if(id == 0){
		//child
		int count = 10;
		while(count--){
			printf("I am child...PID:%d, PPID:%d\n", getpid(), getppid());
			sleep(1);
		}
		exit(0);
	}
	//father
	int status = 0;
	pid_t ret = wait(&status);
	if(ret > 0){
		//wait success
		printf("wait child success...\n");
		if(WIFEXITED(status)){
			//exit normal
			printf("exit code:%d\n", WEXITSTATUS(status));
		}
	}
	sleep(3);
	return 0;
}

3.2.2waitpid

更灵活的版本,可以等待指定子进程。

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: 默认为 0 ,表⽰阻塞等待
若设置为WNOHANG: 进入非阻塞模式, pid 指定的⼦进程没有结束,则 waitpid() 函数返回 0 ,不予以等待。若正常结束,则返回该⼦进程的ID

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

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

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

3.3非阻塞轮询

上述所给的例子中,如果子进程一直不退出,父进程就会一直等待子进程结束,在这期间不做任何事,这称为阻塞等待。(通过这我们也可以知道,阻塞并不只发生在向硬件发送请求等待硬件状态准备好,也可以发生在父进程等待子进程结束获取状态)

实际上我们可以让父进程并不一直等待,在子进程退出之前可以做点别的事,等到子进程真正退出了再获取退出信息,处理对应逻辑。

做法很简单,就是使用waitpid,将第三个参数传WNOHANG,这样等待进程如果没有结束,就直接返回0,不等了,去干别的事。如果正常结束,就返回子进程PID。

例如,我们可以通过while函数,每隔一段时间就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){
			printf("wait child success...\n");
			printf("exit code:%d\n", WEXITSTATUS(status));
			break;
		}
		else if (ret == 0){
			printf("father do other things...\n");
			sleep(1);
		}
		else{
			printf("waitpid error...\n");
			break;
		}
	}
	return 0;
}

输出结果如下(*^_^*)

3.3.0子进程对父进程数据并发备份 代码模型

3.4多进程创建与等待 代码模型

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
	pid_t ids[10];
	for (int i = 0; i < 10; i++){
		pid_t id = fork();
		if (id == 0){
			//child
			printf("child process created successfully...PID:%d\n", getpid());
			sleep(3);
			exit(i); //将子进程的退出码设置为该子进程PID在数组ids中的下标
		}
		//father
		ids[i] = id;
	}
	for (int i = 0; i < 10; i++){
		int status = 0;
		pid_t ret = waitpid(ids[i], &status, 0);
		if (ret >= 0){
			//wait child success
			printf("wiat child success..PID:%d\n", ids[i]);
			if (WIFEXITED(status)){
				//exit normal
				printf("exit code:%d\n", WEXITSTATUS(status));
			}
			else{
				//signal killed
				printf("killed by signal %d\n", status & 0x7F);
			}
		}
	}
	return 0;
}

运行后发现,父进程同时创建了这些子进程,然后依次读取退出子进程的退出信息

4.进程替换

fork()之后父子进程各自执行 父进程的部分代码,如果想要子进程执行一个全新的程序,就需要进程替换来完成!

进程替换是通过特定的接口,加载磁盘上的一个全新的程序(代码和数据),加载到调用进程的地址空间。

4.1替换原理

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

4.2替换函数

其实有六种以exec开头的替换函数,统称exec函数:

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

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

如果调用出错则返回-1

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

4.2.1命名解释

这些函数原型看起来很容易混,但只要理解后缀含义就很好理解:

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

v (vector):参数使用数组

p (path):表示能自动搜索环境变量

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

实际上只有execve是真正的系统调用,其他五个函数最终调用的都是execve(execve在man2,其他在man3)说明其他五个函数其实是对execve系统调用进行了封装以满足不同用户的不同调用场景。

4.2.2函数解释

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

第一个参数是程序执行的路径,第二个参数是一个可变参数列表,表示你要如何执行这个程序,以NULL结尾。

比如执行ls程序

execl("/usr/bin/ls", "ls", "-a", "-i", "-l", NULL);

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

第一个参数是执行程序的名字,其他同上

执行ls程序例子:

execlp("ls", "ls", "-a", "-i", "-l", NULL);

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

第三个参数是你自己设置的环境变量,以NULL结尾。

例如,你设置了MYVAL环境变量,在mycmd程序内部就可以使用该环境变量。

char* myenvp[] = { "MYVAL=2021", NULL };
execle("./mycmd", "mycmd", NULL, myenvp);

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

第二个参数是一个指针数组,数组当中的内容表示你要如何执行这个程序,数组以NULL结尾。

比如执行ls程序(反复鞭尸bushi)

char* myargv[] = { "ls", "-a", "-i", "-l", NULL };
execv("/usr/bin/ls", myargv);

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

执行ls程序

char* myargv[] = { "ls", "-a", "-i", "-l", NULL };
execvp("ls", myargv);

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

例如,你设置了MYVAL环境变量,在mycmd程序内部就可以使用该环境变量。

char* myargv[] = { "mycmd", NULL };
char* myenvp[] = { "MYVAL=2021", NULL };
execve("./mycmd", myargv, myenvp);

5.实现Shell命令行解释器

shell也就是命令行解释器,其运行原理就是:当有命令需要执行时,shell创建子进程,让子进程执行命令,而shell只需等待子进程退出即可。

思考下面这个shell典型的互动:

[root@localhost epoll]# ls
client.cpp readme.md server.cpp utility.h
[root@localhost epoll]# ps
PID TTY TIME CMD
3451 pts/0 00:00:00 bash
3514 pts/0 00:00:00 ps

用下图的时间轴来表示事件发生次序。其中时间从左向右。shell由标识符为bash的方块代表,它随时间流逝从左向右移动。shell从用户读入字符串“ls”。shell建立一个新的进程,然后在那个新的进程中运行ls程序并等待那个进程结束。

然后shell读取新的一行输入,建立一个新的进程,在这个进程中运行程序 并等待这个进程结束。所以要写一个shell,需要循环以下过程:

1.获取命令行

2.解析命令行

3.建立一个子进程(fork)

4.替换子进程(execvp)

5.父进程等待子进程退出(wait)

根据这些思路,和我们前面学的技术,就可以实现一个shell了。

代码实现如下:

myshell.h

  1 #ifndef __MYSHELL_H__
  2 #define __MYSHELL_H__
  3 #include<iostream>
  4 #include<stdlib.h>
  5 #include<string.h>
  6 #include<cstdio>
  7 #define ARGS 64
  8 extern char *gargv[];
  9 void Debug();
 10 void InitGlobal();
 11 void PrintCommandPrompt();
 12 bool GetCommandString(char cmd_str_buff[],int len);
 13 bool ParseCommandString(char cmd[]);
 14 void ForkAndExec();
 15 bool BulitInCommandExec();                                                     
 16 #endif

myshell.cc

  1 #include"myshell.h"
  2 #include<iostream>
  3 #include<stdlib.h>
  4 #include<string.h>
  5 #include<string>
  6 #include<cstdio>
  7 #include<unistd.h>
  8 #include<sys/types.h>
  9 #include<sys/wait.h>
 10 //命令行参数表,故意定义为全局
 11 //用来存命令
 12 char *gargv[ARGS]={NULL};
 13 int gargc=0; 
 14 char pwd[1024];//保存当前shell进程的工作路径
 15 int lastcode=0;
 16 
 17 void Debug()
 18 {
 19   printf("hello shell!\n");
 20 }
 21 static std::string GetUserName()
 22 {
 23   std::string username=getenv("USER");
 24   return username.empty()?"None":username;
 25 }
 26 static std::string GetHostName()
 27 {
 28   std::string hostname=getenv("HOSTNAME");
 29   return hostname.empty()?"None":hostname;
 30 }
 31 static std::string GetPwd()
 32 {
 33   //getenv("PATH")获取的是环境变量的路径,chdir时不会改>    变进程环境变量表,也就是chdir不会改变环境变量pwd,需要sh    ell自己更新环境变量的值
 34   //std::string pwd = getenv("PWD");
 35   //return pwd.empty()?"None":pwd;                      
 36   char temp[1024];
 37   getcwd(temp,sizeof(temp));
 38   //更新pwd
 39   snprintf(pwd,sizeof(pwd),"PWD=%s",temp);
 40   //设置环境变量
 41   putenv(pwd);
 42   //设置命令行显示格式
 43   std::string pwd_lable=temp;
 44   const std::string pathsep="/";
 45   auto pos=pwd_lable.rfind(pathsep);                    
 46   if(pos==std::string::npos)
 47   {
 48     return "None";
 49   }
 50 
 51   pwd_lable=pwd_lable.substr(pos+pathsep.size());
 52   
 53   return pwd_lable;
 54 }
 55 static std::string GetHomePath()
 56 {
 57     const char* home= getenv("HOME");
 58     return home==nullptr?"/":home;
 59 }
 60 //输入提示符
 61 void PrintCommandPrompt()
 62 {
 63     
 64     std::string user =GetUserName();
 65     std::string hostname=GetHostName();
 66     std::string pwd=GetPwd();
 67     printf("[%s@%s %s]# ",user.c_str(),hostname.c_str(),    pwd.c_str());
 68 }
 69 //获取键盘输入
 70 bool GetCommandString(char cmd_str_buff[],int len)
 71 {
 72     if(cmd_str_buff==NULL||len<=0)return false;
 73     char *res=fgets(cmd_str_buff,len,stdin);
 74     if(res==NULL)return false;
 75     // ls -a -l\n -> ls -a -l
 76     cmd_str_buff[strlen(cmd_str_buff)-1]=0;
 77     return true;
 78 }
 79 //解析命令
 80 bool ParseCommandString(char cmd[])
 81 {
 82   
 83    if(cmd==NULL)return false;
 84 #define SEP " "                                         
 85    gargc = 0; //别忘重置
 86 
 87    //"ls -a -l"->"ls""-a""-l"
 88    gargv[gargc++]=strtok(cmd,SEP);
 89    
 90    while((bool)(gargv[gargc++]=strtok(NULL,SEP)));
 91    //回退一次命令行参数个数,把NULL删了
 92    gargc--;
 93    return true;
 94 
 95 }
 96 void InitGlobal()
 97 {
 98   gargc=0;
 99   memset(gargv,0,sizeof(gargv));
100 
101 }
102 void ForkAndExec()
103 {
104    pid_t id=fork();
105   if(id<0)
106   { 
107     perror("fork");
108     return;
109   }
110   else if(id==0)
111   {
112     //子进程
113     execvp(gargv[0],gargv);
114     exit(0);
115   }
116   else
117   {
118     //父进程
119     int status=0;
120     pid_t rid=waitpid(id,&status,0);
121     if(rid>0)
122     {
123       lastcode=WEXITSTATUS(status);
124     }
125   }
126 }                                                       
127 //shell执行内建命令
128 bool BulitInCommandExec()
129 {
130   std::string cmd=gargv[0];
131   bool ret=false;
132   if(cmd=="cd")
133   {
134     if(gargc==2)
135     {
136       std::string target=gargv[1];
137       if(target=="~")
138       {
139         ret=true;
140         chdir(GetHomePath().c_str());
141       }
142       else
143       {
144        ret=true;
145        chdir(gargv[1]);
146       }
147     }
148     else if(gargc==1)
149     {
150       ret=true;
151       chdir(GetHomePath().c_str());
152     }
153     else
154     {
155       //BUG
156     }
157   }
158   else if(cmd=="echo")
159   {
160     if(gargc==2)
161     {
162       std::string args=gargv[1];
163         if(args[0]=='$')
164         {
165           if(args[1]=='?')
166            {
167              printf("lastcode:%d\n",lastcode);
168              lastcode=0;
169              ret = true;
170            }
171           else
172           {
173             const char*name=&args[1];
174             printf("%s\n",getenv(name));
175             lastcode=0;
176             ret=true;
177           }
178         }
179       else{
180         printf("%s\n",args.c_str());
181         ret=true;
182       }
183     }
184   }
185   return ret;
186 }

main.cc

  1 #include"myshell.h"
  2 #define SIZE 1024
  3 
  4 int main()
  5 {
  6   char commandstr[SIZE];
  7   while(true)
  8   {
  9     //0.初始化操作
 10      InitGlobal();
 11      //1.输入命令行提示符
 12      PrintCommandPrompt();
 13      //2.获取用户输入的命令
 14      if(!GetCommandString(commandstr,SIZE))cont    inue;
 15      //3.解析
 16      ParseCommandString(commandstr);
 17      //4.检查命令,内建命令,要让shell自己执行
 18      if(BulitInCommandExec())
 19      {
 20         continue;
 21      }
 22      //5.执行命令,让子进程来执行               
 23      ForkAndExec();
 24      
 25   }
 26   return 0;
 27 }

此篇完

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值