Linux中进程的创建、进程的终止、进程的等待、进程的程序替换
进程的创建
在进程的创建中,我们一个非常重要的函数 fork()函数,fork()函数会创建一个新的进程,为原有进程的子进程,原有就为父进程。
我们来看一下fork()函数的原型。
#include <unistd.h>
pid_t fork(void);
返回值:子进程返回0,父进程返回子进程的pid。fork()失败返回-1(linux下)
进程调用fork()函数后具体的操作
1)当进程调用fork()函数后,会将控制转移到内核中的fork()代码。
2)内核会分配新的内存和内核数据结构给子进程
3)将父进程部分数据结构内容拷贝到子进程中(写时拷贝)
4)添加子进程到系统的进程列表中
5)fork()返回,开始调度器调度
第一步:
调用fork()函数后到内核中去申请资源。当资源申请好就创建出子进程,子进程和父进程各自拥有独立的资源,然后把父进程的数据代码拷贝一份到子进程中。但是要注意的是:这里的拷贝是采用了写时拷贝。通常情况下,父子进程是共享代码的,数据是写时拷贝。
当有了子进程时,代码是怎么执行的。
当fork()完了子进程和父进程就从fork()函数开始向下执行,但是父进程先执行还是子进程先执行,这是取决去调度器。
虚拟地址空间简述
我们在写程序的时候每次取地址或者传指针等,这些地址都是操作系统为了物理内存更为合理的使用,虚拟出地址空间。通过虚拟地址空间。
1)能有效的分配和使用物理内存
2)能保护进程与进程之间的独立
3)同时一定程度上提高了运行速度
我们了解一下,虚拟地址与物理地址
fork()创建子进程通常在一个进程下需要做别的事情是时,可以创建一个子进程去做,自己来处理结果。例如客户端请求父进程接受,子进程去处理。
创建子进程势失败的原因
1)系统中子进程数太多达到上限
2)内存不足
vfork()函数
vfork()函数也是用来创建子进程,但是与fork()相比还是有一定的差别 。
- vfork用于创建一个子进程,而子进程和父进程共享地址空间(也就是共享页表),而fork的子进程具有独立的地址空间
- vfork是一定保证了子进程先执行,当子进程调用exec或者(_exit)之后父进程才能被执行。fork父子进程调度完全取决于调度器,在同一时刻。
-
当子进程改变程序中数据时候,父进程也会改变。也是说明了共享地址空间
在这里有一个我们要注意:
在vfork函数创建的子进程中,为什么用return程序会崩溃。
前面我们说的第一点,因为vfork是父子共用一块地址空间,也就是用一个页表,所以在子进程运行的过程中父进程是处于等待状态的,要是在子进程中return,那么也就是将main函数的栈帧结束了,那么当子进程结束后,父进程开始运行,但是这个时候main函数的栈帧都已经释放,所以父进程再运行就会崩溃。进程的终止
进程的终止是一个进程的结束或者说是一个进程的退出,进程的退出可以分为两种。一种是是正常的退出,一种是异常的退出。
正常退出
进程的正常退出,大概有三种,严格的说只有两种。
1)main函数的返回
2)调用系统函数或者库函数(_exit() exit() )
main()函数返回
main()函数返回,这个很好理解,当main()函数执行到return 0 时候进程也正常退出。进程也就结束。就像我们写的第一个程序hello world ,当执行程序时,就会创建出一个进程,当return后,进程也就结束。
_exit()函数
先看函数声明:#include<unistd.h> // 系统调用 void _exit(int status); // 参数status为退出码。(注意退出码为int 但是用两个字节,后面说)
_exit()函数是系统函数,用于程序的退出,一般在进程中调用_exit()会直接结束程序,不会做进程结束前的清理工作。
exit()函数
函数说明:
#include<stdlib.h>
void exit(int status);
exit函数和_exit()函数功能都是让进程退出,而exit()函数在底层也是调用了_exit函数让进程退出,但是为什么要用exit函数呢?其实exit函数在调用的时候大概分为三个步骤
1)执行用户定义的清理函数
2)关闭所有打开的流,冲涮缓冲区。
3)调用_exit()函数
举个例子
int main()
{
printf("123");
exit(0);
return 0;
}
int main()
{
printf("123");
_exit(0);
return 0;
}
上面两个代码的运行结果是不一样的。
前一个是有输出为123,而后面的没有输出。
这就说明exit会在结束进程前做一些关闭文件,刷新缓冲区等事情。
异常退出
异常退出就是在进程在执行在某个一指令时收到了某一个信号,将程序终止,比如我们在执行过程中用的Ctrl+c这是表示在进程执行的过程中,进程收到了一个2号信号将程序终止掉。
还有在我们写程序的时候总会出现程序奔溃的情况,那就我们拿内存访问越界来说。当我们要访问某个地址空间的时候,因为我们现代的内存空间都用的是虚拟地址空间,所以操作系统会去查页表,然后找到对应的物理内存块,这时会有个mmu地址映射检查,如果映射在自己的内存空间中,则正常访问,否则,将发出11号信号,将程序终止掉。
进程的等待
什么是进程等待?为什么要进程等待?
进程的等待,因为一个进程在为了在它结束前需要等其它(子进程)进程完成一些事情,但是事情又没有完成,所以要进入等待状态,要等其它(子进程)完成,它才能继续执行或者结束。
为什么要进程等待,这个就比如,一个进程在创建出一个子进程后为了不让子进程变成僵尸进程,所以要进行进程的等待,等待接受子进程返回的结果。
那么进程中等待是怎么实现的?
wait方法
首先我们先看看wait方法
#include<sys/wait.h>
pid_t wait(int* status);
返回值:成功返回等待进程的pid,失败则返回-1
参数为输出型参数,如果不需要知道则可以传入NULL
wait 为阻塞式等待,意思就是如果一个子进程在没有结束前父进程都会在wait这里等着子进程的返回信息。
waitpid方法
先来看看函数的原型:
#include<sys/wait.h>
pid_t waitpid(pid_t pid, int* status, int options);
返回值
有三种情况:
1)当waitpid正常返回时,返回子进程的ID。
2)当参数中options的参数被设置为WNOHANG,而调用中waitpid发现没有已经退出的子进程可以收集,那么就返回0。
3)如果在调用中出错,那么返回-1,这是errorn会被置成相应的错误信息。
参数
pid:
pid值 = -1时,waitpid等待是任意一个子进程,与wait等待类似。
pid值 > 0 时,waitpid等待的子进程的ID(也就是进程pid)与参数pid相等的进程。
status:
参数为输出型参数。
注意:后面讲的输出型参数解析里面,参数虽然是int型,但是在用的时候只用两个字节的内容。所以为了方便使用,定义了两个宏。
WIFEXITED(status):若为正常终止的子进程返回的状态,则为真(非0),否则(0)。(主要用于判断子进程是否正常退出)解释:判断正常退出是判断低int的低15位是否全为0,而退出码是在高两个字节里面存储
WEXITSTATUS(status):·若WIFEXITED判断为非零(即子进程正常退出),那么它就可以提取正常子进程退出的退出码。
options
参数里面如果是0,那么就想wait一样是阻塞式等待,如果是WNOHANG,那么就是非阻塞式等待。
阻塞式等待:就是当子进程创建出来后,父进程在执行wait或者waitpid (-1,status,0),这时候如果子进程还在执行没有退出,那么父进程就会进入睡眠状态不会执行其他任务,等待子进程退出。
非阻塞式等待:当子进程创建出来后,父进程需要知道子进程的结果,调用waitpid(-1,status,WNOHANG)后,那么如果父进程发现子进程孩还在执行时,父进程不会进入睡眠状态,而是去执行其他事情,每过一段时间回来看看子进程是否执行完成,如果完成回收信息,没有则继续去做自己的事情。
#include <stdio.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
pid_t id = fork();
if (id > 0)
{
// 父进程
int s = 0;
do
{
int ret = waitpid(-1, &s, WNOHANG);
// 判断子进程是否正常退出
// 并且返回的是要处理的子进程
if (WIFEXITED(s) && ret == id)
{
printf("eixt coid :%d", WEXITSTATUS(s));
break;
}
else if (ret == 0)
{
// 程序还在执行
sleep(1);
printf("running\n");
}
else
{
// 等待失败
perror("waitpid error\n");
break;
}
}while(1);
}
else if (id == 0)
{
// 子进程
sleep(10);
printf("child over");
exit(123);
}
else
{
perror("fork\n");
}
return 0;
}
获取进程的status
关于status的参数解释:
- 在wait(int status)和waitpid(int status) 中的status是一个int的输出型参数,由操作系统来进程填充。
- 如果传递的是NULL,表示不关心子进程的退出状态。
- 改参数为输出型参数,但是参数具体由操作系统填充,所以操作系统会根据改参数,将子进程的退出信息反馈给父进程。
- 最重要的一点: status虽然是int型的参数,但是具体用,却只用低两个字节,就相当于位图。
首先我们说明为什么要低两个字节?
是因为,我们要表示正常状态,并且要表示出它的退出状态码,或者被信号所截杀的终止信号码。所以采用int只用低两位字节,一个字节表是进程是否正常退出,一个表示退出码。
我们用图来更清楚的认识一下:
我们再来理解一段代码:
#include <stdio.h>
#include <sys/wait.h>
#include <stdlib.h>
int main()
{
pid_t id = fork();
if (id > 0)
{
// 父
int w = 0;
// 如果取成功就返回子进程的pid
int ret = wait(&w);
if (ret > 0 && (w & 0x7f == 0))
{
// 正常退出
printf("子进程正常退出的退出码 = %d\n", w>>8);
}
else if (ret > 0)
{
// 异常退出
printf("异常终止的信号 = %d", w & 0x7f) // 只用后15个比特位
}
else
{
perror("wait");
}
}
else if (id == 0)
{
// 子
sleep(10); // 先睡10秒
exit(0);
}
else
{
perror("fork()");
}
return 0;
}
通过上面的代码我们可以得出,如果正常退出,我们就可以得到退出码,如果异常退出,我们就可以得到异常退出的信号。
进程的程序替换
首先要说明的是进程的程序替换是在程序运行过程中,当执行到exec函数时候,该进程的跑去执行其它的程序,就比如说:我们在用linux时候,我们会输入一个ls来查看当前目录底下有那些内容,那么对于我们来看是一瞬间的事情,但是在输入ls时,系统创建一个进程,用来调用ls的可执行程序,那么这个就是程序的替换。
替换的原理
从原理上来看我们程序替换,而不是进程的替换,那么我们就需要用当前进程去创建出子进程来进行程序的替换,往往子进程用来调用一个程序替换的函数,从而程序的子进程跑去执行,而父进程就处于等待状态,所以进程的程序替换不会改变进程pid。
我们用图来解释一下:
进程替换中需要注意
1)进程的替换只是替换了进程的代码数据,被替换的进程的id不会改变
2)进程替换在调用exec成功后,后面的代码将不会被执行到
3)在进程替换后,堆栈会清空原来进程的数据。
了解进程替换的函数
进程的替换函数以exec开头的家族:
它们的都在头文件为:
#incldue <unistd.h>
大致可以划分为两种类型,一种是参数列表,一种是数组型。
先来看看参数列表exec函数
// path为要执行的程序的路径,arg为参数列表(注意参数列表以NULL结尾)
int execl(const char* path, const char* arg,···);
// 这是自动的去环境变量中找path,file为可执行程序,arg参数列表
int execlp(const char* file, const char* arg, ···);
// path为执行程序路径,arg参数列表,envp为自己需要配置环境变量
int execle(const char* path,const char* arg,···,char* const envp[]);
用数组来装参数的exec函数
// path为执行程序的路径,argv数组为命令行参数
int execv(const char* path, char* const argv[]);
// file为执行程序名字,路径自动查找环境变量中的路径
int execvp(const char* file, char* const argv[]);
// path为可执行程序路径,argv装有命令行参数的数组,envp为需要自己来配置的环境变量。
int execve(const char* path, char* const argv[], char* const envp[]);
实现一个简单的shell
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <stdlib.h>
#include <string.h>
// 这是从命令行获取的字符串进行切分,装入argv数组中
void Do_Split(char buf[], char* argv[])
{
char* token = strtok(buf, " ");
argv[0] = token;
int i = 1;
while (token != NULL)
{
token = strtok(NULL, " ");
argv[i++] = token;
}
argv[i] = NULL;
}
// 这是创建出一个子进程,进行程序替换
void Do_Execute(char* argv[])
{
pid_t id = fork();
if (id > 0)
{
// father
wait(NULL);
}
else if (id == 0)
{
// child
execvp(argv[0], argv);
perror(argv[0]);
printf("替换错误 \n");
exit(1);
}
else
{
perror("fork\n");
}
}
int main(int argc, char* argv[], char* env[])
{
(void)argc;
(void)env;
char buf[1024] = {};
while (1)
{
char arr[50] = {};
gethostname(arr, 9);
printf("[aaa@qq.com%s]$ ",arr);
/* gets(buf); */
scanf("%[^\n]%*c",buf);
Do_Split(buf,argv);
Do_Execute(argv);
}
return 0;
}
上一篇: shell中while循环的常用写法