Linux——进程控制
前言:大佬写博客给别人看,菜鸟写博客给自己看,我是菜鸟
1.进程终止
1.1进程退出场景
先前我们说过,main函数并不是进程的开始,进程的开始是从start(目前尚未学习)开始,并调用main函数,那么main函数的返回值代表什么意思?
main函数不同返回值所代表的意思:
①:0 → 代码运行完毕,结果正确
②:非0 → 代码运行完毕,结果不正确
1.2常见进程退出的方法
正常终止:
①:从main返回
②:调用exit() →库函数,进程退出时会进行缓冲区的刷新
③:调用_exit() →系统调用,进程退出的时候不会进行缓冲区的刷新
注1:正常终止的情况下,可以通过 echo $? 查看最近一次进程的退出码
注2:exit和_exit() 括号内的值可以任意指定,你想写什么就写什么
注3:只有操作系统能够决定一个进程的生死,因此库函数exit()本质是底层封装了_exit
异常退出:
ctrl+c
2.进程等待
2.1进程等待的必要性
僵尸进程:当子进程结束时,父进程不管不顾则会进入僵尸状态。僵尸状态可能会存在内存泄露的问题。
注1:处于该状态下的子进程等待着父进程去处理它
注2:其实更重要的是回收子进程的资源,子进程的退出信息有时候根本不重要。
问:那么父进程是如何回收子进程资源,并获取子进程退出信息的?
答:每个子进程退出时,都会有对应的退出码,退出码会记录到PCB当中,父进程可以调用waitpid() 去找子进程的对应信息,并按位操作传递到父进程对应的地址空间上
注:在源码中我们可以看到退出码以及退出信号(这个稍后论述)
2.2进程等待的方法
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;
如果选项(options)设置为WNOHANG,而调用中waitpid() 发现没有已退出的子进程可收集,则返回0;
如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;pid :Pid= -1 , 等待任⼀个⼦进程。与 wait 等效。Pid> 0. 等待其进程 ID 与 pid 相等的⼦进程。status: 输出型参数(为NULL时表示不关心子进程状态)WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常出)WEXITSTATUS(status): 若 WIFEXITED非 零,提取子进程退出码。(查看进程的退出码)options:默认为 0 ,表示阻塞等待WNOHANG: 若 pid 指定的子进程没有结束,则 waitpid() 函数返回 0 ,不予以等待。若正常结束,则返回该子进程的ID 。注:
errno:strerror(errno); 可以显示具体错误信息;return errno; 可以直接返回错误的值。
WNOHANG:非阻塞调用→父进程不断调用函数去访问子进程获取子进程信息,调用一次后立马返回,又叫非阻塞轮询,通过循环完成。在非阻塞(NO BLOCK)状态下,父进程在调用完后,可以去执行其他进程,而对于阻塞状态而言,当父进程调用函数访问子进程时,会一直处于等待子进程响应的状态,例如scanf()函数。
2.3获取子进程的status
wait和waitpid,都有一个status参数,该参数时一个输出型参数,由操作系统填充。
如果传递为NULL,表示不关心子进程的退出状态信息
如果非NULL,操作系统会根据该参数,将子进程的推出信息反馈给父进程
但是status不能简单的当作整型来看待
我们来看下述代码:
int main() { int i = 4; exit(1); return 0; }
当执行上述代码,并通过 echo $? 指令查看退出码时,我们可以得到以下结果:
可以看到,退出码就是exit 括号中的参数。
而当我们写下如下图代码并执行时,却得到了意想不到的结果。
int main() { int i = 4; i /= 0; exit(1); return 0; }
以下为status的位图:
status为整型,一共4个字节,每个字节8个位,其中,高16位不用,低16位中高8位为退处状态(退出码),第8位为core dump标志(此处先不做论述),后7位为信号位。
我们可以通过按位与的方式来分别获得status中的退出码和信号
退出状态:status&0x7F
信号:(status>>8)&0xFF
注:当进程正常终止时,信号部分默认为0,只看退出码状态;而当进程意外终止时,此时退出码已经不重要了,全为0,只看终止信号部分。
3.进程替换
前言:进程替换是一个系统级别的概念
一个父进程执行fork()之后,就会有一个子进程,二者各自执行父进程的代码,那如果子进程想执行一个全新的程序呢?此时就有了进程替换这个概念。
进程替换函数:
#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, char *const argv[]); int execvp(const char *file, char *const argv[]); int execve(const char *path, char *const argv[], char *const envp[]);
3.1命名解释
补充1:
子进程在替换程序的过程中,并没有创建新的进程,而是用新的程序的代码和数据覆盖式的替换当前数据和代码,即进程替换前后的pid不变
注:我们可以通过代码来验证
问:为什么进程发生替换后没有影响父进程?
答:1.进程具有独立性 2.子进程在创建的时候,会继承父进程的数据和代码,当子进程修改参数时,系统会发生写实拷贝,给数据开辟一块新的物理空间,虚拟地址不变,并改变页表中的映射关系。对于进程替换,则数据和代码都要发生写实拷贝。
补充2:
可以看到 部分exec* 函数中还有关环境变量表的参数,如果在子进程中,自己设置了环境变量表,那么在调用 exec*函数时,子进程中新修改环境变量表会覆盖原先的环境变量表。
问:子进程是如何继承父进程的环境变量表的?
答:通过 exec* 函数。对于没有环境变量表参数的 exec* 函数,它本质上会去调用另一个exec*函数,而该函数中包含 环境变量表参数,且这个参数默认为 **environ(全局指针),子进程在创建时,这个全局指针会默认指向父进程的环境变量表;而对于有环境变量表的 exec* 函数,则如补充2中说明的那样对父进程的环境变量表进行覆盖,也可以通过 putenv("字符串") 添加新的环境变量,再将环境变量参数设置为 **environ,来实现不覆盖原环境变量表的基础上添加新的环境变量。
认知1:
①:exec* 接口相当于加载器,bash创建父进程,父进程创建子进程,子进程再通过 exec* 去执行别的进程
②:可以通过exec* 接口去调用其他语言写的程序,本质上不是调用程序,是调用解释器,不管什么语言,只要在计算器中运行,都是进程,只要是进程都可以替换
4.简单实现一个shell
☆☆☆前言:代码本身没有实际意义,但是可以锻炼编码能力,此处包含很多先前学过的东西
4.1思路
思路:
①:编写命令行提示符,包含用户名USER、主机名HOSTNAME、当前路径
②:获取用户输入的命令,通过字符串输入,例如"ls -l -a"
③:命令分析,将字符串"ls -l -a" 转为 "ls" "-l" "-a"
④:判断是否为内建命令,cd echo等
⑤:执行,创建子进程,调用 execvp() 函数进行进程替换
4.2代码实现
#include <iostream>
#include <cstdio>
#include <cstring>
#include <cstdlib>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#define COMMAND_SIZE 1024
#define FORMAT "[%s@%s %s]# "
// 下面是shell定义的全局变量
#define MAXARGC 128
char *g_argv[MAXARGC];
int g_argc = 0;
// 2. 环境变量表
#define MAX_ENVS 100
char *g_env[MAX_ENVS];
int g_envs = 0;
// 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 = getenv("PWD");
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;
//本来要从配置文件来
//1. 获取环境变量
for(int i = 0; environ[i]; i++)
{
// 1.1 申请空间
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"; //for_test
g_env[g_envs] = NULL;
//2. 导成环境变量
for(int i = 0; g_env[i]; i++)
{
putenv(g_env[i]);
}
environ = g_env;
}
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());
snprintf(cmd_prompt, size, FORMAT, GetUserName(), GetHostName(), GetPwd());
}
void PrintCommandPrompt()
{
char prompt[COMMAND_SIZE];
MakeCommandLine(prompt, sizeof(prompt));
printf("%s", prompt);
fflush(stdout);
}
bool GetCommandLine(char *out, int size)
{
// ls -a -l => "ls -a -l\n" 字符串
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;
}
// 3. 命令行分析 "ls -a -l" -> "ls" "-a" "-l"
bool CommandParse(char *commandline)
{
#define SEP " "
g_argc = 0;
// 命令行分析 "ls -a -l" -> "ls" "-a" "-l"
g_argv[g_argc++] = strtok(commandline, SEP);
while((bool)(g_argv[g_argc++] = strtok(nullptr, SEP)));
g_argc--;
return true;
}
bool Cd()
{
// cd argc = 1
if(g_argc == 1)
{
std::string home = GetHome();
if(home.empty()) return true;
chdir(home.c_str());
}
else
{
std::string where = g_argv[1];
// cd - / cd ~
if(where == "-")
{
// Todu
}
else if(where == "~")
{
// Todu
}
else
{
chdir(where.c_str());
}
}
return true;
}
void Echo()
{
if(g_argc == 2)
{
// echo "hello world"
// echo $?
// echo $PATH
std::string opt = g_argv[1];
if(opt == "$?")
{
std::cout << lastcode << std::endl;
lastcode = 0;
}
else 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;
}
}
}
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")
{
}
else if(cmd == "alias")
{
// std::string nickname = g_argv[1];
// alias_list.insert(k, v);
}
return false;
}
int Execute()
{
pid_t id = fork();
if(id == 0)
{
//child
execvp(g_argv[0], g_argv);
exit(1);
}
// father
pid_t rid = waitpid(id, nullptr, 0);
(void)rid; // rid使用一下
return 0;
}
int main()
{
while(true)
{
// 1. 输出命令行提示符
PrintCommandPrompt();
// 2. 获取用户输入的命令
char commandline[COMMAND_SIZE];
if(!GetCommandLine(commandline, sizeof(commandline)))
continue;
// 3. 命令行分析 "ls -a -l" -> "ls" "-a" "-l"
CommandParse(commandline);
//PrintArgv();
// 4. 执行命令
Execute();
}
return 0;
}
4.3代码分析
4.3.1全局变量
#define COMMAND_SIZE 1024 //用户能够输入的最多字符个数
#define FORMAT "[%s@%s %s]# "//用于后续代码的维护
#define MAXARGC 128 //命令行参数表的最大个数
char *g_argv[MAXARGC]; //命令行参数表
int g_argc = 0; //命令行参数表中数据的个数
#define MAX_ENVS 100 //环境变量表数据最大个数
char *g_env[MAX_ENVS]; //环境变量表
int g_envs = 0; //当前个数
int lastcode = 0; //用于记录最后一次进程的退出码
4.3.2配置环境变量表
void InitEnv()
{
extern char **environ;
memset(g_env, 0, sizeof(g_env));
g_envs = 0;
//1. 获取环境变量
for(int i = 0; environ[i]; i++)
{
// 1.1 申请空间
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"; //for_test
g_env[g_envs] = NULL;
//2. 导成环境变量
for(int i = 0; g_env[i]; i++)
{
putenv(g_env[i]);
}
environ = g_env;
}
每个进程创建时,都需要配置环境变量表。
通过定义一个全局字符数组 g_env[] 来模拟环境变量表,字符数组存储的数据为字符指针,通过malloc()开辟strlen(environ[i])+1大小的空间,最后一位为\0,再通过strcpy()将原环境变量表中的数据拷贝到自己的环境变量当中,另外,通过遍历字符数组,可以实现环境变量表参数的增加
注:环境变量表最后一位一定要置为NULL
最后将自己的环境变量表导入到系统的环境变量表,同时更新environ
问:是否可以删除第二步,直接执行 environ = g_env?
答:不可以!这样做只是将全局指针指向了一个自定义环境变量表,但是进程中实际的环境变量表并没有更新,后续执行还是按照原先的环境变量表来。
基础复习1:
environ:二级字符指针,environ[i] 为一级字符指针,指向字符串的首元素地址
char str[]:字符数组,单个数据为 'a' ,数据类型为 char
char* str[]:字符指针数组,单个数据为 "a",数据类型为 char* 的指针,指向字符首元素地址
4.3.3命令行提示符
void MakeCommandLine(char cmd_prompt[], int size)
{
//snprintf(cmd_prompt, size, FORMAT, GetUserName(), GetHostName(), DirName(GetPwd()).c_str());
snprintf(cmd_prompt, size, FORMAT, GetUserName(), GetHostName(), GetPwd());
}
void PrintCommandPrompt()
{
char prompt[COMMAND_SIZE];
MakeCommandLine(prompt, sizeof(prompt));
printf("%s", prompt);
fflush(stdout);
}
标准的命令行提示符如图所示:
包含:用户名、主机名、当前路径(最后一个路径)。因此我们需要创建一个字符数组,并调用getenv()函数去获得相应数据,最后通过snprintf()函数,将得到的数据格式化管理。
学习:int snprintf(char *str, size_t size, const char *format, ...);
功能:将内容格式化传入到目标字符数组内
size 保证了传入的大小,最大是 size-1 因为得保证最后一个字符为\0;
format 格式化字符串,类似于printf("format");中的字符串注:snprintf()不会将结果打印在显示器上,他不是printf();
当自定义函数MakeCommandLine()执行完毕后,此时 prompt数组中的数据已经格式化保存完毕,再通过printf()函数打印字符串即可实现命令行提示符的输出,如图所示:
基础复习2:当数组作为实参被传递时,形参既可以用相应数组接收(必须加[] ),也可以用相应的指针接收
4.3.4获取用户输入的命令
bool GetCommandLine(char *out, int size)
{
// ls -a -l => "ls -a -l\n" 字符串
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;
}
int main()
{
while(true)
{
// 1. 输出命令行提示符
// 2. 获取用户输入的命令
char commandline[COMMAND_SIZE];
if(!GetCommandLine(commandline, sizeof(commandline)))
continue;
// 3. 命令行分析
// 4. 执行命令
}
return 0;
}
我们需要将用户从键盘上输入的指令存放到字符数组commandline当中,为此调用自定义函数GetCommandLine(),将字符数组和大小设为实参,同时在自定义函数中调用fgets()函数来读取键盘输入,并返回给c。
问:为什么要执行out[strlen(out)-1] = 0; ?
答:用户在输入完指令时,会按回车来进行确认,因此这么做是为了将最后一个字符"\n"排除。排除后若strlen(out)仍为零,说明此时没有输入任何指令
当commandline()函数返回值为:false时,说明此时指令输入失败,取反执行 continue; 跳过后续命令重新开始该循环
基础复习3:char *fgets(char *str, int n, FILE *stream);
功能:用于从文件或标准输入中读取一行数据,并将数据的首地址返回。
基础复习3:size_t strlen ( const char * str );
统计一个字符串的长度,直至遇到\0为止
基础复习3:continue;
跳过本轮循环,执行下一轮循环
4.3.5命令行分析
bool CommandParse(char *commandline)
{
#define SEP " "
g_argc = 0;
g_argv[g_argc++] = strtok(commandline, SEP);
while((bool)(g_argv[g_argc++] = strtok(nullptr, SEP)));
g_argc--;//因为是后置++,多加了一次,因此需要--
return true;
}
命令行分析的核心即为:将 "ls -a -l" -> "ls" "-a" "-l",并保存在全局变量g_argv[]内。
每一次进行命令分析时,都要刷新g_argc,确保下一次能够成功解析命令。
学习:char * strtok ( char * str, const char * delimiters );
功能:将一串字符串按指定分隔符进行分割
例如:字符串"hello,world" 按 ","进行分割,就得到了hello,并将字符串首地址返回。
strtok(nullptr,SEP)表面不是第一次调用strtok,那么strtok()函数会从上一次分割的位置开始向后查找满足条件的位置,以此循环,直至结束。
该函数的功能就是将用户输入的字符串指令一一分割存储到g_argv[]内
4.3.6判断是否为内建命令
bool CheckAndExecBuiltin()
{
std::string cmd = g_argv[0];
if(cmd == "cd")
{
Cd();
return true;
}
else if(cmd == "echo")
{
Echo();
return true;
}
return false;
}
内建命令:cd、echo等这些都被称为内建命令。
通过一个string类 cmd 来接收命令行参数表中首个数据,并判断是否为内建命令,若是则执行相应语句。
4.3.6.1 cd命令
bool Cd()
{
// cd argc = 1
if(g_argc == 1)
{
std::string home = GetHome();
if(home.empty()) return true;
chdir(home.c_str());
}
else
{
std::string where = g_argv[1];
// cd - / cd ~
if(where == "-")
{
// Todu
}
else if(where == "~")
{
// Todu
}
else
{
chdir(where.c_str());
}
}
return true;
}
目前我们正在执行第四步中的Cd()函数,此时:
①:g_argv[]中存储的是用户输入的指令(已解析)
②:g_argc 中存储着当前命令行参数表中指令个数
其次,cd指令有:
①:cd 回到家目录
②:cd /指定目录
③:cd - 和 cd ~ 这里不做考虑
于是,思路就清楚了,通过判断 g_argc的个数,来编写代码:
👉:为1,说明 g_argv[] 中只有一条指令 cd,即返回家目录,通过调用GetHome()(内部封装了getenv("HOME"))得到家目录,再通过chdir库函数进入到家目录
👉:不为1,说通过chdir库函数直接进入命令行参数表中的路径
4.3.6.2 ehco命令
void Echo()
{
if(g_argc == 2)
{
// echo "hello world"
// echo $?
// echo $PATH
std::string opt = g_argv[1];
if(opt == "$?")
{
std::cout << lastcode << std::endl;
lastcode = 0;
}
else 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;
}
}
}
对于echo而言:
①:echo $? → 获取上一进程的退出码
②:echo $环境变量名(例如“HOME”) → 获取指定环境变量名
③:echo 字符串 → 打印字符串
有了对cd代码的认识,echo编写就容易了,至少从思路上而言,可以看到对于echo而言,命令行参数表中一定有两个参数,因此我们只需对第二个参数进行分析判断,将第二个参数存储到 string类 opt 当中来进行if条件判断
echo $?:
我们需要在Execute();中获取子进程的退出码,通过 WEXITSTATUS(status);获取后直接打印即可
部分代码如下:其中lastcode为全局变量
int status = 0; pid_t rid = waitpid(id, &status, 0); if(rid > 0) { lastcode = WEXITSTATUS(status); }
echo $:获取当前环境变量名
我们需要通过getenv() 来获得当前变量名,而 opt中第一个参数为$不需要,因此我们通过substr(1) 从第二个位置开始往后遍历,并返回给 env_name;再通过getenv();获取环境变量名,最后字符串
echo 字符串:
直接打印即可
4.3.6.3 补充
我们需要对GetPwd做一次修整,当我们执行cd命令时,命令行提示符,即第①步时
此时如果使用getenv("PWD")会出错(因为PWD仍为原来环境变量表中的PWD),为此我们需要更新环境变量中的PWD,代码如下:
const char *GetPwd()
{
//const char *pwd = getenv("PWD");
const char *pwd = getcwd(cwd, sizeof(cwd));
if(pwd != NULL)
{
snprintf(cwdenv, sizeof(cwdenv), "PWD=%s", cwd);
putenv(cwdenv);
}
return pwd == NULL ? "None" : pwd;
}
4.3.7执行
int Execute()
{
pid_t id = fork();
if(id == 0)
{
//child
execvp(g_argv[0], g_argv);
exit(1);
}
// father
pid_t rid = waitpid(id, nullptr, 0);
(void)rid; // rid使用一下
return 0;
}
创建子进程,通过execvp来调用相应的指令
4.4总结:
虽然代码本身没有太多实际意义,但是可以加强自己代码能力
复习:
传址or传值?数组or数组名?
当我们创建一个数组,例如 char arr[]; 那么arr就是数组名,数组名为地址,如果将数组名作为实参传递,那么传的就是地址,在形参中通过相应指针进行接收,就是传址调用,传址调用会改变原来数组内的数据
指令:
char *getcwd(char *buf, size_t size);
功能:获得当前进程的工作目录,并将首地址存放到字符指针 buf 中
返回值:
成功:指向当前工作工作路径的字符串
失败:返回NULL
int putenv(char *string);
功能:修改环境变量(没有添加,有就覆盖)
返回值:
成功:0
失败:非零
int snprintf(char *str, size_t size, const char *format, ...);
功能:格式化数据,并将字符串的首地址放入到字符指针str中
注:str必须经过初始化,否则会出现段错误
返回值:
成功:0
失败:-1
int chdir(const char *path);
功能:更改当前工作路径
返回值:
成功:0
失败:-1
原文地址:https://blog.csdn.net/m0_51952310/article/details/144187241
免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!