文章目录
- 前言
- 一、进程的创建
-
- [1.1 fork返回值](#1.1 fork返回值)
- [1.2 写时拷贝](#1.2 写时拷贝)
- [1.3 使用fork的场景和注意情况](#1.3 使用fork的场景和注意情况)
- 二、进程终止
-
- [2.1 进程常⻅退出方法](#2.1 进程常⻅退出方法)
-
- [2.1.1 退出码](#2.1.1 退出码)
- [2.2 _exit && exit](#2.2 _exit && exit)
- 三、进程等待
-
- [3.1 wait](#3.1 wait)
- [3.2 waitpid](#3.2 waitpid)
- 四、进程替换
-
- [4.1 替换原理](#4.1 替换原理)
- [4.2 替换函数](#4.2 替换函数)
-
- [4.2.1 函数解释](#4.2.1 函数解释)
- 五、自主Shell命令行解释器
- 总结
前言
前面我们已经掌握了基本的进程概念,从冯诺依曼结构->进程概念->进程优先级和切换->环境变量->虚拟地址的初步了解,本文将来进一步了解其中一些进程发生过程中的细节实现,并说明如何使用一个进程
一、进程的创建
系统调用函数fork(),从已存在进程中创建⼀个新进程。新进程为⼦进程,⽽原进程为⽗进程。
c
#include <unistd.h>
pid_t fork(void);
//pid_t类型等同于int整形类型
//返回值:⾃进程中返回0,⽗进程返回⼦进程id,出错返回-1
进程调⽤fork,当控制转移到内核中的fork代码后,内核做:
- 分配新的内存块和内核数据结构给⼦进程
- 将⽗进程部分数据结构内容拷⻉⾄⼦进程
- 添加⼦进程到系统进程列表当中
- fork返回,开始调度器调度
- fork后的父子进程谁先运行完全由调度器来决定。
1.1 fork返回值
- ⼦进程返回0
- ⽗进程返回的是⼦进程的pid。
- 如果 fork() 调用失败(由于系统资源不足、超出进程限制等原因),它将返回一个负值
1.2 写时拷贝
子进程的创建会伴随着虚拟内存的开辟,也伴随着和父进程相同的数据结构的产生。
父进程有一个名为页表的数据结构,内部会存储数据的权限,在父进程创建子进程的时候内部可读权限的数据不变,而可写权限的数据会变成可读权限,,父子任意⼀⽅试图写⼊数据的时候,便触发这个权限获取需要进行写时拷贝的状态,把这个写入数据的权限变成可读可写,然后写入数据方通过页表改变指向,去指向新开辟的物理地址。
写时拷贝通常通过以下机制实现:
- 引用计数:在分配内存空间时,系统会额外分配一些空间来存放引用计数器,用于记录这块空间的引用次数。当有新的指针指向这块空间时,引用计数加一;当某个指针不再指向这块空间或进程终止时,引用计数减一。
- 写时分配:当某个进程或线程尝试修改共享的数据块时,系统会检查该数据块的引用计数。如果引用计数大于一,说明有其他进程或线程也在共享这块数据。此时,系统会为修改者分配一个新的内存空间,并将原数据块的内容复制到新空间中。然后,修改者在新空间中修改数据,而其他进程或线程仍然访问原数据块。
运用这中操作的原因主要是为了节省内存资源避免不必要的浪费,本质上是延迟按需申请
1.3 使用fork的场景和注意情况
使用场景:
⼀个⽗进程希望复制⾃⼰,使⽗⼦进程同时执⾏不同的代码段。例如,⽗进程等待客⼾端请求,
⽣成⼦进程来处理请求。⼀个进程要执⾏⼀个不同的程序。例如⼦进程从fork返回后,调⽤exec函数
调用fork失败的原因系统中有太多的进程
实际⽤⼾的进程数超过了限制
二、进程终止
进程退出场景
•代码运⾏完毕,结果正确
•代码运⾏完毕,结果不正确
•代码异常终⽌(例如 ctrl + c,主要依赖信号机制,暂不做赘述)
2.1 进程常⻅退出方法
正常终⽌(可以通过 echo $?查看进程退出码):
- 从main返回(main函数结束表示进程结束)
- 调⽤exit
- 调用 _exit
异常退出:
- ctrl + c,信号终⽌
2.1.1 退出码
main函数的返回值,本质表示:进程完成时是否是正确的结果,如果不是,可以用不同的数字表示不同的出错原因
退出码是一个整形,前16比特位无意义,后16比特位用来表示进程的退出场景,其中如果是正常退出,那么0 - 7号比特位为0,15 - 8比特位显示退出状态,获取这个退出状态需要将其右移八位并按位与0xff( 退出状态码>>8 & 0xff) ,异常退出的话低7个比特位不为0,退出状态码无意义。
退出码(退出状态)可以告诉我们最后⼀次执⾏的命令的状态。在命令结束以后,我们可以知道命令是成功完成的还是以错误结束的。其基本思想是,程序返回退出代码 0 时表⽰执⾏成功,没有问题。并且退出码是子进程返回给父进程的,由父进程所记录。
代码 1 或 0 以外的任何代码都被视为不成功。异常终止下退出码无意义
在shell命令行解释器中我们可以用 echo $? 来查看最近一次进程的退出码。
• 退出码 0 表⽰命令执⾏⽆误,这是完成命令的理想状态。
• 退出码 1 我们也可以将其解释为"不被允许的操作"。例如在没有 sudo 权限的情况下使⽤ yum;再例如除以 0 等操作也会返回错误码 1 ,对应的命令为 let a=1/0
• 130 ( SIGINT 或 ^C )和 143 ( SIGTERM )等终⽌信号是⾮常典型的,它们属于128+n 信号,其中 n 代表终⽌码。
• 可以使⽤strerror函数来获取退出码对应的描述。
我们可以调用errno来获取最近一次系统调用或库函数调用失败的原因。它是一个全局变量,在 C 和 C++ 编程中,当某些函数执行失败时,它们通常会设置 errno 以提供一个详细的错误代码。这些错误代码通常定义在 <errno.h> 头文件中。
2.2 _exit && exit
在一个程序的任何地方使用exit函数,都表示进程结束,并返回给父进程bash,子进程的退出码
_exit函数
c
#include <unistd.h>
void _exit(int status);
//参数:status 定义了进程的终⽌状态,及退出码,⽗进程通过wait来获取该值
- 说明:虽然status是int,但是仅有低8位可以被⽗进程所⽤。所以_exit(-1)时,在终端执⾏$?发现
返回值是255。
exit函数
c
#include <unistd.h>
void exit(int status);
exit最后也会调⽤_exit, 但在调⽤_exit之前,还做了其他⼯作:
- 执⾏⽤⼾通过atexit或on_exit定义的清理函数。
- 关闭所有打开的流,所有的缓存数据均被写⼊。
- 调⽤_exit。
return是⼀种更常⻅的退出进程⽅法。执⾏return n等同于执⾏exit(n),因为调⽤main的运⾏时函数会将main的返回值当做 exit的参数
三、进程等待
当一个子进程执行完的时候,如果不对其进行任何处理,那么它就会变成僵尸进程造成资源浪费等,并且我们也需要知道一个子进程运行结果的情况来获取一些信息。因此进程等待就是利用系统调用来处理内存泄漏和获取子进程退出信息的方式。
主要方法是通过系统调用wait和waitpid来实现。
3.1 wait
wait 函数是一个用于等待子进程结束的系统调用。通常与fork函数一起使用,以确保父进程能够正确地等待其子进程完成执行。
c
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *status);
- status: 这是一个指向整数的指针,用于存储子进程的终止状态。如果不需要这个状态信息,可以传递NULL。然而,获取子进程的退出状态通常是很有用的,因为它可以告诉父进程子进程是正常退出还是由于接收到信号而终止的
- wait函数会阻塞调用它的进程(类似使用scanf函数),直到它的一个子进程结束。
- 一旦有子进程结束,wait函数会收集该子进程的状态信息,并返回子进程的PID。
- 如果status参数不是NULL,wait函数还会通过status指针返回子进程的终止状态(正常退出是退出码,异常则是信号)
3.2 waitpid
c
#include <sys/types.h>
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int *status, int options);
参数:
pid:
指定要等待的子进程的 PID(进程标识符)。
特殊值:
-1:等待任何子进程。0:等待与调用进程在同一个进程组中的任何子进程。
>0:等待指定 PID 的子进程。< -1:等待指定进程组中的任何子进程(取绝对值)。
status:指向整数的指针,用于存储子进程的终止状态。可以通过宏来检查子进程的退出状态或信号终止信息。
如果不需要状态信息,可以传递 NULL。
宏:WIFEXITED(status): 若为正常终⽌⼦进程返回的状态,则为真。(查看进程是否是正常退出)
WEXITSTATUS(status): 若WIFEXITED⾮零,提取⼦进程退出码。(查看进程的退出码)(退出状态码>>8 & 0xff 若是异常退出则是 退出状态码 & 0x7f )
options:默认为0,表⽰阻塞等待
WNOHANG: 如果指定子进程没有结束,立即返回,而不是阻塞。可以实现非阻塞轮询,在父进程等待子进程结束的时候做其他事情
返回值:
成功时返回子进程的 PID。
失败时返回 -1,并设置 errno 以指示错误。
子进程的PCB中会记录自己的退出信息等,然后由系统调用wait和waitpid输出status等参数返回给父进程来进行操作
四、进程替换
4.1 替换原理
程序替换是通过特定的系统调用接⼝,加载磁盘上的⼀个全新的程序(代码和数据),加载到调⽤进程的地址空间中!
我们可以知道一个进程的PCB中有一个虚拟内存表并且还有页表用来和物理内存建立联系,当发生进程替换时,并不会发生PCB的替换而是将磁盘中的数据和代码段来替换虚拟内存表中的数据和代码段,但不会替换环境变量,如果是子进程进行进程替换,则会发生代码和数据的写时拷贝
4.2 替换函数
在库文件中有六种以exec开头的函数,统称exec函数:
c
#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[]);//系统调用上面库函数都依赖于此函数来实现
4.2.1 函数解释
所有的exec类函数的第一个参数都是为了帮我们找到这个替换的进程的地址,后面的参数是决定涵盖那些选项,并且需要以NULL结尾
以下是此类替换函数的标准传参样例:
exec后面分别跟有"l","p","e","v",分别表示"list","path","env","vector'。它代表了我们该以什么形式传参,"l"和"v"不会同时出现
• l(list) : 表⽰参数采⽤列表• v(vector) : 参数⽤数组
• p(path) : 有p⾃动搜索环境变量PATH
• e(env) :表⽰⾃⼰维护环境变量
五、自主Shell命令行解释器
以下是一个简单的命令行解释器的模拟实现,在unbutu下进行
- 交互获取字符串,打印提示符
- 分割字符串为接下来进程替换函数参数做准备
- 子进程创建并替换结束等待回收资源
c
7 #include <sys/wait.h>
8 #define LEFT "["
9 #define RIGHT "]"
10 #define LABLE "#"
11 #define LINE_SIZE 1024
12 #define ARGC_SIZE 32
13 #define DELIM " \t"
14 #define EXIT_CODE 44
15 const char* getusername()
16 {
17 return getenv("USER");
18 }
19 const char* getpwd()
20 {
21 return getenv("PWD");
22 }
23 void Interact(char *cline,int size)
24 {
25 printf(LEFT"%s@wu_wang %s"RIGHT""LABLE" ",getusername(),getpwd());
26
27 char* s=fgets(cline,size,stdin);
28 assert(s);
29 (void)s;
30
31 cline[strlen(cline)-1]='\0';
32
33 }
34 int splitstring(char* commandline,char* argv[])
35 {
36 int i=0;
37 argv[i++]=strtok(commandline,DELIM);
38 while(argv[i++]=strtok(NULL,DELIM));
39 return i-1;
40 }
41
42 int main()
43 {
44 extern char **environ;
45 char* argv[ARGC_SIZE];int i = 0;
46 char commandline[LINE_SIZE];
47 int quit=0;
48 while(!quit)
49 {
50 //交互
51 Interact(commandline,sizeof(commandline));
52 //commandlin -> 命令行参数已经获得
53 //分割命令行参数为进程替换做准备
54 int argc=splitstring(commandline,argv);
55 if(argc==0) continue;
56 //替换子进程
57 pid_t id=fork();
58 if(id<0)
59 {
60 perror("fork");
61 continue;
62 }
63 else if(id==0)
64 {
65 execvpe(argv[0],argv,environ);
66 exit(EXIT_CODE);
67 }
68 else
69 {
70 int status=0;
71 pid_t rid=waitpid(id,&status,0);
72 if(rid==id){};
73 }
74
75 // for(int i=0;argv[i];i++)
76 // {
77 // printf("[%d]:%s \n",i,argv[i]);
78 // }
79 }
80
81 return 0;
82 }
总结
本文主要对进程概念做进一步的了解和并初步使用