目录
[3学习到进程程序替换, 微型shell](#3学习到进程程序替换, 微型shell)
[5学习到进程终止,认识?](#5学习到进程终止,认识?)
while :; do ps axj | head -1 && ps axj | grep testWait | grep -v grep ; sleep 1; echo "------------------------------"; done
这是一个监视进程的脚本
1学习进程创建,fork/vfork
用循环的方式一次性创建一批进程等等上一节博客已经完成了
2学习到进程等待
1什么是进程等待?
通过系统调用wait/waitpid,来进行
2为什么要进程等待?
僵尸进程,无法kill,需要通过进程等待来杀掉他,进而解决内存泄漏问题
我们要通过进程等待,获得子进程的推出情况---知道我布置给子进程的任务,它完成怎么样了--
3怎么办?
父进程通过wait/waitpid进行僵尸进程的回收问题!
等待是必须的,wait()是等待任意一个进程,对于多进程的等待问题,你用for循环创建了几个进程,就要用for循环,调用几个wait()
wait当任意一个子进程退出的时候,wait回收了这个进程,如果父进程wait等待子进程退出的时候,子进程如果不退出,父进程默认在wait的时候,调用这个系统调用的时候,也就不返回,默认叫做阻塞状态!
waitpid() : 可以指定等待某个子进程
代码1 : 多进程等待问题
cpp
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <sys/types.h>
#define N 5
void ChildFun()
{
int cnt = 5;
while(cnt)
{
printf("I am child PID:%d,PPID:%d,cnt:%d\n",getpid(),getppid(),cnt);
cnt--;
sleep(1);
}
}
int main()
{
int i = 0;
for(i = 0;i<N;i++)
{
pid_t id = fork();
if(id<0)
{
perror("fork");
exit(0);
}
else if(id==0)
{
//child
ChildFun();
exit(1);
}
printf("create child process: %d succees\n",id);
}
//parent
int cnt = 10;
while(cnt)
{
printf("I am father PID:%d,PPID:%d,cnt:%d\n",getpid(),getppid(),cnt);
cnt--;
sleep(1);
}
for(i = 0;i<N;i++)
{
pid_t id = wait(NULL);
if(id>0)
{
printf("wait %d success\n",id);
}
}
sleep(5);
return 0;
}
status : 可以获得子进程的退出的情况,通过指针把函数内部的参数带出来
第三个参数options给0的时候,父进程等待的方式就是阻塞的方式
另外一个就是WNOHANG
这时候会有返回值ret
1ret>0的时候就是等待成功
2ret=0的时候就是子进程还没有退出,未就绪
3ret<0的时候就是等待失败
阻塞其实就是父进程不能再执行其他的任务了
非阻塞轮询: 非阻塞+循环,这个就是父进程wait的时候发现子进程还在运行,那么父进程不会阻塞,但是一直重复去判断子进程是否在运行
非阻塞轮询+自己的事情:发现子进程还在运行的时候,直接返回执行自己的事情,一会再去重新调用wait
但是问题就是我们只有非阻塞,这个轮询要我们自己要加上,其实就是在waitpid()外面一层加一个while循环,当正常退出的时候用break跳出这个while循环
子进程有几种退出场景?其实就是这些场景
父进程等待,期望获得子进程的哪些信息?
一般status(int)我们用低16个bit位,如果正常退出,不会发送信号,低7位都为0
int status = 0;
wait(&status);
信号: status&0x7F 退出码(status>>8)&0xFF
上面这种还要去自己用位运算去处理,下面这两个宏直接可以去判断,不用自己去运算
WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
父进程要拿子进程的状态数据,任意数据,为什么要用wait等系统调用呢? -> 进程具有独立性
总结来说,wait其实就是通过操作系统获得子进程的退出码和信号,并且把它从Z状态给释放
3学习到进程程序替换, 微型shell
替换原理
用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数 以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动 例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。
替换函数
有六种exec开头的函数,原理都是相同的,只是传参的不同
第一个参数是为了解决先找到这个路径,后面的参数就是给这个程序传什么,怎么执行
cpp#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 execvpe(const char *file, char *const argv[],char *const envp[]); int execve(const char *path, char *const argv[], char *const envp[]);现象:程序替换成功后,exec*后续的代码不会被执行,替换失败才可能执行后续代码
exec* 函数,只有失败的返回值,没有成功的返回值
小知识:linux 中形成的可执行程序是有格式的,ELF,可执行程序的表头,可执行程序的入口地址就在表头当中
替换的原理:
因为我们知道,执行一个可执行文件的时候,会把磁盘中的可执行文件的代码和数据放到物理内存中,替换其实就是将你所要替换的可执行文件的代码和数据替换当前进程的代码和数据,注意!进程没有改变,pid没有变
命名理解:
l(list) : 表示参数采用列表
v(vector) : 参数用数组(就是不用可变参数,而是用字符串指针数组)
p(path) : 有p自动搜索环境变量PATH(带p会通过环境变量找到路径,那么传这个文件比如"ls", 或者也可给路径如"/usr/bin/ls")
e(env) : 表示自己维护环境变量
注意传参最后都以NULL结尾
Makefile一次编译形成两个可执行文件
事实上,只有execve是真正的系统调用,其它五个函数最终都调用 execve,所以execve在man手册 第2节,其它函数在 man手册第3节。
这些函数之间的关系如下图所示。 下图exec函数族 一个完整的例子:
所谓脚本,就是命令行解释器把脚本里面的代码一行一行拿出来执行脚本以.sh为结尾,而且脚本文件以#!/usr/bin/bash为开头
而且运行脚本的时候用bash 脚本.sh来执行脚本文件
无论是我们的可执行程序,还是脚本,为什么能够跨语言调用?
因为所有语言运行起来都是进程
环境变量什么时候给进程的?
环境变量也是数据,创建子进程的时候,环境变量已经被子进程继承下去了!
所以程序替换中,环境变量信息不会被替换
extern char **environ; 这个environ变量在unistd.h中定义了,就是进程的环境变量
那么就可以看出,如何给替换的子进程传递环境变量?(可用第三个参数)
1 父进程putenv
2可以传递环境变量 environ
3可以自定义环境变量 char* const myenv[] = {"x1=111"}然后将这个作为参数传递的时候,采用的策略是覆盖,不是追加,说白了就是父进程的环境变量全部不要了
4重新认识shell运行原理
我们可以综合前面的知识,做一个简易的shell
命令行其实就是打印一个字符串,有用户名,主机名,路径 在程序中就是用getenv获取环境变量
通过scanf将输入的字符串存入一个数组中(但是scanf读到空格就完了,不能读到空格),我们要用fgets()这个函数
系统打开的时候默认会打开三个流,从stdin中获取输入
函数原型
但是fgets也会获得\n,要处理掉
如何将字符串以空格进行分割 -- 用strtok()
执行的话是要开一个子进程去执行,父进程去等待这个进程
子进程去替换一下就行//,用execvp就行(有数组,而且还包含环境变量)
有一些内建命令要让父进程去跑
有系统调用chdir来改变进程的路径,指令的判断是比较复杂的
1 argv[0]如果是cd --->第二个参数可能是. .. 路径
同时还要修改环境变量 --》sprintf(getenv("PWD"),"%s",argv[1]);这样修改
对于获取当前路径,可以使用getcwd 这个系统调用接口,因为用环境变量获取比较麻烦(路径改变环境变量不会更行)
内建命令其实就是shell内部的一个函数
同时也可以让ls带上颜色
2 echo 打印环境变量的时候会有$为字符串的开头
还要关于关于退出码 argv[1] = $? 的时候是想要获得最近一次的退出码lastcode
3export 直接putenv把环境便令导入(这里还要注意,putenv只是改变里环境变量的一个指针,指向你的这个地址,你只是改变了这个指针,如果有又导入一个,那么你用argv[1]导入的话这个环境变量还会改变,所以可以用一个大的字符数组,用strcpy,将argv[1]追加到里面,这个环境变量表和本地变量表是你自己维护的),而且这个是内建命令,子进程导入的话,父进程是看不到的
完整代码
cpp
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#define LEFT "["
#define RIGHT "]"
#define LABLE "#"
#define LINE_SIZE 1024
#define ARGV_SIZE 32
#define DELIM " \r"
#define EXIT_CODE 44
int lastcode = 0;
int quit = 0;
char commandline[LINE_SIZE];
char *argv[ARGV_SIZE];
char pwd[LINE_SIZE];
char myenv[LINE_SIZE];
const char* getusername()
{
return getenv("USER");
}
const char* gethostname()
{
return getenv("HOSTNAME");
}
void getpwd()
{
getcwd(pwd,sizeof(pwd));
}
void Interact(char* cline,int size)
{
getpwd();
printf(LEFT"%s@%s %s"RIGHT""LABLE,getusername(),gethostname(),pwd);
char* s = fgets(cline,size,stdin);
assert(s);
(void)s;
//abcd\n\0
cline[strlen(cline)-1]='\0';
}
int splitstring(char cline[],char* _argv[])
{
int i = 0;
argv[i++]=strtok(cline,DELIM);
while(_argv[i++] = strtok(NULL,DELIM));
return i-1;
}
void NormalExcute(char* _argv[])
{
pid_t id = fork();
if(id<0)
{
perror("fork");
return;
}
else if(id==0)
{
//子进程
execvp(_argv[0],_argv);
exit(EXIT_CODE);
}
else
{
int status = 0;
pid_t ret = waitpid(id,&status,0);
if(ret==id)
{
lastcode = WIFEXITED(status);
}
}
}
int InterCommand(char* _argv[],int _argc)
{
if(_argc==2 && strcmp(_argv[0],"cd")==0)
{
chdir(_argv[1]);
getpwd();//更新pwd的目录,能刷新到命令行
//更新环境变量
sprintf(getenv("PWD"),"%s",pwd);
return 1;
}
else if(_argc==2 && strcmp(_argv[0],"echo")==0)
{
if(strcmp(_argv[1],"$?")==0)
{
printf("%d\n",lastcode);
lastcode = 0;
}
else if(*_argv[1]=='$')
{
printf("%s\n",getenv(_argv[1]+1));
}
else
{
printf("%s",_argv[1]);
}
return 1;
}
else if(_argc==2 && strcmp(_argv[0],"export")==0)
{
strcpy(myenv,_argv[1]);
putenv(myenv);
return 1;
}
if(strcmp(_argv[0],"ls")==0)
{
_argv[_argc++] = "--color";
_argv[_argc] = NULL;
}
return 0;
}
int main()
{
while(!quit)
{
//1
//2交互问题,获取命令行--> ls -a -l
Interact(commandline,sizeof(commandline));
//3字串分割问题,解析命令
int argc = splitstring(commandline,argv);
if(argc==0) continue;
//test
// for(i = 0;argv[i];i++)
// {
// printf("[%d]:%s\n",i,argv[i]);
// }
//4指令的判断,要判断是否是内建命令
int n = InterCommand(argv,argc);
//5普通命令的执行
if (!n) NormalExcute(argv);
}
return 0;
}
所以我们进行登录的时候,系统就是要启动一个shell进程,我们shell本身的环境变量是从哪里来的?
当用户登陆的时候,shell会读取用户目录下的.bash_profile文件,里面保存了导入环境变量的方法
5学习到进程终止,认识$?
进程退出的原因
1代码运行完毕,结果正确
2代码运行完毕,结果不正确
3代码异常终止
可以用return 来返回不同的数字,表征着不同的出错原因
所以main函数的返回值,本质表示:进程运行完成时候的正确结果,如果不是,可以用不同的数字表示不同的出错原因
用echo $? 就可以拿到进程退出的退出码
$? 表示最近一次进程退出的退出码
strerror(): 将错误码转换为错误码描述,这个是用系统的错误描述,自己也可以写一个数组来搞自己的错误码描述
errno: 是一个用于表示错误代码的变量,定义在 <errno.h> 头文件中,当系统调用或库函数发生错误时,会设置 errno 为相应的错误码,你再调用strerror(errno)就可以得到你操作的错误原因
perror(str): 这个会直接打印出程序errno的错误码的描述,通过你加一些str ,就是输出str : 错误码描述
代码异常终止的时候,错误码就没有用了
进程出现了异常,本质上就是进程收到了信号!!
我们是先判断收到没有信号,再去看进程的退出码
父进程为什么要关心子进程的错误码?
父进程把错误码转交给用户
return是一种更常见的退出进程方法。执行return等同于执行exit(n),因为调用main的运行时函数会将main的返 回值当做 exit的参数。exit()在任意地方被调用,都表示进程退出,而return 在函数中是函数返回,在main中是指进程退出
_exit()是一个系统调用接口,系统层面直接关闭进程
exit()是一个函数,调用的时候还会在函数当中进行其他操作
printf会把数据写入缓冲区中













