欢迎扫码关注微信公众号:柒零玖嵌入式,更多嵌入式软硬件相关分享!
1、相关概念理解
1.1 程序与进程(★★)
程序,是指编译好的二进制文件,在磁盘上,不占用系统资源(cpu、内存、打开的文件、设备、锁....)
进程,是一个抽象的概念,与操作系统原理联系紧密。进程是活跃的程序,占用系统资源。在内存中执行。(程序运行起来,产生一个进程)
- 程序 → 剧本(纸)
- 进程 → 戏(舞台、演员、灯光、道具...)
同一个剧本可以在多个舞台同时上演。同样,同一个程序也可以加载为不同的进程(彼此之间互不影响)。如:同时开两个终端。各自都有一个bash但彼此ID不同。
2、CPU执行的流程(★★★)
程序存储在网盘、硬盘等介质上,运行程序时被加载到内存,如DDR。CPU寄存器数量有限,因此设计了缓存机制,及cache。数据通过内存先放入catch,然后catch将数据给了寄存器后由CPU进行处理。代码经过预处理、编译、汇编和链接后生成二进制文件。CPU一次从缓冲区catch中取一条指令,由CPU内部的预取器取出一条指令,之后交给CPU内部译码器进行指令译码,确定指令功能,如加法运算。之后交给ALU(算数运算单元)进行运算,运算后将数据回写到寄存器中。
3、虚拟地址、物理地址、MMU(★★★★★)
3.1 虚拟地址
3.1.1 虚拟地址空间分配
Linux下执行一个程序a.out(ELF格式),将产生一个的进程,Linux将为其分配0-4G的虚拟地址空间(每个进程均对应一个独立的虚拟地址空间)。虚拟地址空间分为用户空间(0-3G)、内核空间(3-4G)。因为编译好的程序被分为多个段,如.text段、.data段、.bss段,因此虚拟地址空间也为其分配的响应存储位置,通过反汇编可以看出。如下图所示。
3.1.2 系统调用
如上分析我们知道,虚拟地址空间被分为用户空间和内核空间,那么用户空间如何实现与内核空间的通信?最常用的方式就是通过系统调用来实现。如下分析printf的执行流程。
应用层操作的是用户空间0-3G,write()函数完成了用户空间到内核空间过度。sys_write()可以完成3-4G内核空间的操作。从而调用设备驱动函数进行数据显示。
3.2 物理地址
3.3 MMU
3.4 为什么使用虚拟地址空间映射
- 方便编译器和操作系统安排程序的地址分布。
- 方便进程之间隔离
不同进程使用的虚拟地址彼此隔离。一个进程中的代码无法更改正在由另一进程使用的物理内存。
- 方便OS使用你那可怜的内存。
内存管理器会将物理内存页(通常大小为 4 KB)保存到磁盘文件。数据或代码页会根据需要在物理内存与磁盘之间移动。
4 PCB进程控制块(★★★★★)
4.1 文件描述符
当我们打开一个文件的时候会返回一个文件描述符:
文件描述符可以看成一个数组,用户可以打开1024-3个文件。前三个是系统文件描述符。打开的是标准输入、标准输出、标准错误。每打开一个文件,则占用一个文件描述符,且是当前最小的。文件描述符存在于PCB进程控制块中,其结构如下图所示。
那么进程控制块在哪?是一个什么东西,他的作用又是什么呢?
4.2 进程控制块
4.2.1 PCB的定义
每个进程在内核中都有一个进程控制块(PCB)来维护进程相关的信息,Linux内核的进程控制块是task_struct结构体(linux/include/linux/sched.h)里面存储了很多信息。如上图已经看出,PCB位于内核空间。
4.2.2 PCB主要内容
主要掌握PCB中包含的以下信息。
* 进程id。系统中每个进程有唯一的id,在C语言中用pid_t类型表示,其实就是一个非负整数。
* 进程的状态,有就绪、运行、挂起、停止等状态。
* 进程切换时需要保存和恢复的一些CPU寄存器。
* 描述虚拟地址空间的信息。
* 描述控制终端的信息。
* 当前工作目录(CurrentWorking Directory)。
* umask掩码。
* 文件描述符表,包含很多指向file结构体的指针。
* 和信号相关的信息。
* 用户id和组id。
* 会话(Session)和进程组。
* 进程可以使用的资源上限(ResourceLimit)。
5 fork函数与exec函数族(★★★★★)
5.1 fork函数
5.1.1 认识fork函数
函数作用:
创建一个子进程。
函数原型:
pid_t fork(void);
失败返回-1;成功返回:① 父进程返回子进程的ID(非负) ②子进程返回 0
pid_t类型表示进程ID,但为了表示-1,它是有符号整型。(0不是有效进程ID,init最小,为1)
注意返回值,不是fork函数能返回两个值,而是fork后,fork函数变为两个,父子需【各自】返回一个。
5.1.2 使用fork
5.1.2.1 fork父子进程不同的返回值
处理父子进程不同的返回值,做不同的事情:#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main(void)
{
pid_t pid;
pid = fork();
if (pid == -1 ) {
perror("fork");
exit(1);
} else if (pid > 0) {
sleep(2);
printf("I'm parent pid = %d, parentID = %d\n", getpid(), getppid());
} else if (pid == 0) {
printf("child pid = %d, parentID=%d\n", getpid(), getppid());
}
return 0;
}
5.1.2.2 循环创建N个进程
循环创建5个进程,注意子进程的处理。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(void)
{
int i;
pid_t pid;
printf("xxxxxxxxxxx\n");
for (i = 0; i < 5; i++) {
pid = fork();
if (pid == 0) {
break;
}
}
if (i < 5) {
sleep(i);
printf("I'am %d child , pid = %u\n", i+1, getpid());
} else {
sleep(i);
printf("I'm parent\n");
}
return 0;
}
5.1.2.3 fork父子进程资源共享问题
全局变量,父子进程是否共享?
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int a = 100; //.data
int main(void)
{
pid_t pid;
pid = fork();
if(pid == 0){ //son
a = 2000;
printf("child, a = %d\n", a);
} else {
sleep(1); //保证son先运行
printf("parent, a = %d\n", a);
}
return 0;
}
详细分析见父子进程差异一节。5.1.3 进程相关API函数
5.1.4 子父进程的差异
父子进程之间在fork后。有哪些相同,那些相异之处呢?
刚fork之后:
父子相同处: 全局变量、.data、.text、栈、堆、环境变量、用户ID、宿主目录、进程工作目录、信号处理方式...
父子不同处: 1.进程ID 2.fork返回值 3.父进程ID 4.进程运行时间 5.闹钟(定时器) 6.未决信号集
似乎,子进程复制了父进程0-3G用户空间内容,以及父进程的PCB,但pid不同。真的每fork一个子进程都要将父进程的0-3G地址空间完全拷贝一份,然后在映射至物理内存吗?当然不是!父子进程间遵循读时共享、写时复制的原则。这样设计,无论子进程执行父进程的逻辑还是执行自己的逻辑都能节省内存开销。
注意父子进程共享全局变量是不存在的!见如上5.1.2.3。
父子进程共享:
1. 文件描述符(打开文件的结构体)
2. mmap建立的映射区 (进程间通信详解)
特别的,fork之后父进程先执行还是子进程先执行不确定。取决于内核所使用的调度算法。
5.2 exec函数族
5.2.1 exec函数功能
fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。
将当前进程的.text、.data替换为所要加载的程序的.text、.data,然后让进程从新的.text第一条指令开始执行,但进程ID不变,换核不换壳。
5.2.2 exec函数族类型
其实有不少于六种以exec开头的函数,统称exec函数:
intexecl(const char *path, const char *arg, ...);
intexeclp(const char *file, const char *arg, ...);
intexecle(const char *path, const char *arg, ..., char *const envp[]);
intexecv(const char *path, char *const argv[]);
intexecvp(const char *file, char *const argv[]);
intexecve(const char *path, char *const argv[], char *const envp[]);
5.2.3 使用exec函数
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
int main(int argc, char *argv[])
{
printf("========================\n");
char *argvv[] = {"ls", "-l", "-F", "R", "-a", NULL};
pid_t pid = fork();
if (pid == 0) {
execl("/bin/ls", "ls", "-l", "-F", "-a", NULL);
execv("/bin/ls", argvv);
perror("execlp");
exit(1);
} else if (pid > 0) {
sleep(1);
printf("parent\n");
}
return 0;
}
5.3 子进程回收
在unix/linux中,正常情况下,子进程是通过父进程创建的,子进程在创建新的进程。子进程的结束和父进程的运行是一个异步过程,即父进程永远无法预测子进程到底什么时候结束。 当一个进程完成它的工作终止之后,它的父进程需要调用wait()或者waitpid()系统调用取得子进程的终止状态。
一个进程在终止时会关闭所有文件描述符,释放在用户空间分配的内存,但它的PCB还保留着,内核在其中保存了一些信息:如果是正常终止则保存着退出状态,如果是异常终止则保存着导致该进程终止的信号是哪个。这个进程的父进程可以调用wait或waitpid获取这些信息,然后彻底清除掉这个进程。我们知道一个进程的退出状态可以在Shell中用特殊变量$?查看,因为Shell是它的父进程,当它终止时Shell调用wait或waitpid得到它的退出状态同时彻底清除掉这个进程。
5.3.1 孤儿进程与僵尸进程
5.3.1.1 孤儿进程
孤儿进程:一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被init进程(进程号为1)所收养,并由init进程对它们完成状态收集工作。
孤儿进程是没有父进程的进程,孤儿进程这个重任就落到了init进程身上,init进程就好像是一个民政局,专门负责处理孤儿进程的善后工作。每当出现一个孤儿进程的时候,内核就把孤 儿进程的父进程设置为init,而init进程会循环地wait()它的已经退出的子进程。这样,当一个孤儿进程凄凉地结束了其生命周期的时候,init进程就会代表党和政府出面处理它的一切善后工作。因此孤儿进程并不会有什么危害。
产生孤儿进程:父进程结束,子进程依然在工作,此时子进程的父进程变为init进程(进程ID一般为1)。
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
int main(void)
{
pid_t pid;
pid = fork();
if (pid == 0) {
while (1) {
printf("I am child, my parent pid = %d\n", getppid());
sleep(1);
}
} else if (pid > 0) {
printf("I am parent, my pid is = %d\n", getpid());
sleep(9);
printf("------------parent going to die------------\n");
} else {
perror("fork");
return 1;
}
return 0;
}
5.3.1.2 僵尸进程
僵尸进程:一个进程使用fork创建子进程,如果子进程退出,而父进程并没有调用wait或waitpid获取子进程的状态信息,那么子进程残留资源(PCB)仍然保存在内核中。这种进程称之为僵死进程。
unix提供了一种机制可以保证只要父进程想知道子进程结束时的状态信息,就可以得到。这种机制就是: 在每个进程退出的时候,内核释放该进程所有的资源,包括打开的文件,占用的内存等。但是仍然为其保留一定的信息(包括进程号the process ID,退出状态the termination status of the process,运行时间the amount of CPU time taken by the process等)。直到父进程通过wait/waitpid来取时才释放。但这样就导致了问题,如果进程不调用wait/waitpid的话, 那么保留的那段信息就不会释放,其进程号就会一直被占用,但是系统所能使用的进程号是有限的,如果大量的产生僵死进程,将因为没有可用的进程号而导致系统不能产生新的进程. 此即为僵尸进程的危害,应当避免。
任何一个子进程(init除外)在exit()之后,并非马上就消失掉,而是留下一个称为僵尸进程(Zombie)的数据结构,等待父进程处理。这是每个 子进程在结束时都要经过的阶段。如果子进程在exit()之后,父进程没有来得及处理,这时用ps命令就能看到子进程的状态是“Z”。如果父进程能及时 处理,可能用ps命令就来不及看到子进程的僵尸状态,但这并不等于子进程不经过僵尸状态。如果父进程在子进程结束之前退出,则子进程将由init接管。init将会以父进程的身份对僵尸状态的子进程进行处理。
产生僵尸进程:子进程结束父进程没有回收子进程残余资源(PCB)
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
int main(void)
{
pid_t pid, wpid;
pid = fork();
if (pid == 0) {
printf("---child, my parent= %d, going to sleep 10s\n", getppid());
sleep(10);
printf("-------------child die--------------\n");
} else if (pid > 0) {
while (1) {
printf("I am parent, pid = %d, myson = %d\n", getpid(), pid);
sleep(1);
}
} else {
perror("fork");
return 1;
}
return 0;
}
5.3.2 子进程回收方法
5.3.2.1 wait()函数
父进程调用wait函数可以回收子进程终止信息。该函数有三个功能:
①阻塞等待子进程退出(wait一次调用只回收一个子进程)
②回收子进程残留资源
③获取子进程结束状态(退出原因)。
pid_t wait(int *status); 成功:清理掉的子进程ID;失败:-1 (没有子进程)
当进程终止时,操作系统的隐式回收机制会:1.关闭所有文件描述符 2. 释放用户空间分配的内存。内核的PCB仍存在。其中保存该进程的退出状态。(正常终止→退出值;异常终止→终止信号)
可使用wait函数传出参数status来保存进程的退出状态。借助宏函数来进一步判断进程终止的具体原因。宏函数可分为如下三组:
1. WIFEXITED(status) 为非0 → 进程正常结束
WEXITSTATUS(status) 如上宏为真,使用此宏 → 获取进程退出状态 (exit的参数)
2. WIFSIGNALED(status)为非0 →进程异常终止
WTERMSIG(status) 如上宏为真,使用此宏 → 取得使进程终止的那个信号的编号。
*3. WIFSTOPPED(status) 为非0 → 进程处于暂停状态
WSTOPSIG(status) 如上宏为真,使用此宏 → 取得使进程暂停的那个信号的编号。
WIFCONTINUED(status) 为真 → 进程暂停后已经继续运行
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
int main(void)
{
pid_t pid, wpid;
pid = fork();
int status;
if (pid == 0) {
printf("---child, my parent= %d, going to sleep 10s\n", getppid());
sleep(20);
printf("-------------child die--------------\n");
exit(77);
} else if (pid > 0) {
while (1) {
printf("I am parent, pid = %d, myson = %d\n", getpid(), pid);
wpid = wait(&status);
if (wpid == -1) {
perror("wait error");
exit(1);
}
if (WIFEXITED(status)) { //为真说明子进程正常结束
printf("child exit with %d\n", WEXITSTATUS(status));
} else if (WIFSIGNALED(status)) { //为真说明子进程被信号终止(异常)
printf("child is killed by %d\n", WTERMSIG(status));
}
sleep(1);
}
} else {
perror("fork");
return 1;
}
return 0;
}
如上函数,如果子进程正常退出,则退出77,如果通过kill -9杀死子进程,则提示killed by 9.
5.3.2.2 waitpid()函数
作用同wait,但可指定pid进程清理,可以不阻塞。
pid_t waitpid(pid_t pid, int *status, in options); 成功:返回清理掉的子进程ID;失败:-1(无子进程);参3为WNOHANG,返回0:且子进程正在运行。
特殊参数和返回情况:
参数pid:
> 0 回收指定ID的子进程
-1 回收任意子进程(相当于wait)
0 回收和当前调用waitpid一个组的所有子进程
< -1 回收指定进程组内的任意子进程
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <sys/wait.h>
int main(void)
{
pid_t pid, pid2, wpid;
int flg = 0;
pid = fork();
pid2 = fork();
if(pid == -1){
perror("fork error");
exit(1);
} else if(pid == 0){ //son
printf("I'm process child, pid = %d\n", getpid());
sleep(5);
exit(4);
} else { //parent
do {
wpid = waitpid(pid, NULL, WNOHANG);
//wpid = wait(NULL);
printf("---wpid = %d--------%d\n", wpid, flg++);
if(wpid == 0){
printf("NO child exited\n");
sleep(1);
}
} while (wpid == 0); //子进程不可回收
if(wpid == pid){ //回收了指定子进程
printf("I'm parent, I catched child process,"
"pid = %d\n", wpid);
} else {
printf("other...\n");
}
}
return 0;
}
参考:
https://www.cnblogs.com/Anker/p/3271773.html
未完待续。。。