进程控制
一、进程创建
目录
[2.4.exit VS _exit](#2.4.exit VS _exit)
1.1.fork函数
从已经存在的进程中创建一个新的进程
**原进程:**父进程
**新进程:**子进程
cpp
#include <unistd.h>
pid_t fork(void);
返回值:⼦进程中返回0,⽗进程返回⼦进程PID,出错返回-1
进程调用fork,内核中的操作:
- 分配新的内存块和内核数据结果给子进程
- 将父进程部分数据结构内容拷贝至子进程
- 添加子进程到系统进程的列表当中
- fork返回,调度器开始调度

1.2.fork函数的返回值
**子进程:**返回0
**父进程:**返回子进程的PID
1.3.写时拷贝
父子进程代码共享,当父子没有写入时,数据也是共享的
当任意一方试图写入,发生写时拷贝,各自生成一份副本
基本原理:
创建子进程时,操作系统会将页表的数据项设为只读
当写入数据时,操作系统会通过报错的形式
来重新申请内存,拷贝数据,建立映射关系

1.4.fork常规用法
创建子进程,让父子进程各自执行后续代码的一部分
创建子进程,让子进程执行全新的程序
1.5.fork调用失败的原因
系统中的进程过多,内存空间不足
实际用户的进程超过了限制
二、进程终止
**进程终止的本质:**系统少了个进程,释放PCB,进程地址空间,页表,代码和数据
(注:僵尸进程在终止时,它的PCB会被维护,方便父进程获取退出信息)
2.1.进程退出场景
- 代码运行完毕,结果正确
- 代码运行完毕,结果不正确
- 代码异常终止
子进程是由父进程创建的,子进程退出后,父进程接收子进程执行的结果,根据结果进行决策
main函数的返回值通常表示该进程的执行情况
**情况1:**代码运行完毕,结果正确,返回0
**情况2:**代码运行完毕,结果不正确,返回非0(不同的值表示结果不正确的原因)
进程退出后main函数的返回值会被写到PCB内部
父进程bash通过系统调用函数获取,写入status
main函数的返回值 == 进程正常退出时的退出码
**实验:**打印出最近一个进程退出时的退出码
- proc.c

实验现象

2.2.进程退出码
**进程执行完毕,结果正确:**0
**进程执行完毕,结果不正确:**非0值
**实验:**打印退出码对应的描述
- proc.c

实验现象
在Linux系统中,一共有134个默认的退出码
status可以存8位退出码,可以自定义121个


**实验:**打印打开文件不存在时,对应的标准退出码errno
- proc.c

实验现象

查看使用指令ls打开不存在文件时的描述与标准退出码

如果进程代码能够运行完毕,可以返回定制的退出码
- proc.c

实验现象

如果进程代码发生异常被信号杀死,只能返回128+信号值
- proc.c

实验现象

进程出现异常时 ,是进程收到信号,此时status的15~8位数据为垃圾值,退出码无意义
2.3.进程退出方式
**方式一:**main函数返回,表示进程结束(其他函数返回,表示调用完成)
main正常返回后,返回值会当作该进程的退出码,作为exit函数的参数
**方式二:**调用exit函数
- proc.c

实验现象

任何地方调用exit函数,表示进程结束
将子进程的退出码返回给父进程bash
**方式三:**调用_exit函数
- proc.c

实验现象

2.4.exit VS _exit
exit(C语言的库函数)
cpp
#include <stdlib.h>
void exit(int status);
参数:status 只定义了进程的退出码

换行刷新缓冲区,打印消息,2s后进程退出

消息先被放在缓冲区,2s后进程退出,自动刷新缓冲区,打印消息
_exit(系统调用函数)
cpp
#include <unistd.h>
void _exit(int status);
参数:status 只定义了进程的退出码
(注:只有低8位可以使用:_exit(-1) == 255)

换行刷新缓冲区,打印消息,2s后进程退出

消息先被放在缓冲区,2s后进程退出,不会刷新缓冲区,没有打印消息
库函数 与系统调用函数 属于上下层之间的关系
库函数会调用相关的系统调用函数来完成任务
可以判断缓冲区一定不是操作系统内部缓冲区,而是C语言提供的库缓冲区

三、进程等待
3.1.进程等待的必要性
子进程退出,如果父进程不处理,子进程 会变成僵尸进程 ,造成内存泄漏 的问题并且无法被杀死
通过进程等待,父进程可以:
- 回收子进程的资源(核心)
- 获取子进程退出信息(可选)
3.2.进程等待的方法
wait方法
等待任意一个退出的子进程
cpp
#include<sys/types.h>
#include<sys/wait.h>
pid_t wait(int* status);
返回值:成功返回目标僵尸进程PID,失败返回-1
参数:退出状态,不关⼼可以设置为NULL
**实验:**回收子进程的资源,解决僵尸进程问题
- proc.c

实验现象


父进程等待子进程,在子进程没有退出时,父进程会阻塞在wait调用处

waitpid方法
cpp
pid_ t waitpid(pid_t pid, int *status, int options);
返回值:
正常情况:返回收集到的子进程PID
没有已退出的⼦进程可收集:返回0
调用出错:返回-1 (errno会被设置成相应的值,指示错误)
参数:
1.pid
pid = -1:等待任意一个子进程
pid > 0:等待与输入的PID相等的子进程
2.status(输出型参数)
/*宏函数*/
WIFEXITED(status): 查看进程是否是正常退出,非0表示正常退出(检测信号值是否为0)
WEXITSTATUS(status):提取⼦进程退出码
3.options
默认为0,表示阻塞等待
WNOHANG:
若指定⼦进程没有结束,返回0
若指定子进程正常结束,返回该⼦进程的PID
**实验:**使用waitpid进程等待一个正常退出的子进程
- proc.c

实验现象

**实验:**使用waitpid进程等待一个没有创建的子进程
- proc.c

实验现象

3.3.退出状态(status)
**status:**一个整型变量,描述子进程的退出状态,高16位无效,只研究低16位
正常退出时:
- **[15,8]:**退出代码
- [7,0]:0
发生异常时:
- **[15,8]:**垃圾值
- **[7]:**core dump标志位(TODO)
- **[6,0]:**退出信号

**实验:**获取子进程的退出状态
- proc.c

实验现象

status打印的是256,而不是1
需要将statu右移8位,再打印
- proc.c

实验现象

3.4.退出信号与异常终止
信号查看

**左侧:**信号值
**右侧:**信号名称
信号的本质是宏,查看的是宏值与宏名称
进程没有异常 时,status的低7个bit位为0,高8位为退出码
进程异常终止 时,status的低7个bit位为异常对应的信号,高8位无意义
**实验:**获取进程没有异常时的退出码与退出信号
- proc.c

实验现象

**实验:**获取进程异常退出时的退出信号
- proc.c

实验现象
退出码无意义,退出信号为11,代表段错误,表示有野指针问题

原理图

**实验:**通过宏函数获取退出状态中的退出码数据
- proc.c

实验现象

**实验:**通过宏函数判断是否发生异常
- proc.c

实验现象

3.5.阻塞与非阻塞等待


**WNOHANG(Wait No Hang):**非阻塞(None Block)
非阻塞轮询(循环完成)
父进程不断访问子进程,子进程未退出,父进程直接挂断,一段时间后再次访问,直到访问成功
- **返回值大于0:**等待结束,子进程退出
- **返回值等于0:**调用结束,子进程没有退出
- **返回值小于0:**等待失败
父进程在等待子进程的间隙中,可以执行自己的程序,使效率更高
阻塞调用
父进程保持访问子进程状态,不挂断,直到子进程退出,访问成功
**实验:**使用非阻塞调用
- proc.c

实验现象

**实验:**通过非阻塞调用,让父进程在时间间隙中执行自己的任务
- proc.c

实验现象

当使用sleep指令后,bash会发生阻塞等待,导致后续的指令无法执行

四、进程程序替换
4.1.程序替换原理
将新路径下的程序代码与数据,覆盖 到原进程 地址空间的代码 段与数据段
进程 == 内核数据结构(PCB) + 代码和数据,此时PCB不变,调整页表

程序替换的过程中,并没有创建新的进程
只是把当前进程的代码和数据,用新的程序的代码和数据覆盖式地进行替换
一旦程序替换成功,就去执行新的代码,原始代码的后半部分已经不存在了
**实验:**使用execl实现程序替换
- proc.c

实验现象

exec系列的函数只有失败返回值,没有成功返回值
**实验:**打印execl调用失败时的返回值
- proc.c

实验现象

4.2.替换函数
非系统调用函数,用语言封装了系统调用函数execve

- **l(list):**表示参数采用列表
- **v(vector):**表示参数采用数组
- **p(path):**有p自动搜索环境变量PATH
- **e(env):**自己维护环境变量
|---------|------|-------|--------------|
| 函数名 | 参数格式 | 是否带路径 | 是否使用当前环境变量 |
| execl | 列表 | 不是 | 是 |
| execlp | 列表 | 是 | 是 |
| execle | 列表 | 不是 | 不是,需自己组装环境变量 |
| execv | 数组 | 不是 | 是 |
| execvp | 数组 | 是 | 是 |
| execvpe | 数组 | 是 | 不是,需自己组装环境变量 |
execl函数
**参数1:**路径+程序名(要执行谁)
**参数2:**命令行怎么写就怎么传(怎么执行)
**最后一个参数:**NULL(表示参数传递完成)
**实验:**创建子进程来调用替换函数
- proc.c

实验现象

进程具有独立性,子进程的数据和代码都会发送写时拷贝,所以不会影响父进程
程序加载的本质是加载器动态创建进程的过程,exec系列的接口属于加载器范畴
**实验:**使用替换函数替换自己写的程序

- proc.c

实验现象

**实验:**使用替换函数替换python程序

- proc.c

实验现象

**实验:**使用替换函数替换shell程序

- proc.c

实验现象

**实验:**观察发生进程程序替换的子进程PID是否变化

- proc.c

实验现象

execlp函数
**参数1:**执行的文件名(会在环境变量PATH中查找指定的命令)
**参数2:**命令行怎么写就怎么传(怎么执行)
**最后一个参数:**NULL(表示参数传递完成)
- proc.c

实验现象

execle函数
**参数1:**路径+程序名(要执行谁)
**参数2:**命令行怎么写就怎么传(怎么执行)
**参数3:**提供一个环境变量表(指针数组,最后一个元素为NULL)
execv函数
**参数1:**路径+程序名(我要执行谁)
**参数2:**提供一个命令行参数表(指针数组,最后一个元素为NULL)
- proc.c

实验现象

execvp函数
**参数1:**执行的文件名(会在环境变量PATH中查找指定的命令)
**参数2:**提供一个命令行参数表(指针数组,最后一个元素为NULL)
- proc.c

实验现象

execvpe函数
**参数1:**执行的文件名(会在环境变量PATH中查找指定的命令)
**参数2:**提供一个命令行参数表(指针数组,最后一个元素为NULL)
**参数3:**提供一个环境变量表(指针数组,最后一个元素为NULL)

- proc.c

实验现象

发生替换的子进程会使用全新的环境变量列表
而不是子进程从父进程中继承下来的环境列表
4.2.putenv函数
**功能:**哪个进程调用就将环境变量以新增方式添加到该进程中

- proc.c

实验现象

**实验:**使用environ指针让execvpe函数实现新增环境变量
- proc.c

实验现象

4.3.execve函数
系统调用函数

替换函数的底层都是调用execve函数

五、自主Shell命令行解释器
5.1.Myshell代码
cpp
#include <iostream>
#include <cstdio>
#include <cstring>
#include <cstdlib>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <cstring>
#include <unordered_map>
#define COMMAND_SIZE 1024
#define FORMAT "[%s@%s %s]# "
//shell定义的全局数据
//命令行参数表
#define MAXARGC 128
char *g_argv[MAXARGC];
int g_argc = 0;
//环境变量表
#define MAX_ENVS 100
char *g_env[MAX_ENVS];
int g_envs = 0;
//别名映射表
std::unordered_map<std::string,std::string> alias_list;
//for test
char cwd[1024];
char cwdenv[1024];
//last exit code
int lastcode = 0;
//获取用户名
const char *GetUserName()
{
const char *name = getenv("USER");
return name == NULL ? "None" : name;
}
//获取主机名
const char *GetHostName()
{
const char *hostname = getenv("HOSTNAME");
return hostname == NULL ? "None" : hostname;
}
//获取当前路径
const char *GetPwd()
{
const char *pwd = getcwd(cwd,sizeof(cwd));
if(pwd != NULL)
{
snprintf(cwdenv, sizeof(cwdenv), "PWD=%s",cwd);
putenv(cwdenv);
}
return pwd == NULL ? "None" : pwd;
}
//获取家目录
const char *GetHome()
{
const char *home = getenv("HOME");
return home == NULL ? "" : home;
}
//初始化环境变量
void InitEnv()
{
extern char **environ;
memset(g_env, 0, sizeof(g_env));
g_envs = 0;
//从父进程shell中获取环境变量
for(int i = 0; environ[i]; i++)
{
//申请空间
g_env[i] = (char*)malloc(strlen(environ[i] + 1));
//拷贝变量
strcpy(g_env[i], environ[i]);
g_envs++;
}
g_env[g_envs++] = (char*)"HAHA=for_test";
g_env[g_envs] = NULL;
//导入环境变量
for(int i = 0; g_env[i]; i++)
{
putenv(g_env[i]);
}
}
//内建命令
bool Cd()
{
if(g_argc == 1)
{
std::string home = GetHome();
if(home.empty())
{
return true;
}
chdir(home.c_str());
}
else
{
std::string where = g_argv[1];
if(where == "-")
{
//cd -
}
else if(where == "~")
{
//cd ~
}
else
{
chdir(where.c_str());
}
}
return true;
}
bool Echo()
{
if(g_argc == 2)
{
std::string opt = g_argv[1];
if(opt == "$?")
{
std::cout << lastcode << std::endl;
lastcode = 0;
return true;
}
if(opt[0] == '$')
{
std::string env_name = opt.substr(1);
const char *env_value = getenv(env_name.c_str());
if(env_value)
{
std::cout << env_value << std::endl;
}
}
else
{
std::cout << opt << std::endl;
}
}
return true;
}
//获取当前路径的目录名
std::string DirName(const char *pwd)
{
#define SLASH "/"
std::string dir = pwd;
if(dir == "SLASH") return "SLASH";
auto pos = dir.rfind(SLASH);
if(pos == std::string::npos) return "BUG?";
return dir.substr(pos+1);
}
//生成命令行
void MakeCommandLine(char cmd_prompt[],int size)
{
snprintf(cmd_prompt, size, FORMAT, GetUserName(),GetHostName(),DirName(GetPwd()).c_str());
}
//输出命令行
void PrintCommandPrompt()
{
char prompt[COMMAND_SIZE];
MakeCommandLine(prompt, sizeof(prompt));
printf("%s",prompt);
fflush(stdout);
}
//输入命令行字符串
bool GetCommandLine(char *out, int size)
{
char *c = fgets(out, size, stdin);
if(c == NULL)
{
return false;
}
out[strlen(out) - 1] = 0;//清理\n
if(strlen(out) == 0)
{
return false;
}
return true;
}
//命令行分析
bool CommandParse(char *commandline)
{
#define SEP " "
g_argc = 0;
//"ls -a -l" -> "ls" "-a" "-l"
g_argv[g_argc++] = strtok(commandline,SEP);
while(g_argv[g_argc++] = strtok(nullptr,SEP));
g_argc--;
return g_argc > 0 ? true : false;
}
//打印命令行参数
void PrintArgv()
{
for(int i = 0; g_argv[i]; i++)
{
printf("argv[%d]->%s\n",i,g_argv[i]);
}
}
//检测并且执行内建命令
bool CheckAndExecBuiltin()
{
std::string cmd = g_argv[0];
if(cmd == "cd")
{
Cd();
return true;
}
else if(cmd == "echo")
{
Echo();
return true;
}
// else if(cmd == "export")
// {
// Export();
// return true;
// }
// else if(cmd == "alias")
// {
// std::string nickname = g_argv[1];
// alias_list.insert(k,v);
// }
return false;
}
//执行命令
int Execute()
{
//#4:执行命令
pid_t id = fork();
if(id == 0)
{
//子进程
execvp(g_argv[0],g_argv);
exit(1);
}
int status = 0;
//父进程
pid_t rid = waitpid(id, &status, 0);
if(rid > 0)
{
lastcode = WEXITSTATUS(status);
}
return 0;
}
int main()
{
//shell启动时从系统中获取环境变量
//myshell环境变量从父进程shell获取
InitEnv();
while(true)
{
//#1:输出命令行提示符
PrintCommandPrompt();
//#2:获取用户输入的命令
char commandline[COMMAND_SIZE];
if(!GetCommandLine(commandline,sizeof(commandline)))
{
//输入失败后重新输入
continue;
}
//#3:命令行分析
if(!CommandParse(commandline))
{
continue;
}
//PrintArgv();
//printf("echo:%s\n",commandline);
//检测别名
//#4:检测并处理内建命令
if(CheckAndExecBuiltin())
{
continue;
}
//#5:执行命令
Execute();
}
return 0;
}