目录
接着上文的进程概念:现在让我们来更深入地理解进程内部的相关知识
一总结
我们在没学过进程时,给它的定义是:进程=PCB + 代码和数据;而现在理解了进程来对它进行进一步的定义:
进程=内核的相关管理数据结构(task_struct + 虚拟地址空间 + 页表) + 代码(共享) + 数据(写时拷贝)
有了它,再次来理解前面所说的进程之间具有独立性:
进程之间的运行是互不干扰的。父进程创建子进程:子进程继承父进程的代码和数据(只读),一旦发生更改,会发生写时拷贝,保证父子进程之间的独立性!
二fork函数
在前面我们已经对fork函数有了一定的了解,但它的内部是如何实现父进程创建子进程的呢?
我们借助图来解释:
在fork()之前(未执行fork()函数) ,只有父进程在走;在执行fork()时,内部会拷贝(浅拷贝)一份与父进程一样的进程,为子进程:表示子进程在fork()内部已经可以随时准备可以被调度了;
而fork函数为了能更好地管理父子进程(对应关系),设置了不同的返回值:
~ 子进程返回0
~ 父进程返回的是子进程的pid
让我们能很方便来用fork()函数创建子进程来进行学习!
三进程的终止
1先明白
进程的终止是在干什么? 释放曾经的代码和数据?
不准确:释放曾经的代码和数据所占的空间与相关内核数据结构
内核数据结构没有释放,就会导致Z状态:僵尸状态
2退出码&&退出信号
平时在写代码时,在代码执行完后最后总要加上 return 0来告诉编译器说我们的代码执行完了;但其实在编译器眼中,return 0还表示你的代码执行的结果是正确的才返回0的。
代码运行完后,一定要返回每个值来保证程序的完整。我们把这个非0返回值叫做:错误码
错误码有它对应的错误描述信息,我们可以打印出来看看:
如果要在Linux中获取进程的退出码,我们可以:
使用echo内建命令,打印的就是bash内部的变量数据(指令大都是bash创建子进程来执行的)
所以:
我们可以认为:进程终止可以总结为三种情况:
Ⅰ 代码跑完,结果正确
Ⅱ 代码跑完,结果不正确
Ⅲ 代码执行时,出现异常提前退出
代码能跑完我们就要关注它对应的退出码来看看结果;
代码没跑完出现异常返回的退出码通常是乱码:编译器在编译运行的时候,崩溃了——OS发现你的进程(代码)做了不该做的事情提前把进程终止;一旦出现这种情况,退出码也就没有了意义!
进程出异常要进行终止(OS介入),本质上是因为进程收到OS的退出信号
总结:
进程终止:
1.先确定是否异常;2.不是异常,就一定是代码跑完了,看退出码就行
衡量一个进程退出,只需关注两个数字:退出码,退出信号!
3做法
a. main函数return ,表示进程终止(非main函数return,表示函数结束)
b.调用exit()函数,提前终止进程
c.系统调用_exit(),提前终止进程
这两个函数唯一的区别是:exit在进程退出的时候,会冲刷缓冲区;而_exit不会。
exit在完成执行用户定义的清理函数,冲刷缓存,关闭流等后,也会去调用_exit()来终止进程:这里说的冲刷缓存的缓冲区不是内核缓冲区,而是语言级别的缓冲区(后面写IO时说)
四阻塞等待&&非阻塞等待
前面我们说了:如果子进程退出时父进程不管不顾,子进程对应的状态为Z(僵尸)状态会一直存在,造成内存泄漏。
1怎么办
父进程可以通过等待获取子进程的退出信息,知道子进程是由于什么原因退出的;回收系统资源
2等待函数
pid_t wati(int *status) 等待任意一个子进程退出 这里参数暂时不管设为NULL
等待成功时返回子进程的id
~ 父进程在等待时当前状态是阻塞等待;如何理解?
父进程在阻塞等待,OS就把它的状态设置为S并把它的PCB链入到子进程的队列中;
当子进程退出时,OS就把子进程队列里的父进程给唤醒过来,wait执行后在返回就得到了子进程的id
子进程本身就是软件,父进程本质是在等待某种软件条件就绪~
pid_t waitpid(pid_t pid , int *status , int options)
其中,int *status 是一个输出型参数,与scanf("%d",&ret)的使用一样,不过它的输入是由OS来进行输入,输入的是子进程的退出信息;而在上面我们知道:退出信息有两个:退出码和退出信号
而status便是这两个的结合而成的值,要想知道两个退出值必须对它进行翻译:
0000 0000 0000 00000000 0000 0000 0000 红色表示退出信号 蓝色表示退出码
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
void ChildRun()
{
int *p = NULL;
int cnt = 5;
while(1)
{
printf("I am child process, pid: %d, ppid:%d, cnt: %d\n", getpid(), getppid(), cnt);
sleep(1);
cnt--;
*p = 100;
}
}
int main()
{
printf("I am father, pid: %d, ppid:%d\n", getpid(), getppid());
pid_t id = fork();
if(id == 0)
{
// child
ChildRun();
printf("child quit ...\n");
exit(123);
}
sleep(7);
// fahter
//pid_t rid = wait(NULL);
int status = 0;
pid_t rid = waitpid(id, &status, 0);
if(rid > 0)
{
printf("wait success, rid: %d\n", rid);
}
else
{
printf("wait failed !\n");
}
sleep(3);
printf("father quit, status: %d, child quit code : %d, child quit signal: %d\n", status, (status>>8)&0xFF, status & 0x7F);
}
把status通过&来看到最终的两个信号的值
我们也可以调用系统接口WEXITSTATUS(status)来得到子进程的退出码
我们只是想让子进程的结果(退出码和退出信号)是否成功告诉给父进程就行,那么我们定义全局变量exit_code,exit_signal来使用能行吗?
答案是:不行!父进程无法看到子进程定义的全局变量
3非阻塞等待
如果子进程没有退出,则父进程就在执行waitpid时进行阻塞等待;那在这期间,能不能让父进程在等待过程中干一些自己的事呢(执行要完成的代码)?
pid_t waitpid(pid_t pid , int * status , int options)
返回值有三种情况:
>0 等待成功,子进程退出返回子进程的pid,回收成功
<0 等待失败
==0 检测是成功的,只不过子进程没有退出,需要你进行下一步的等待
我们就可以从返回值为0入手:让父进程在等待时发现子进程还没退出后,来允许父进程做自己的事;
设计:1.while + 在非阻塞等待的时候 + 父进程自己的事 ->非阻塞轮询
2.将waitpid的options更改为WNOHANG
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include "task.h"
typedef void(*func_t)();
#define N 3
func_t tasks[N] = {NULL};
void LoadTask()
{
tasks[0] = PrintLog;
tasks[1] = Download;
tasks[2] = MysqlDataSync;
}
void HandlerTask()
{
for(int i = 0; i < N; i++)
{
tasks[i](); // 回调方式
}
}
// fahter
void DoOtherThing()
{
HandlerTask();
}
void ChildRun()
{
//int *p = NULL;
int cnt = 5;
while(cnt)
{
printf("I am child process, pid: %d, ppid:%d, cnt: %d\n", getpid(), getppid(), cnt);
sleep(1);
cnt--;
//*p = 100;
}
}
int main()
{
printf("I am father, pid: %d, ppid:%d\n", getpid(), getppid());
pid_t id = fork();
if(id == 0)
{
// child
ChildRun();
printf("child quit ...\n");
exit(123);
}
LoadTask();
// father
while(1)
{
int status = 0;
pid_t rid = waitpid(id, &status, WNOHANG); // non block
if(rid == 0)
{
usleep(100000);
printf("child is running, father check next time!\n");
DoOtherThing();
}
else if(rid > 0)
{
if(WIFEXITED(status))
{
printf("child quit success, child exit code : %d\n", WEXITSTATUS(status));
}
else
{
printf("child quit unnormal!\n");
}
break;
}
else
{
printf("waitpid failed!\n");
break;
}
}
五进程程序替换
先来看代码&&现象:
#include<stdio.h>
#include<unistd.h>
int main()
{
printf("testexec ... begin!");
execl(“/user/bin/ls” , "ls" , "-l" , "-a" , NULL);
printf("testexec ... end!\n");
return 0;
}
执行我们写的程序时,竟然有指令 ls -a -l的功能;execl函数竟然怎么神奇!!
1分析
我们在执行我们的代码时,其中的execl()函数会根据传入的参数来执行起来新的程序;在execl()函数之前,打印信息能够被打印出来;在执行该函数后,后续的代码不见了:因为被execl()函数替换了,向后执行了新的程序(user/bin/ls的可执行程序)
我们要知道:指令能够被执行的原因,是我们它本身就是一个可执行程序(文件)
2原理
以上面代码为例子:当还没有执行execl函数进行替换时,ls的程序(代码和文件)都保存在磁盘中;但进行替换时,本质上就是ls的程序被加载到内存中,把我们excel后的代码完全替换为ls的代码;这样就形成了一个新的程序。./运行可执行程序就是在执行它
那么在进程的程序替换过程里,有没有创建新的进程呢?
没有:调用exec前后该进程的id并未发生改变
3替换函数
有六种以exec开头的函数,统称exec函数:
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 execve(const char *path, char *const argv[], char *const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
参数:const char *path 我们要执行的文件名 char *const argv[] 命令行参数表
const char *arg 命名行中怎么执行就这么传参
const char *path 传入文件所在的路径
函数之间的规律:
l(list) : 表示参数采用列表
v(vector) : 参数用(命令行参数,环境变量)数组
p(path) : 有p自动搜索环境变量PATH
e(environment) : 表示自己维护环境变量(可自己传环境变量表)
事实上,只有execve是真正的系统调用,其它五个函数最终都调用 execve,所以execve在man手册 第2节,其它函数在man手册第3节。这些函数之间的关系如下图所示 :
上面的程序替换,我们替换的都是系统命令,可不可以来替换我们自己写的程序呢?
分别写两个代码:mypragma.cpp里是进行打印工作;testtxe中创建子进程来进行替换工作:
// testexe.c
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
printf("testexec ... begin!\n");
pid_t id = fork();
if(id == 0)
{
putenv("HHHH=111111111111111111");
// 我的父进程本身就有一批环境变量!!!, 从bash来
char *const argv[] =
{
(char*)"mypragma",
(char*)"-a",
(char*)"-b",
NULL
};
//char *const envp[] =
//{
// (char*)"HAHA=111111",
// (char*)"HEHE=222222",
// NULL
//};
extern char**environ;
printf("child pid: %d\n", getpid());
sleep(2);
execvpe("./mypragma", argv, environ);
exit(1);
}
// fahter
int status = 0;
pid_t rid = waitpid(id, &status, 0);
if(rid > 0)
{
printf("father wait success, child exit code: %d\n", WEXITSTATUS(status));
}
printf("testexec ... end!\n");
return 0;
}
// mypragma.cpp
#include <iostream>
#include <unistd.h>
using namespace std;
int main(int argc, char *argv[], char *env[])
{
int i = 0;
for(; argv[i]; i++ )
{
printf("argv[%d] : %s\n", i, argv[i]);
}
printf("-------------------------------\n");
for(i=0; env[i]; i++)
{
printf("env[%d] : %s\n", i, env[i]);
}
printf("-------------------------------\n");
cout << "hello C++, I am a C++ pragma!: " << getpid() << endl;
cout << "hello C++, I am a C++ pragma!: " << getpid() << endl;
cout << "hello C++, I am a C++ pragma!: " << getpid() << endl;
cout << "hello C++, I am a C++ pragma!: " << getpid() << endl;
return 0;
}
执行testexe,可以看到myprogma程序的内容可以被替换从而实现出来,且两者的pid是相同的,证明了程序替换确实是不会创建新的进程的!
结论:程序替换可以支持不用的应用场景,语言之间不同也可以进行!
六简易Shell
有了上面的储备知识,我们就可以来制作与XShell类似的Shell了,流程:
1打印用户行
从左向右依次是:(环境变量)username(用户名),hostname(主机名),cwd(所在路径),标识符(#表示超级用户,$表示普通用户)(我们自己设计成 >);将这些信息填写在字符数组中打印出来就可以了。
在环境变量里,cwd的路径是完整的路径;我们要的只是当前路径的目录就行,要对它进行特殊处理:将这个路径往后进行遍历,直到找到’/‘后就停止
#define SIZE 512
#define ZERO '\0'
#define SEP " "
#define NUM 32
#define SkipPath(p) do{ p += (strlen(p)-1); while(*p != '/') p--; }while(0)
char cwd[SIZE*2];
char *gArgv[NUM];
int lastcode = 0;
const char *GetUserName()
{
const char *name = getenv("USER");
if(name == NULL) return "None";
return name;
}
const char *GetHostName()
{
const char *hostname = getenv("HOSTNAME");
if(hostname == NULL) return "None";
return hostname;
}
const char *GetCwd()
{
const char *cwd = getenv("PWD");
if(cwd == NULL) return "None";
return cwd;
}
// commandline : output
void MakeCommandLineAndPrint()
{
char line[SIZE];
const char *username = GetUserName();
const char *hostname = GetHostName();
const char *cwd = GetCwd();
SkipPath(cwd);
snprintf(line, sizeof(line), "[%s@%s %s]> ", username, hostname, strlen(cwd) == 1 ? "/" : cwd+1);
printf("%s", line);
fflush(stdout);
}
2获取用户命令字符串
用字符数组来存储用户输入的信息(本质上是字符串),用fgets来接收(不会遇到空格停止)
int GetUserCommand(char command[], size_t n)
{
char *s = fgets(command, n, stdin);
if(s == NULL) return -1;
command[strlen(command)-1] = ZERO;
return strlen(command);
}
char usercommand[SIZE];
int n = GetUserCommand(usercommand, sizeof(usercommand));
if(n <= 0) return 1;
3分割用户输入字符串
用户输入的指令中会带有指令的选项,比如:ls -a -l ;我们要对它进行分割就要用到strtoke()函数来使用(按空格来进行一个一个分割字符串)
void SplitCommand(char command[], size_t n)
{
(void)n;
// "ls -a -l -n" -> "ls" "-a" "-l" "-n"
gArgv[0] = strtok(command, SEP);
int index = 1;
while((gArgv[index++] = strtok(NULL, SEP)));
// done, 故意写成=,表示先赋值,在判断. 分割之后,strtok会返回NULL,刚好让gArgv最后一个元素是NULL, 并且while判断结束
}
5执行命令
创建一个子进程来执行程序替换:将分割好的字符数组gArgv用替换函数来执行对应指令的可执行程序;父进程就只需等待子进程退出即可
void ExecuteCommand()
{
pid_t id = fork();
if(id < 0) Die();
else if(id == 0)
{
// child
execvp(gArgv[0], gArgv);
exit(errno);
}
else
{
// fahter
int status = 0;
pid_t rid = waitpid(id, &status, 0);
if(rid > 0)
{
lastcode = WEXITSTATUS(status);
if(lastcode != 0) printf("%s:%s:%d\n", gArgv[0], strerror(lastcode), lastcode);
}
}
}
4检查是否为内建命令
写好后,代码基本上能够进行使用了;但在使用时有个问题:凡是涉及到内建命令就会失败,我们要进行单独处理:
以cd为例:但判断gArgv[0]为cd时,为了代码的整洁,重新写个处理指令cd的函数;
要让pwd的路径与cd移动后的路径相同,我们就要对环境变量PWD更改才能做到;
用char* path来接收要更新的路径,用chdir()函数来对当前路径进行更改;
刷新环境变量:用getcwd()函数来获取当前工作路径存储到temp中;单独来维护PWD的环境变量用cwd来维护,将temp的路径写道cwd中再putenv导入环境变量就ok了(此时PWD的路径就用cwd来维护了)
char cwd[SIZE*2];
char *gArgv[NUM];
int lastcode = 0;
const char *GetHome()
{
const char *home = getenv("HOME");
if(home == NULL) return "/";
return home;
}
void Cd()
{
const char *path = gArgv[1];
if(path == NULL) path = GetHome();
// path 一定存在
chdir(path);
// 刷新环境变量
char temp[SIZE*2];
getcwd(temp, sizeof(temp));
snprintf(cwd, sizeof(cwd), "PWD=%s", temp);
putenv(cwd);
}
int CheckBuildin()
{
int yes = 0;
const char *enter_cmd = gArgv[0];
if(strcmp(enter_cmd, "cd") == 0)
{
yes = 1;
Cd();
}
else if(strcmp(enter_cmd, "echo") == 0 && strcmp(gArgv[1], "$?") == 0)
{
yes = 1;
printf("%d\n", lastcode);
lastcode = 0;
}
return yes;
}
源代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#define SIZE 512
#define ZERO '\0'
#define SEP " "
#define NUM 32
#define SkipPath(p) do{ p += (strlen(p)-1); while(*p != '/') p--; }while(0)
// 为了方便,我就直接定义了
char cwd[SIZE*2];
char *gArgv[NUM];
int lastcode = 0;
void Die()
{
exit(1);
}
const char *GetHome()
{
const char *home = getenv("HOME");
if(home == NULL) return "/";
return home;
}
const char *GetUserName()
{
const char *name = getenv("USER");
if(name == NULL) return "None";
return name;
}
const char *GetHostName()
{
const char *hostname = getenv("HOSTNAME");
if(hostname == NULL) return "None";
return hostname;
}
// 临时
const char *GetCwd()
{
const char *cwd = getenv("PWD");
if(cwd == NULL) return "None";
return cwd;
}
// commandline : output
void MakeCommandLineAndPrint()
{
char line[SIZE];
const char *username = GetUserName();
const char *hostname = GetHostName();
const char *cwd = GetCwd();
SkipPath(cwd);
snprintf(line, sizeof(line), "[%s@%s %s]> ", username, hostname, strlen(cwd) == 1 ? "/" : cwd+1);
printf("%s", line);
fflush(stdout);
}
int GetUserCommand(char command[], size_t n)
{
char *s = fgets(command, n, stdin);
if(s == NULL) return -1;
command[strlen(command)-1] = ZERO;
return strlen(command);
}
void SplitCommand(char command[], size_t n)
{
(void)n;
// "ls -a -l -n" -> "ls" "-a" "-l" "-n"
gArgv[0] = strtok(command, SEP);
int index = 1;
while((gArgv[index++] = strtok(NULL, SEP))); // done, 故意写成=,表示先赋值,在判断. 分割之后,strtok会返回NULL,刚好让gArgv最后一个元素是NULL, 并且while判断结束
}
void ExecuteCommand()
{
pid_t id = fork();
if(id < 0) Die();
else if(id == 0)
{
// child
execvp(gArgv[0], gArgv);
exit(errno);
}
else
{
// fahter
int status = 0;
pid_t rid = waitpid(id, &status, 0);
if(rid > 0)
{
lastcode = WEXITSTATUS(status);
if(lastcode != 0) printf("%s:%s:%d\n", gArgv[0], strerror(lastcode), lastcode);
}
}
}
void Cd()
{
const char *path = gArgv[1];
if(path == NULL) path = GetHome();
// path 一定存在
chdir(path);
// 刷新环境变量
char temp[SIZE*2];
getcwd(temp, sizeof(temp));
snprintf(cwd, sizeof(cwd), "PWD=%s", temp);
putenv(cwd); // OK
}
int CheckBuildin()
{
int yes = 0;
const char *enter_cmd = gArgv[0];
if(strcmp(enter_cmd, "cd") == 0)
{
yes = 1;
Cd();
}
else if(strcmp(enter_cmd, "echo") == 0 && strcmp(gArgv[1], "$?") == 0)
{
yes = 1;
printf("%d\n", lastcode);
lastcode = 0;
}
return yes;
}
int main()
{
int quit = 0;
while(!quit)
{
// 1. 我们需要自己输出一个命令行
MakeCommandLineAndPrint();
// 2. 获取用户命令字符串
char usercommand[SIZE];
int n = GetUserCommand(usercommand, sizeof(usercommand));
if(n <= 0) return 1;
// 3. 命令行字符串分割.
SplitCommand(usercommand, sizeof(usercommand));
// 4. 检测命令是否是内建命令
n = CheckBuildin();
if(n) continue;
// 5. 执行命令
ExecuteCommand();
}
return 0;
}
以上便是我的学习Linx进程终止所总结的知识点,希望对你有所帮助!
有问题欢迎在评论区里指出,谢谢!!