fork函数创建进程,新进程为子进程,原进程为父进程;
fork函数包含在头文件 <unistd.h>
进程调用fork,当控制转移到内核中的fork代码后,内核做:
关于fork函数的返回值:
子进程和父进程共享fork函数之后的代码
实例演示:
int main()
{
printf("before: pid: %d\n",getpid());
pid_t id=fork();
printf("after:\n");
if(id==0)
{
//子进程
printf("我是子进程 pid: %d ppid: %d\n",getpid(),getppid());
}
else if(id>0)
{
//父进程
printf("我是父进程 pid:%d ppid: %d\n",getpid(),getppid());
}
else
{
printf("出错\n");
}
return 0;
}
进程退出时的三种情况:
进程退出的常用方法
那么谁会关心一个进程的运行情况呢?
答案是父进程。子进程在退出时,会成为僵尸进程,需要父进程的回收。
那么父进程期望获得子进程退出时得哪些信息呢?
可以通过查看退出码,来知晓进程的退出情况
可以用以下命令查看最后一次进程退出的退出码
echo $?
我们可以打印一下,每个错误码对应着什么错误信息
int main()
{
for(int i=0;i<100;i++)
{
printf("%d : %s\n",i,strerror(i));
}
return 0; }
可以发现,错误码为0时,代表代码正常执行完毕,所以我们平时主函数里的return 都是return 0
当然我们也可以自己设计一套错误码体系。
exit 和 _exit 都可以退出进程,但是exit在退出进程前会做其它工作:
而 _exit 是直接退出进程,所以缓冲区绝对不在内核。
所以一般推荐使用 exit 函数来退出进程。
return是一种更常见的退出进程方法。执行return n等同于执行exit(n),因为调用main的运行时函数会将main的返回值当做 exit的参数。
通过系统调用wait/waitpid,来进行对子进程进行状态检测与回收的功能!
我们知道子进程在退出时会变成僵尸进程:
父进程通过调用 wait/waitpid 进行僵尸进程的回收问题!
查看 man 手册 ,wait 函数所在的头文件是 <sys/types.h> 和 <sys/wait.h>
返回值: 成功返回被等待进程pid,失败返回-1。 参数: 输出型参数,获取子进程退出状态,不关心则可以设置成为NULL.
wait等待的是任意一个子进程
实例:
#include <sys/types.h>
#include <sys/wait.h>
void Run()
{
int cnt=5;
while(cnt)
{
printf("我是一个子进程 pid: %d ppid: %d\n",getpid(),getppid());
cnt--;
sleep(1);
}
}
int main()
{
pid_t id=fork();
if(id==0)
{
//子进程
Run();
exit(0);
}
else if(id>0)
{
//父进程
int cnt=10;
while(cnt)
{
printf("我是一个父进程 pid: %d ppid: %d\n",getpid(),getppid());
cnt--;
sleep(1);
}
pid_t ret=wait(NULL);
}
else
printf("出错\n");
return 0;
}
可以用下面的指令查看运行时进程的变化
while :; do ps ajx | head -1 && ps ajx | grep testwait | grep -v grep;sleep 1;echo "------------------------"; done
waitpid 函数一共有三个参数
pid: pid=-1,等待任何一个子进程。与wait等效。 pid>0,等待其进程ID与pid相等的子进程。 status: WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出) WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码) options: WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的ID。
pid很容易理解,这里重点讲讲 status。
status 是一个输出型参数,它的类型是 int ,说明有32个比特位
低7位用来表示终止信号 第8位表示 core dump标志 第9位到第15表示退出状态,也就是说正常退出为0,异常退出为非0 所以除了上面的使用宏函数来访问status的退出码什么的还可以用下列方式访问 status 获取终止信号:status&0x7f 获取退出码: (status>>8)&0xff
我们为什么要传一个输出型参数呢?可不可以使用全局变量代替这个输出型参数 status?
答案是不可以!因为进程之间具有独立性。
等待的原理:
其实子进程在退出的时候,会把退出码,终止信号写入到PCB的 exit_code 和 exit_signal 变量中,等待进程时,也就是从子进程的PCB中读取这两个变量的值,并写入到输出型变量 status 中,这样父进程就可以知道子进程的退出信息了。
int exit_code;
int exit_signal;
waitpid的第三个参数 options 为0时,表示当子进程一直没有退出的时候,父进程处于阻塞等待。
什么是阻塞等待?
即在子进程退出前,父进程什么也不做,一直在等着子进程退出,此时父进程处于阻塞状态。
当waitpid的第三个参数 options 为 WNOHANG ,父进程以非阻塞轮询的方式等待子进程。
什么是非阻塞轮询?
即父进程会检查一次看子进程有没有退出,没有则返回0,此时父进程可以做一些自己的事,而不是一味的等待子进程的退出,在子进程退出前循环以上的过程,直到子进程退出,返回 >0 的一个数,返回负数则表示等待失败。
实例:
int main()
{
pid_t id = fork();
if (id == 0)
{
//子进程
int cnt = 5;
while (cnt)
{
printf("我是一个子进程 pid: %d ppid: %d\n", getpid(), getppid());
cnt--;
sleep(1);
}
}
else if (id > 0)
{
//父进程
int status = 0;
while (1) //轮询
{
pid_t ret = waitpid(-1, &status, WNOHANG); //非阻塞
if (ret > 0)
{
printf("子进程退出,等待成功\n");
break;
}
else if (ret == 0)
{
printf("你先等等,子进程还没有退出....\n");
sleep(1);
}
else
{
printf("等待失败\n");
break;
}
}
return 0;
}
}
在理解什么是进程替换之前,我们先来看看进程替换怎么使用,下面是操作系统提供的进程替换的一些函数
参数所表达的意思:
以上的这些函数中,只有execve是系统调用,其它函数在底层都会调用这个函数。
对于像execl 和 execlp 有可变参数的函数,其实它们的使用方法很简单,从第二个参数开始,参数的写法就很我们在命令行中的一样,且最后一个参数是NULL。
可以想想,当我们要执行一个程序时,第一件事是什么?
第一件事就是要先找到这个程序,找到程序后做什么?
第二件事就是你得知道要怎么执行这个程序。
这样就能更好的理解这些函数为什么要这么用了
例如命令行中输入 ls -l -a (以单进程的进程替换来演示)
int main()
{
execl("/usr/bin/ls","ls","-l","-a",NULL);
return 0;
}
下面是其它进程替换函数的一些用法
int main()
{
extern char**environ;
execl("/usr/bin/ls","ls","-l","-a",NULL);
char*const myargv[]={"ls","-l","-a",NULL};
execv("/usr/bin/ls",myargv); //v表示数组
execvp("ls",myargv); //有p的可以省去路径
execvpe("ls",myargv,environ); //有e的可以自己控制环境变量,且采用的策略是覆盖而不是追加
return 0;
}
先来看这样一段代码:
int main()
{
printf("before:\n");
execl("/usr/bin/ls","ls","-l","-a",NULL);
printf("after:\n");
return 0;
}
打印结果会是什么?
发现只打印了before ,after呢?也就是 execl 前面的代码会被执行,后面的代码不会被执行,这是为什么?
进程替换的原理:
进程在替换时,只会替换掉物理内存中原来程序的代码和数据,其它的并不会动,且调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。
所以execl函数执行完后,原来的代码和数据就被替换了,物理内存中是全新的代码和数据,也就不是原来的代码,所以execl后的代码不会被执行,除非execl函数调用失败。
那么环境变量也是数据,它会被替换吗?
不会!!因为创建子进程的时候,环境变量已经被子进程继承下去了,所以进程替换不会替换环境变量。
前面的例子是单进程的执行系统命令的进程替换,接下来我们实现一个多进程的执行自己命令的进程替换。
int main()
{
pid_t id=fork();
if(id==0)
{
execl("./other1exe","./other1exe",NULL);
}
else if(id>0)
{
wait(NULL);
execl("./other2exe","./other2exe",NULL);
}
return 0;
}
对于有多个目标的makefile文件,可以这样写,就可以一次生成所需要的全部文件。
.PHONY:all
all: other1exe other2exe test
other1exe:other1exe.cpp
g++ -o $@ $^ -std=c++11
other2exe:other2exe.cpp
g++ -o $@ $^ -std=c++11
test:test.c
gcc -o $@ $^ -std=c99
.PHONY:clean
clean:
rm -rf test other1exe other2exe
定义一个伪目标 all ,all 的依赖文件就是你所需要生成的文件。
打印结果:
有了以上的这些知识后,我们就可以自己制作一个简易的myshell了,使它也能像shell那么使用。
这里要特别注意环境变量的维护!
因为在linux中,环境变量的内容是在一个区域放着的,而环境变量表 env 中存的是环境变量的地址,这些地址指向所对应的环境变量; 而我们putenv一个环境变量时,其实是在环境变量表中找一个未使用的下标,把要导入的环境变量的地址放进去,这个地址就指向导入的环境变量的内容。 所以当我们要put环境变量时,只是将它的地址填入了环境变量表中,而环境变量的内容是由我们自己输入的,在我们自己创建的命令行参数表中,而这个命令行参数表是会变的,但环境变量表依然指向不变,但是其实所指向的内容已经变了,所以就会导致导入环境变量不成功。 为解决上述问题,我们需要自己创建空间,用来专门维护环境变量。
#define SIZE 1024
#define ARGV_SIZE 32
#define DELIM " \t"
#define EXIT_CODE -1
#define NONE -1
#define IN_RDIR 0
#define OUT_RDIR 1
#define APPEND_RDIR 2
int quit=0;
int lastcode=0;
char pwd[SIZE];
char commandline[SIZE];
char *argv[ARGV_SIZE];
char *rdirfilename=NULL;
int rdir=NONE;
//维护环境变量表
extern char**environ;
char myenv[SIZE];
const char*getusername() //获取用户名
{
return getenv("USER");
}
const char* gethostname() //获取主机名
{
return getenv("HOSTNAME");
}
void getpwd() //获取当前路径
{
getcwd(pwd,sizeof(pwd)); getcwd是系统调用接口
}
void interate(char *cline,int size) //交互
{
getpwd();
printf("[%s@%s %s]# ",getusername(),gethostname(),pwd);
char*s=fgets(cline,size,stdin);
assert(s); //检查是否输入成功
(void)s; //一些编译器会对未使用的变量报警告,这里防止这个情况发生
//abcd\n\0
cline[strlen(cline)-1]='\0'; //将最后读入的回车变成 '\0' ,使其符合C形式的字符串
}
int separationline(char *cline,char*_argv[]) //分割字符串
{
int i=0;
_argv[i++]=strtok(cline,DELIM); //注意strtok函数的用法
while(_argv[i++]=strtok(NULL,DELIM));
return i-1; //返回命令行参数表的大小
}
void normalcom(int _argc,char *_argv[]) //普通命令
{
pid_t id=fork();
if(id==0)
{
execvp(_argv[0],_argv); //通过进程替换来执行普通命令
exit(EXIT_CODE);
}
else if(id>0)
{
int status =0;
pid_t rid=waitpid(id,&status,0); //等待回收子进程
if(rid==id)
{
lastcode=WEXITSTATUS(status); //设置错误码
}
}
else
{
perror("fork");
return ;
}
}
int buildcom(int _argc,char*_argv[]) //内建命令
{
//cd export echo
if(_argc==2&&strcmp(_argv[0],"cd")==0)
{
chdir(_argv[1]);
getpwd();
sprintf(getenv("PWD"),"%s",pwd);
return 1;
}
else if(_argc==2&&strcmp(_argv[0],"export")==0)
{
strcpy(myenv,_argv[1]);
putenv(myenv);
return 1;
}
else if(_argc==2&&strcmp(_argv[0],"echo")==0)
{
if(strcmp(_argv[1],"$?")==0) //打印最后一次程序退出的错误码
{
printf("%d\n",lastcode);
lastcode=0;
}
else if(*_argv[1]=='$') //打印环境变量
{
char*val=getenv(_argv[1]+1);
if(val) printf("%s\n",val);
}
else //普通打印
{
printf("%s\n",_argv[1]);
}
return 1;
}
if(strcmp(_argv[0],"ls")==0) //特殊处理ls命令,为文件带上颜色,例如目录是蓝色的
{
_argv[_argc++]="--color";
_argv[_argc]=NULL;
}
return 0;
}
int main()
{
while(!quit)
{
//实现交互
interate(commandline,sizeof(commandline));
//分割命令
int argc=separationline(commandline,argv);
if(argc==0)
continue;
//执行命令
//内建命令
int n=buildcom(argc,argv);
//普通命令
if(!n) normalcom(argc,argv);
}
return 0;
}
扫码关注腾讯云开发者
领取腾讯云代金券
Copyright © 2013 - 2025 Tencent Cloud. All Rights Reserved. 腾讯云 版权所有
深圳市腾讯计算机系统有限公司 ICP备案/许可证号:粤B2-20090059 深公网安备号 44030502008569
腾讯云计算(北京)有限责任公司 京ICP证150476号 | 京ICP备11018762号 | 京公网安备号11010802020287
Copyright © 2013 - 2025 Tencent Cloud.
All Rights Reserved. 腾讯云 版权所有