文章目录
- 一.子进程的创建
- 操作系统内核视角下的父子进程存在形式
- 验证子进程对父进程数据的写时拷贝
- 二.进程等待
- 进程非阻塞等待示例:
- 三.进程替换
- 内核视角下的进程替换过程:
- 综合利用进程控制系统接口实现简单的shell进程
进程控制主要分为三个方面,分别是:子进程的创建,进程等待,进程替换
一.子进程的创建
- 父进程调用
fork()
系统接口创建子进程后,操作系统会为子进程创建独立的PCB
结构体和虚拟地址空间mm_struct
,因此父子进程之间具有互相独立性
操作系统内核视角下的父子进程存在形式
- 父进程调用
fork()
函数之后:
验证子进程对父进程数据的写时拷贝
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int g_val = 0;
int main(int argc,const char * argv[])
{
//创建子进程
pid_t id = fork();
if(id < 0)
{
//创建子进程失败,退出主函数
perror("fork");
return 0;
}
else if(id == 0)
{
//子进程执行流
//子进程对变量g_val进行修改,引发写时拷贝
g_val=100;
printf("childProcess[%d]: %d : %p\n", getpid(), g_val, &g_val);
}
else
{
//父进程的执行流
//父进程先休眠3秒,让子进程先完成g_val变量的写入
sleep(3);
printf("parentProcess[%d]: %d : %p\n", getpid(), g_val, &g_val);
}
sleep(1);
return 0;
}
- 执行结果:
- 内核视角下,代码段中的写时拷贝图解:
- 写时拷贝保证了父子进程之间互相独立的同时提高了计算机整机的内存使用效率
二.进程等待
- 子进程退出后会进入僵尸状态,僵尸状态进程的内核数据结构对象会保留在操作系统内核空间中,在父进程读取子进程的退出信息后,操作系统才会释放掉子进程所占用的所有系统资源
- 若父进程比子进程先退出,那么操作系统就会接管子进程(成为孤儿进程),子进程退出后,操作系统会自动读取子进程的退出信息.
- 父进程读取子进程的退出信息这一过程称为进程等待
- 进程等待常用系统接口:
int waitpid(int pid, int *status, int options);
-
形参
int pid
: -
形参
int *status
:用于记录进程退出码和退出信号的输出型参数 -
形参
int options
表示进程等待的方式:主要分为阻塞等待和非阻塞等待两大类:- 当
option
为0
时:进程执行阻塞等待,此时进程会进入阻塞状态(PCB
退出运行队列,进入阻塞队列),直到其所等待的子进程退出,该进程才会重新进入运行状态读取子进程的退出信息 - 当
option
为非零时(比如使用系统宏WNOHANG
):进程执行非阻塞等待, 此时进程会尝试读取其子进程的退出信息,若没能读取到子进程的退出信息,则waitpid
系统接口立即返回0
- 当
-
返回值
int
:若waitpid
系统接口读取到子进程的退出信息,返回子进程的pid
,若waitpid
执行过程中出现错误,则返回-1
-
进程非阻塞等待示例:
#include <unistd.h>
#include <stdio.h>
#include <sys/wait.h>
#include <stdlib.h>
int main()
{
int pid = 0;
//创建子进程
pid = fork();
if(pid == -1)
{
//子进程创建失败
printf("fork failed\n");
return -1;
}
else if(pid == 0)
{
//子进程的代码执行流
int cnt = 5;
while(cnt--)
{
sleep(1);
printf("child process running\n");
}
exit(11);
}
else
{
//父进程的代码执行流
//非阻塞等待子进程
int status = 0;
bool quit = 0;
while(quit == 0)
{
//循环非阻塞等待子进程
int res = waitpid(pid,&status,WNOHANG);
if(res > 0)
{
//子进程已退出
printf("waiting pid success,chilProc exit,退出码:%d\n",WEXITSTATUS(status));
quit = 1;
}
else if(res == 0)
{
//子进程还未退出
printf("waiting pid success,childProc still running\n");
}
else
{
//等待发生错误
printf("waiting pid failed\n");
}
sleep(1);
}
}
return 0;
}
三.进程替换
- 进程替换的概念:通过特定的系统调用接口,操作系统可以将进程当前执行的代码段替换成指定系统路径下其他可执行程序的代码段,然后根据进程从头开始执行新的代码段(因此需要通过进程替换系统接口为新代码段传入
main
函数命令行参数)
内核视角下的进程替换过程:
- 可见进程替换并不是创建新的进程(进程的
PCB
和虚拟内存结构体不变) - 进程替换系统接口:
- 形参和返回值统一解释:
- 参数
const char*path
:表示将要替换现有代码段的目标可执行程序的完整路径 - 参数
const char*file
:表示将要替换现有代码段的目标可执行程序的文件名,其系统路径由环境变量PATH
决定 - 参数
const char*arg,...
:表示传给新代码段main
函数命令行参数的字符串,...
表示可变参数列表,可以传入多个字符串,最后一个字符串需传入NULL
- 参数
char *const argv[]
:表示传给新代码段main
函数命令行参数的字符串数组,数组中最后一个字符串需传入NULL
- 参数
cahr*const envp[]
:表示传递给新代码段的环境变量字符串数组 - 当进程替换失败时,
exec
系列系统接口会返回-1
- 参数
综合利用进程控制系统接口实现简单的shell进程
shell
进程的运行原理:shell
进程接收到用户输入的指令后,对指令进行格式化处理,然后创建子进程,子进程通过exec
系列系统接口将自身替换成系统命令并执行以响应用户需求,shell
进程的这种运行机制保证了自身的进程安全(子进程出现错误不会影响到父进程的运行)
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <string.h>
#include <memory.h>
//用户输入的命令行的最大长度
#define CStrLen 1024
//解析命令行得到的格式化字符串数组
#define SStrLen 50
//命令行字符串
char CommStr[CStrLen];
//格式化命令行字符串数组
char* StdStr[SStrLen];
//命令行分隔符
#define Sep " "
//操作系统配置的环境变量
extern char ** environ;
int main()
{
while(1)
{
printf("[我的命令行解释器 myshell]$ ");
fflush(stdout);
memset(CommStr,'\0',sizeof StdStr);
//获取用户输入命令
if(fgets(CommStr,sizeof CommStr,stdin)== NULL)
{
continue;
}
CommStr[strlen(CommStr)-1] = '\0';
StdStr[0] = strtok(CommStr,Sep);
//根据空格对用户输入的字符串进行分割并存入StdStr字符串数组中
int i = 1;
while(StdStr[i++] = strtok(NULL,Sep));
//部分命令(比如cd命令)需要由shell进程自己来执行
if(strcmp(StdStr[0],"cd")== 0)
{
//用chdir函数改变shell进程的工作路径
if(StdStr[1] != NULL)
{
chdir(StdStr[1]);
}
continue;
}
//shell创建子进程来执行系统命令
int pid = fork();
if(pid == 0)
{
//printf("shell的子进程执行系统命令:\n");
execvp(StdStr[0],StdStr);
printf("-mybash: %s: command execute failed\n",StdStr[0]);
exit(-1);
}
else
{
int status;
//父进程进行阻塞等待,等待子进程执行完系统命令结束并获取其退出码
int waitres = waitpid(pid,&status,0);
if(waitres == -1)
{
printf("waitchild process failed\n");
}
}
}
return 0;
}