进程创建
fork函数初始
fork函数是从一个已存在的进程中创建一个新进程,原进程为父进程。
头文件:#include<unistd.h>
函数原型:pid_t fork(void);
返回值:子进程返回0,父进程返回子进程id,出错则返回-1。
进程调用fork,当控制转移到内核中的fork代码后,内核做:
分配新的内存块和内核数据结构给子进程
将父进程部分数据结构内容拷贝至子进程
添加子进程到系统进程列表中
fork返回,开始调度器调度。
当一个进程调用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;
}
运行结果:
这里看到了三行输出,一行before,两行after。进程43676先打印before消息,然后它有打印after。另一个after消息有43677打印的。注意到进程43677没有打印before,为什么呢?如下图所示
所以fork之前父进程独立执行,fork之后父子两个执行流分别执行。注意,fork之后谁先执行完全由调度器决定。
写时拷贝
通常操作系统为了节省空间,代码加载到内存中是只读的,所以父子进程代码是共享的。由于进程的独立性,所以数据是各自独立的,但是如果在创建子进程时就把数据完全拷贝过去的话,会存在有之后才会用到的数据和根本不用的数据也会拷贝过去存在空间浪费,所以操作系统做了一件写时拷贝东西。
写时拷贝:当父子进程任意一方试图写入数据时,那么就会将要写入的部分以写时拷贝的方式各自一份。
fork常规法
一个父进程希望复制自己,使父子进程同时执行不同的代码段。
一个进程要执行不同的程序。
fork调用失败的原因
系统中有太多的进程
实际用户的进程数超过了限制
进程终止
进程退出场景
代码运行完毕,结果正确。
代码运行完毕,结果错误
代码异常终止
进程常见退出方式
正常退出(echo $? 可以查看退出码)
从main返回(return),而·从非main函数中调用表示函数返回。
调用exit()
调用_exit()
异常退出
ctrl+c,x信号终止
程序崩溃
异常退出退出码也就没有意义了
_exit函数
头文件:#include<unistd.h>
函数原型:void _exit(int status);
参数:status就是进程退出码,定义了进程的终止状态,父进程通过wait来获取该值
说明:虽然status是int,但是仅有低8位可以被父进程所用。所以_exit(-1)时,在终端执行echo $?发现返回值是255。
exit函数
头文件:#include<unistd.h>
函数原型:void _exit(int status);
参数:status就是进程退出码,定义了进程的终止状态,父进程通过wait来获取该值
exit最后也会调用exit, 但在调用exit之前,还做了其他工作:
执行用户通过 atexit或on_exit定义的清理函数。
关闭所有打开的流,所有的缓存数据均被写入
调用_exit
return退出
return是一种更常见的退出进程方法。执行return n等同于执行exit(n),因为调用main的运行时函数会将main的返回值当做 exit的参数。
exit、_exit、return退出的区别
exit与return退出是一样的,但是不论程序在哪调用exit进程都会直接退出,return如果在main函数之外的函数调用的话进程不会退出,而是返回一个值给它的调用。
exit与_exit也是基本一样的,区别就是exit会完成进程的收尾工作包括输入缓冲区刷新;__exit 强制终止进程,不会进行进程的后续收尾工作。
例:
int main()
{
printf("hello");
exit(0);
}
运行结果: [root@localhost linux]# ./a.out hello
int main()
{
printf("hello");
_exit(0);
}
运行结果: [root@localhost linux]# ./a.out
进程退出操作系统都做了什么
系统层面,少了一个进程,会释放相关的PCB,虚拟地址空间(mm_struct),页表和各种映射关系,代码和数据在内存中申请的空间也要释放掉。
在思考一个问题进程退出码是给谁的,退出码返回值代表了什么?引出进程等待
进程等待
进程等待的必要性
之前讲过,子进程退出,父进程如果不管不顾,就可能造成‘僵尸进程’的问题,进而造成内存泄漏。
另外,进程一旦变成僵尸状态,那就刀枪不入,“杀人不眨眼”的kill -9 也无能为力,因为谁也没有办法杀死一个已经死去的进程。
最后,父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是不对,或者是否正常退出。
父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息
进程等待的方法
wait方法
头文件:#include<sys/types.h>、#include<sys/wait.h>
函数原型:pid_t wait(int*status);
返回值:成功返回被等待进程pid,失败返回-1。
参数:status:输出型参数,获取子进程退出状态,不关心则可以设置成为NULL,在下面的wiatpid中讲解。
例:
#include <iostream>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/wait.h>
using namespace std;
int main()
{
pid_t id = fork();
if(id == 0){
int count = 0;
while(1){
sleep(1);
cout <<"child...:" << count << endl;
if(count >= 5){
break;
}
count++;
}
exit(0);
}
else{
cout <<"father before..." << endl;
wait(NULL); //阻塞等待,等待子进程退出
cout <<"father after..." << endl;
}
cout << "hello world"<< endl;
}
waitpid方法
-
函数原型: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: WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的ID,白话就是子进程没有退出的话,父进程干自己的事,但是会重复调用waitpid方法,看子进程有没有退出。非阻塞等待。
阻塞等待的本质:其实是进程PCB被放入了等待队列,并将进程的状态设置为S状态。
返回(唤醒)的本质:进程的PCB等待队列被拿到运行队列,从而被CPU调度。
注意:
-
如果子进程已经退出,调用wait/waitpid时,wait/waitpid会立即返回,并且释放资源,获得子进程退出信息。
-
进程退出并不会立即释放进程相关数据结构,而是等待父进程对其进行回收。
-
子进程的退出信息会保存在它的PCB中,最终会被父进程的status输出性参数取走,从而父进程获取子进程的退出信息。
-
bash是命令行启动的所有进程的父进程!
-
bash一定是通过wait()方法得到子进程的退出信息,所以我们可以通过echo $?可以查出子进程的退出码。
-
如果在任意时刻调用wait/waitpid,子进程存在且正常运行,则进程可能阻塞。
-
如果不存在该子进程,则立即出错返回。
获取子进程status
wait和waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充。
如果传递NULL,表示不关心子进程的退出状态信息。
否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程。
status不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究status低16比特位)
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<sys/wait.h>
#include<errno.h>
int main(void)
{
pid_t pid;
if((pid=fork())==-1)
perror("fork"),exit(1);
if(pid==0)
{
sleep(5);
exit(10);
}
else{
int st;
int ret=wait(&st);
if(ret>0&&(st&0x7F)==0)
{
//正常退出
printf("child exit code:%d\n",(st>>8)&0xFF);
//注意这块是为了方便理解而&0xFF的,通常我们自己使用WEXITSTATUS(status)获取子进程的退出码,WIFEXITED(status)判断进程是否正常退出。
}
else if(ret>0)
{
//异常退出
printf("sig code :%d\n",st&0x7F);
}
}
}
进程程序替换
怎么理解程序替换
创建子进程就是为了执行父进程交给他的任务,之前我们都是通过if else让子进程执行父进程代码的一部分;但是如果我们不想让子进程执行父进程的一部分,而是想让它执行一个全新的代码呢?下面引入进程程序替换的概念。
程序替换的本质就是将硬盘中的代码和数据拷贝一份来替换掉进程之前的全部代码(写时拷贝),这个过程并不会创建新进程。
相关的替换函数
#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,co;nst char* argv[]); int execvp(const char*file,char* const argv[]); int execve(const char* path,char* const argv[],char* const envp[]);
1、函数调用成功就会加载新的程序从启动代码往后执行,不在返回。
2、调用出错返回-1。
3、exec出错的时候才有返回值
理解:
-
l(list):表示函数采用参数列表
-
v(vector):参数用数组
-
p(path):有p自动搜索环境变量path
-
e(env):表示自己维护环境变量
函数理解
execl函数
int execl(const char* path,const char* arg,...);
参数解释:
char* path:你想要替换可执行程序文件的文件路径。
char* arg:你想要在命令行怎么调用替换后的可执行程序。
... :可变参数,比如你替换的是ls命令,那么...就是在ls命令的各种参数,你可以在后面啥也不跟,也可以将参数都跟上,这就是可变参数的作用。
接下来我们通过一个例子来了解execl函数
#include<stdio.h>
#include<stdlid.h>
#include<unostd.h>
int main()
{
printf("execl之前\n");
int ret=execl("./hello","./hello",NULL);
printf("%d\n",ret);
return 0;
}
运行结果: execl之前 hello world
可以看出在这块并没有打印ret的值,这是因为execl函数执行后已经用hello的可执行程序替换了原有程序,那么有些人就会好奇到底是替换了全部程序还是只替换了execl函数之后的程序呢?答案是替换了全部的程序,之所以会打印“execl执行之前”这个语句是因为还没有运行到execl。ret是在程序替换失败后才会打印出来的,在替换成功时打印ret的语句会被覆盖所以就不会打印。
execv函数
int execv(const char* path,const char* argv[]);
参数解释:
char* path :你想要替换可执行程序文件的文件路径。
char* argv[]: 参数数组。
例如:
#include<stdio.h>
#include<sdtlib.h>
#include<unistd.h>
int main()
{
char* argv[]={
"ls","-l","-t","-r",NULL
};
execv("/bin/ls",argv);
return 0;
}
这个函数本质与execl没有差别,只是execl函数参数是以列表形式传参的,而execv是以数组方式传参的。
execle函数
int execle(const char* path,const char* arg,...,char* const envp[]);
参数解释:
char* path:你想要替换可执行程序文件的文件路径,所谓的环境变量。
char* arg:你想要在命令行怎么调用替换后的可执行程序
... :可变参数,比如你替换的是ls命令,那么...就是在ls命令的各种参数,你可以在后面啥也不跟,也可以将参数都跟上,这就是可变参数的作用。
char* envp[]:假如这块是用子进程替换父进程。利用子进程里面打印环境变量,然后父进程结合子进程来执行,运行子进程就会有很多环境变量(由于子进程继承了bash的环境变量),再执行父进程就只有我们自己写的环境变量,这是因为execle函数替换时自己构造环境变量并不继承原有的环境变量
例如:
//hello.c文件
#include<stdio.h>
int main(int argc,char* argv[],char* env[])
{
(void)argc;
(void)argv;
size_t i=0;
for(;env[i]!=NULL;i++)
{
printf("%s\n",env[i]);
}
return 0;
}
//exec.c文件
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
int main()
{
const char* env[]={"hehehe",NULL};
execle("./hello","hello",NULL,env);
return 0;
}
运行后你会发现环境变量变为hehehe。
execve函数差别不大自己结合v,l差别推导
execlp函数
int execlp(const char* file,const char* arg,...);
参数解释:
char* file: 你要是使用那个程序文件替换当前文件的文件名。
char* arg:你想要在命令行怎么调用替换后的可执行程序
... :可变参数,比如你替换的是ls命令,那么...就是在ls命令的各种参数,你可以在后面啥也不跟,也可以将参数都跟上,这就是可变参数的作用。
例如:
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
int main()
{
execlp("ls","ls","-l","-t",NULL);
return 0;
}
execvp函数自己推导,这里就不做解释了。
exec函数族例子
其中只有execve是系统调用 。
实例(miniShell)
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#define SIZE 256
#define NUM 16
int main()
{
char cmd[SIZE];
const char* cmd_line="[temp@VM-0-3-centos linux]#";
while(1)
{
cmd[0]=0;
printf("%s",cmd_line);
fgets(cmd,SIZE,stdin);
cmd[strlen(cmd)-1]='\0'; //注意这个是必须的,不加\0的话会出错。
char *args[NUM];
args[0]=strtok(cmd," ");
int i=1;
do{
args[i]=strtok(NULL," ");
if(args[i]==NULL)
break;
++i;
}while(1);
//接下来创建子进程,让子进程完成解析工作
pid_t id=fork();
if(id<0){
perror("fork error\n");
continue;
}
//子进程
if(id==0){
execvp(args[0],args);
exit(1);
}
//获取退出码
int status=0;
pid_t ret =waitpid(id,&status,0);
if(ret>0){
printf("status code: %d\n",(status>>8)&0xff);
}
}
return 0;
}