Linux进程控制:【进程创建】【进程终止】【进程等待】【进程程序替换】【自主shell命令行解释器】
目录
一.进程创建
1.1fork函数初识
1.2写时拷贝
1.3fork常规用法
1.4fork调用失败的原因
二.进程终止
2.1进程退出场景
2.2进程常见退出方法
2.2.1退出码
2.2.2_exit和exit
三.进程等待
3.1进程等待的必要性
3.2进程等待的方式
3.2.1wait
3.2.2waitpid
3.3获取子进程status(第二个参数)
wait从哪里获取的status?
WEXITSTATUS获取子进程退出码
WIFEXITED是否异常退出
3.4阻塞和非阻塞等待(第三个参数)
四.进程程序替换
4.1替换原理
4.2替换函数
效果:
4.2.1函数解释
4.2.2函数的使用与理解
4.3浅谈exec*
4.3.1execl
4.3.2execlp
4.3.3execv
4.3.4execvp
4.3.5execvpe
4.4 命名理解
五.自主shell命令行解释器
5.1打印命令行
清除换行
5.2获取用户输入命令
5.3命令行分析
5.4指行命令
5.5修改路径的显示
编辑
5.6内键命令cd和echo
5.7获取环境变量
5.8全部代码
一.进程创建
1.1fork函数初识
在linux中fork函数是非常重要的函数,它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。
#include <unistd.h>
pid_t fork(void);
返回值:⾃进程中返回0,⽗进程返回⼦进程id,出错返回-1
进程调用fork,当控制转移到内核中的fork代码后,内核做:
• 分配新的内存块和内核数据结构给子进程
• 将父进程部分数据结构内容拷贝至子进程
• 添加子进程到系统进程列表当中
• fork返回,开始调度器调度
当⼀个进程调用fork之后,就有两个二进制代码相同的进程。而且它们都运行到相同的地方。但每个进程都将可以开始它们自己的旅程,看如下程序。
int main( void )
{
pid_t pid;
printf("Before: pid is %d\n", getpid());
if ( (pid=fork()) == -1 )perror("fork()"),exit(1);
printf("After:pid is %d, fork return %d\n", getpid(), pid);
sleep(1);
return 0;
}
运⾏结果:
[root@localhost linux]# ./a.out
Before: pid is 43676
After:pid is 43676, fork return 43677
After:pid is 43677, fork return 0
所以,fork之前父进程独立执行,fork之后,父子两个执行流分别执行。注意,fork之后,谁先执行完全由调度器决定。
1.2写时拷贝
通常,父子代码共享,父子在不写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方式各自一份副本。具体见下图:
因为有写时拷贝技术的存在,所以父子进程得以彻底分离!完成了进程独立性的技术保证! 写时拷贝,是一种延时申请技术,可以提高整机内存的使用率
刚开始在页表数据段和代码段都是只读的,所以在我们想写入的时候进行查页表,操作系统就会报错,这种报错不是真的报错,这时候会缺页中断(会判断到底是咋回事,到底是异常了,还是要进行写时拷贝了):
当我们在只读的页表数据段或代码段中进行写操作时,操作系统会引发一个缺页中断。缺页中断是一种异常情况,发生在访问尚未分配或不可访问的内存页时。
当发生缺页中断时,操作系统会检查引发缺页中断的内存访问类型。如果是写操作,并且该页是通过写时拷贝机制共享的,操作系统会执行写时拷贝机制。
1.3fork常规用法
• 一个父进程希望复制自己,使父子进程同时执行不同的代码段。例如,父进程等待客户端请求, 生成子进程来处理请求。
• 一个进程要执行一个不同的程序。例如子进程从fork返回后,调用exec函数。
1.4fork调用失败的原因
• 系统中有太多的进程
• 实际用户的进程数超过了限制
二.进程终止
进程终止的本质是释放系统资源,就是释放进程申请的相关内核数据结构和对应的数据和代码。
2.1进程退出场景
无非就三种情况:
• 代码运行完毕,结果正确
• 代码运行完毕,结果不正确
• 代码异常终止
我们写的C语言函数通常在结尾加上return 0;意思就是说我们的程序正常结束了。而其他不同的值代表不同的出错原因。
返回的数是返回给了父进程,那我们怎么查看呢?
2.2进程常见退出方法
已知我当前目录没有log.txt:
这里肯定会返回1:
echo $?(打印最近一个进程退出时的退出码,main函数的返回值我们叫进程退出码)(而我们的进程退出码是写到我们的task_struct内部(僵尸进程))
1. 从main返回
2. 调用exit
3. _exit
所以我们在echo一下,它打印的是最近进程的退出码,上一个echo
2.2.1退出码
退出码(退出状态)可以告诉我们最后一次执行的命令的状态。在命令结束以后,我们可以知道命令是成功完成的还是以错误结束的。其基本思想是,程序返回退出代码0 时表示执行成功,没有问题。 代码1 或0 以外的任何代码都被视为不成功。
C标准库string.h的头文件里有一个函数strerror可以把数字转换为我们人可以看懂的字符串:
134个:
在C标准库中,有一些函数会设置错误码来指示函数调用是否成功以及发生了哪些错误。这些错误码通常被定义为全局变量errno,并且在头文件<errno.h>中声明。
运行之后查看错误码是2:
注意,异常了退出码无意义,进程一旦出现异常,一般就是进程收到了信号。
2.2.2_exit和exit
这两个都可以使进程退出,先看现象:
只会打印出来一个(任何地方调用exit,表示进程结束,并返回给父进程bash子进程的退出码):
进度条时,这里的知识点是缓冲区刷新:
到这里是正常的,在缓冲区:
2秒后:
并没有我们期待的打印main。
exit是库函数,_exit是系统调用:
exit最后也会调用_exit,但在调用_exit之前,还做了其他工作:
1. 执行用户通过atexit或on_exit定义的清理函数。
2. 关闭所有打开的流,所有的缓存数据均被写入
3. 调用_exit
如果是系统内部的缓冲区,_exit是系统调用,exit是封装了系统调用的库函数,如果_exit不能刷新缓冲区,那么exit也一定不能:
三.进程等待
3.1进程等待的必要性
• 子进程退出,父进程如果不管不顾,就可能造成‘僵尸进程’的问题,进而造成内存泄漏。
• 另外,进程一旦变成僵尸状态,那就刀枪不入,“杀⼈不眨眼”的kill -9 也无能为力,因为谁也没有办法杀死一个已经死去的进程。
• 最后,父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是 不对,或者是否正常退出。
• 父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息
3.2进程等待的方式
3.2.1wait
先看代码:
5s前的状态(子进程未退出):
5s~10s(子进程退出,僵尸状态):
10s后(wait等到):
pid也是对应着的:
wait等待任意个退出的子进程,如果等待子进程,子进程没有退出,父进程会阻塞在wait函数调用处(scanf)
3.2.2waitpid
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。
第一个参数:
可以选择的等待哪个子进程,-1的作用同等于上面的wait函数,>0的值就是pid
状态的变化和上面的wait函数一样。
3.3获取子进程status(第二个参数)
前面只说了回收子进程,但获取子进程状态呢?
• wait和waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充。
• 如果传递NULL,表示不关心子进程的退出状态信息。
• 否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程。
• status不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究status低16 比特位):
注意下面代码,我调整了退出码并且创建了一个变量status,取地址作为waitpid的第二个参数:
虽然我退出的是1,但是这里是256,原因就是把status当做位图来看:
如果想要正常显示就需要用到位运算:
上面说的全是正常退出的情况,那么如果有异常呢?
前面说了,后七位是终止信号的标志位,如果后七位全为0,那么前面的退出状态才有效(也就是没有异常退出):
先来看一个异常:
子进程我改为死循环,然后使用kill -9 15505杀死这个进程,那么父进程的wait就会等到这个信号:
注意exit code无意义。
wait从哪里获取的status?
我们知道,进程在被杀死后就会进入僵尸状态,在task_struct内部会记录我们此时的退出状态和信号信息等,父进程进行系统调用(wait)找到子进程,把信息按位操作通过status交到父进程所对应的地址空间上。
WEXITSTATUS获取子进程退出码
其实就是一个宏
WIFEXITED是否异常退出
如果是正常的运行完了代码就是真
否则是假,这里写一个假的情况,子进程发生了除0异常:
下面就是异常退出
3.4阻塞和非阻塞等待(第三个参数)
上面说,子进程没有结束,带有wait 的父进程会等待,这里的等待就是阻塞等待。 在阻塞等待的过程中,父进程什么也干不了。
而非阻塞等待父进程就可以做自己的事情,可以查看下面的代码,我模拟了一下父进程在非阻塞等待的工作:
#include<stdio.h>
#include<string.h>
#include<errno.h>
#include<stdlib.h>
#include<unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
typedef void (*func_t)();
#define NUM 5
func_t handles[NUM+1];
void func_one()
{
printf("这是任务1\n");
}
void func_two()
{
printf("这是任务2\n");
}
void func_three()
{
printf("这是任务3\n");
}
//注册
void regist(func_t h[],func_t f)
{
int i=0;
for(;i<NUM;i++)
{
if(h[i]==NULL) break;
}
if(i==NUM) return;
h[i]=f;
h[i+1]=NULL;
}
int main()
{
//功能入数组
regist(handles,func_one);
regist(handles,func_two);
regist(handles,func_three);
pid_t id=fork();
if(id==0)
{
int cnt=3;
while(1)
{
printf("我是子进程,pid:%d,ppid:%d\n",getpid(),getppid());
sleep(1);
cnt--;
// int a=10;
// a/=0;
}
exit(1);
}
while(1)
{
int status=0;
pid_t rid=waitpid(id,&status,WNOHANG);
if(rid>0)
{
printf("wait success,rid:%d,exit code:%d,exit signal:%d\n",rid,WEXITSTATUS(status),status&0x7F);
break;
}
else if(rid==0)
{
int i=0;
//函数指针进行回调
for(;handles[i];i++)
{
handles[i]();
}
printf("本轮调用结束,子进程没有退出\n");
sleep(1);
}
else
{
printf("等待失败\n");
break;
}
}
}
梳理一下:
1.fork创建子进程,返回值小于0是失败,等于0是子进程,大于0是父进程。
2.子进程里是死循环,也就是说如果是阻塞状态的话,用wait,阻塞状态父进程就要一直等着。
3.所以我们就需要换一种方式,我们采用waitpid的系统调用,它的第三个参数默认是0,表示阻塞等待。
4.我们不需要用默认的0,我们需要的是WNOHONG这个宏,它可以让waitpid不在去等待(前提是子进程没有结束),直接让waitpid返回0。
5.光返回0还不够,我们需要在父进程的外部加一个循环(非阻塞轮询),使得父进程的waitpid隔一段时间就查一下,而隔一段的这个时间,父进程就可以干自己的事情,具体怎么干,参考上面的代码
四.进程程序替换
fork() 之后,父子各自执行父进程代码的一部分如果子进程就想执行一个全新的程序呢?进程的程序 替换来完成这个功能!程序替换是通过特定的接口,加载磁盘上的一个全新的程序(代码和数据),加载到调用进程的地址空间 中!
4.1替换原理
用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。
4.2替换函数
#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[]);
效果:
原本会打印出来两个,这里就被替换成了ls命令:
这是成功的情况,那如果我查找命令错误了呢?
我们发现如果execl函数如果发生了错误那么就不会发生程序替换。
4.2.1函数解释
• 这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回。
• 如果调用出错则返回-1
• 所以exec函数只有出错的返回值而没有成功的返回值。
所以exec*系列的函数,不用做返回值判断,只要返回了就是失败
4.2.2函数的使用与理解
如果我不想把原来的代码替换,但依然可以执行execl里面的命令:
进程独立性,写时拷贝
当然我们自己的程序也行
在当前目录下我创建一个:
execl里面路径就也要改一下:
之前说的替换不会创建新的进程,我们也可以看到:
它们的pid 是一样的:
4.3浅谈exec*
4.3.1execl
第一个参数:路径+程序名
第二个参数:这里是可变参数,命令行怎么写,这里就怎么传,但是最后一个一定要是NULL
4.3.2execlp
假如调用ls:
后面的p(PATH),execlp会自动在环境变量查找指定的命令,因为是环境变量,所以除非我们自己修改PATH否则就只能使用系统级别的环境变量。
4.3.3execv
这个其实就是函数不再提供可变模版参数了,提供一个命令行参数表,这里需要我们自己去写一个指针数组。相当于ls命令的main函数参数表。
4.3.4execvp
4.3.5execvpe
此时other里面main函数的参数就只是我们自己传过来的,而从系统获取的就不在用:
那么又有问题了,如果我都想用呢?
可以用我们上面的execvp:
如果是增量式的添加环境变量呢?
或者直接putenv:
然后使用execvp,因为putenv函数将指定的环境变量添加到当前进程的环境中,所以我们在实现了other的查看环境变量,就可以查到我们自己的环境变量。
注意上面是对系统调用的语言层面的封装,下面才是系统调用
4.4 命名理解
• l(list):表⽰参数采用列表
• v(vector):参数用数组
• p(path):有p自动搜索环境变量PATH
• e(env):表示自己维护环境变量
#include <unistd.h>
int main()
{
char* const argv[] = { "ps", "-ef", NULL };
char* const envp[] = { "PATH=/bin:/usr/bin", "TERM=console", NULL };
execl("/bin/ps", "ps", "-ef", NULL);
// 带p的,可以使用环境变量PATH,无需写全路径
execlp("ps", "ps", "-ef", NULL);
// 带e的,需要自己组装环境变量
execle("ps", "ps", "-ef", NULL, envp);
execv("/bin/ps", argv);
// 带p的,可以使用环境变量PATH,⽆需写全路径
execvp("ps", argv);
// 带e的,需要自己组装环境变量
execve("/bin/ps", argv, envp);
exit(0);
}
五.自主shell命令行解释器
5.1打印命令行
当我们在命令行的时候会出现这么一段,分别是用户名,主机名,还有路径:
我们需要通过环境变量来实现:
效果:
清除换行
5.2获取用户输入命令
这里重新写一下上面的实现逻辑,封装一下各个函数:
首先是打印:
获取用户的输入命令:
注意要清除换行符:
5.3命令行分析
因为上面我们传的都是一整个字符串,我们要把这一整个字符串分析出来:
先定义一些全局数据:
我们要写这两个函数来实现功能
这里我们就用strtok来实现分割(注意argc的数):
5.4指行命令
实际上就是进程程序替换的内容:
封装一下:
5.5修改路径的显示
这里用到了C++里面的rfind,反向查找最后的一个单词
然后后面的这个改一下:
5.6内键命令cd和echo
chdir函数
当我们在使用cd这个命令的时候,我们的命令行的打印也会随着改变:
但是这里没有变:
因为我们上面都是从环境变量获取,这是我们自己写的shell,我们自己的路径变化了,但是环境变量没有变:
所以这里用环境变量就不太好了
用getcwd系统调用:
当然可以修改一下环境变量:
echo:
这里主要写一下查看退出码的:
5.7获取环境变量
5.8全部代码
#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<string.h>
#define COMMAND_SIZE 1024
#define FORMAT "[%s@%s %s]# "
//下面定义shell的全局数据
#define MAXARGC 128
char *g_argv[MAXARGC];
int g_argc=0;
char cwd[1024];
char cwdenv[1024];
int lastcode=0;
//环境变量表
#define MAX_ENVS 100
char *g_env[MAX_ENVS];
int g_envs=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 = getpwd("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;
}
//路径提取最后一个
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;
g_argv[g_argc++]=strtok(commandline,SEP);
while((bool)(g_argv[g_argc++] = strtok(nullptr,SEP)));//最后一个是NULL
g_argc--;//NULL也会算上,这里把这个减去
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]);
}
printf("argc:%d\n",g_argc);
}
//两个内键命令,cd和echo
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;
}
}
return true;
}
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=="-")
{
}
else if(where=="~")
{
}
else
{
chdir(where.c_str());
}
}
return true;
}
//检查内键命令
bool CheckAndExecBuiltin()
{
std::string cmd=g_argv[0];
if(cmd=="cd")
{
Cd();
return true;
}
else if(cmd == "echo")
{
Echo();
return true;
}
return false;
}
//命令命令执行
int Execute()
{
pid_t id=fork();
if(id==0)
{
//child
execvp(g_argv[0],g_argv);
exit(1);
}
//father
int status = 0;
pid_t rid=waitpid(id,&status,0);
if(rid>0)
{
lastcode=WEXITSTATUS(status);
}
return 0;
}
void InitEnv()
{
extern char** environ;
memset(g_env,0,sizeof(g_env));
g_envs=0;
//从配置文件夹里来
//1.获取环境变量
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]=NULL;
//2.导成环境变量
for(int i=0;g_env[i];i++)
{
putenv(g_env[i]);
}
environ=g_env;
}
int main()
{
//shell启动时,从系统里获取环境变量
//我们自己的环境变量信息应该从父shell里来
InitEnv();
while(true)
{
//1.输出命令行提示符
PrintCommandPrompt();
//2.获取用户输入的命令
char commandline[COMMAND_SIZE];
if(!GetCommandLine(commandline,sizeof(commandline)))
continue;
//3.命令行分析
CommandParse(commandline);
//PrintArgv();
//4.
if(CheckAndExecBuiltin())
continue;
//5.执行命令
Execute();
}
return 0;
}