进程创建
fork创建子进程
fork函数创建子进程,它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。
#include <unistd.h>
pid_t fork(void);
返回值:自进程中返回0,父进程返回子进程id,出错返回-1
fork创建子进程,操作系统做了什么操作?
-
分配新的内存块和内核数据结构给子进程
-
将父进程部分数据结构内容拷贝至子进程
-
添加子进程到系统进程列表当中
-
fork返回,开始调度器调度
fork函数的返回值
fork函数为什么要给子进程返回0,给父进程返回子进程的PID?
一个父进程可以创建多个子进程,而一个子进程只能有一个父进程。
因此,对于子进程来说,父进程是不需要被标识的;而对于父进程来说,子进程是需要被标识的,因为父进程创建子进程的目的是让其执行任务的,父进程只有知道了子进程的PID才能很好的对该子进程指派任务。
fork函数为什么有两个返回值?
写时拷贝
当子进程刚刚被创建时,子进程和父进程的数据和代码是共享的,即父子进程的代码和数据通过页表映射到物理内存的同一块空间。只有当父进程或子进程需要修改数据时,才将父进程的数据在内存当中拷贝一份,然后再进行修改。
OS为何要采用写时拷贝将父子进程分离?
fork调用失败的原因
-
系统中有太多的进程(内存不够)
-
实际用户的进程数超过了限制(用户是有创建进程数量限制的)
进程终止
退出码
main函数中return的理解
main函数是间接性被操作系统所调用的,当main函数调用结束后就应该给操作系统返回相应的退出信息,而这个所谓的退出信息就是以退出码的形式作为main函数的返回值返回,
我们一般以0表示代码成功执行完毕,以非0表示代码执行过程中出现错误,这就是为什么我们都在main函数的最后返回0的原因。
exit()与_exit()
exit函数
#include <unistd.h>
void exit(int status);
exit函数可以在代码中的任何地方退出进程,并且exit函数在退出进程前会做一系列工作:
- 执行用户通过atexit或on_exit定义的清理函数。
- 关闭所有打开的流,所有的缓存数据均被写入。
- 调用_exit函数终止进程。
_exit函数
_ exit函数用法与exit类似,_ exit函数退出进程的方法我们并不经常使用,_ exit函数也可以在代码中的任何地方退出进程,但是_exit函数会直接终止进程,并不会在退出进程前会做任何收尾工作。
exit()与_exit()函数的区别
进程异常退出
情况一:向进程发生信号导致进程异常退出。
例如,在进程运行过程中向进程发生kill -9信号使得进程异常退出,或是使用Ctrl+C使得进程异常退出等。
情况二:代码错误导致进程运行时异常退出。
例如,代码当中存在野指针问题使得进程运行时异常退出,或是出现除0的情况使得进程运行时异常退出等。
进程等待
进程等待的必要性
- 子进程退出,父进程如果不读取子进程的退出信息,子进程就会变成僵尸进程,进而造成内存泄漏。
- 进程一旦变成僵尸进程,那么就算是kill -9命令也无法将其杀死,因为谁也无法杀死一个已经死去的进程。
- 对于一个进程来说,最关心自己的就是其父进程,因为父进程需要知道自己派给子进程的任务完成的如何。
- 父进程需要通过进程等待的方式,回收子进程资源,获取子进程的退出信息。
进程等待方法
wait方法
#include<sys/types.h>
#include<sys/wait.h>
pid_t wait(int*status);
返回值:
成功返回被等待进程pid,失败返回-1。参数:
输出型参数,获取子进程退出状态,不关心则可以设置成为NULL
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:
默认为0,表示阻塞等待
WNOHANG: ,非阻塞等待,若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。
若正常结束,则返回该子进程的ID。
获取子进程的status
waitpid被调用时内部伪代码模型
父进程要拿到子进程的退出信息,为什么要用wait/waitpid函数?为什么不用全局变量?
父子进程虽然共用同一份代码,但当某个变量要修改时,就会发生写时拷贝,
又因为进程具有独立性,父进程无法获取子进程已经修改的变量的值。
既然进程具有独立性,进程退出码也是子进程的数据,父进程是如何拿到的呢?wait/wawitpid做了什么工作呢?
僵尸进程是一个死亡进程,代码和数据都会被释放,但该进程的PCB还保留着,task_struct保存了该进程退出时的退出信息。
进程退出时,进程的退出码和退出信号就会被写入到PCB中。
基于非阻塞接口的轮询检测方案
当子进程未退出时,父进程都在一直等待子进程退出,在等待期间,父进程不能做任何事情,这种等待叫做阻塞等待。
实际上我们可以让父进程不要一直等待子进程退出,而是当子进程未退出时父进程可以做一些自己的事情,当子进程退出时再读取子进程的退出信息,即非阻塞等待。
进程替换
替换原理
用fork创建子进程后,子进程执行的是和父进程相同的程序(但有可能执行不同的代码分支),若想让子进程执行另一个程序,往往需要调用一种exec函数。
当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,并从新程序的启动例程开始执行。
当进行进程程序替换时,有没有创建新的进程?
进程程序替换之后,该进程对应的PCB、进程地址空间以及页表等数据结构都没有发生改变,只是进程在物理内存当中的数据和代码发生了改变,所以并没有创建新的进程,而且进程程序替换前后该进程的pid并没有改变。
子进程进行进程程序替换后,会影响父进程的代码和数据吗?
子进程刚被创建时,与父进程共享代码和数据,但当子进程需要进行进程程序替换时,也就意味着子进程需要对其数据和代码进行写入操作,这时便需要将父子进程共享的代码和数据进行写时拷贝,此后父子进程的代码和数据也就分离了,因此子进程进行程序替换后不会影响父进程的代码和数据。
替换函数
execl()
不创建子进程的程序替换
创建子进程的程序替换
execv()
execlp()
execvp()
execle()
makefile中如何同时编译多个项目
注意:
以上几个程序替换函数都是系统提供的基本封装,他们底层都是调用execve这个系统直接提供的进程替换接口。
系统直接提供的进程替换接口:
**int execve(const char *filename, char const argv[], char const envp[]);
做一个简易的shell
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/wait.h>
#define NUM 1024
#define SIZE 32
#define SEP " "
//保存完整的命令字符串
char cmd_line[NUM];
//保存切割后的命令行字符串
char *g_argv[SIZE];
// 保存从g_argv拷贝来的字符串
char my_argv[64];
//shell运行原理:通过让子进程执行命令,父进程等待&&解析命令
int main()
{
//0.命令行解释器,一定是一个常驻内存的进程,不退出
while(1)
{
//1.打印出提示信息 [lhj@localhost shell]#
printf("[lhj@localhost shell]# ");
fflush(stdout);//刷新缓冲区
memset(cmd_line,'\0',sizeof cmd_line);
//2.获取用户的键盘输入[输入的是各种指令和选项 :"ls -a -l"]
if(fgets(cmd_line,sizeof cmd_line,stdin)==NULL)
{
continue;
}
//将用户输入的回车键设置为\0 使用户输入的指令与提示信息在同一行
//例如:当用户输入 ls -a -l 然后回车 ,缓冲区读到的数据为:"ls -a -l \n\0"
//为了防止换行,就得把\n替换成\0
cmd_line[strlen(cmd_line)-1]='\0';
//3.解析命令行字符串,将用户输入的字符串分割成子字符串
//例如:用户输入"ls -a -l" -> "ls" "-a" "-l"
g_argv[0]=strtok(cmd_line,SEP);//切割原始字符串,第一次调用,要传入原始字符串
int index=1;
if(strcmp(g_argv[0],"ls")==0)
{
g_argv[index++]="--color=auto";//上颜色
}
if(strcmp(g_argv[0],"ll")==0)
{
g_argv[0]="ls";
g_argv[index++]="-l";
g_argv[index++]="--color=auto";
}
//用循环将整个字符串都全部切割完
//第二次若是还要切割原始字符串,可以直接传入NULL
while(g_argv[index++]=strtok(NULL,SEP));
if(strcmp(g_argv[0],"export")==0&&g_argv[1]!=NULL)
{
// cmd_line 中保存的是完整的字符串
// g_argv中保存的是cmd中的子字符串
//g_argv指针数组中保存的是字符串在cmd_line中的地址
//当下一次执行循环时,cmd_line被清空后,环境变量的地址没变
//但是cmd_line的被清空了,故需要将g_argv的内容拷贝到my_argv中
strcpy(my_argv,g_argv[1]);
int ret=putenv(my_argv);
if(ret==0)
printf("%s export success\n",g_argv[1]);
}
//4.内置命令,让父进程(shell)自己执行的命令,我们叫做内置命令
//内置命令本质就是shell中的一个函数调用
if(strcmp(g_argv[0],"cd")==0)
{
if(g_argv[1]!=NULL)
chdir(g_argv[1]);
continue;
}
//5.fork()
pid_t id=fork();
if(id==0)//child
{
printf("下面功能是让子进程进行的\n");
execvp(g_argv[0],g_argv);
exit(1);
}
//father
int status=0;
pid_t ret=waitpid(id,&status,0);
if(ret >0)
printf("exit code:%d\n",WEXITSTATUS(status));
}
}