【Linux】进程

本文主要介绍了进程的相关理解:查看进程、进程状态、进程的优先级、环境变量、进程地址空间、Linux内核进程调度队列。

目录

冯诺依曼体系结构

操作系统

进程

查看进程

几点预备小知识

进程创建的代码方式

为什么要创建子进程

 样例代码:依次创建多个进程

进程状态

Linux的进程状态

僵尸进程和孤儿进程

进程的运行、阻塞和挂起

进程的优先级

概念

Linux的优先级的特点&&查看方式

其他概念

命令行参数

环境变量 

 常见环境变量

整体理解环境变量和系统

获取环境变量代码实现

 环境变量再理解

进程地址空间

从代码看现象

地址空间细节理解

如何理解地址空间

为什么要有地址空间

如何理解虚拟地址

Linux内核进程调度队列

优先级

活动队列

过期队列

active指针和expired指针


冯诺依曼体系结构

截止目前,我们所认识的计算机,都是由诸多硬件组成:

输入设备:键盘、显示器、摄像头、话筒、磁盘、网卡

输出设备:显示器、声卡、磁盘、网卡

CPU:运算器、控制器

存储器:内存

关于冯诺依曼体系结构,有这样的经典示意图:

图中的箭头可以看做数据的流向,数据是要在计算机的体系结构中进行流动的,在流动过程中,进行数据的加工处理,数据从一个设备到另一个设备,在本质上是一种拷贝!

数据设备间的拷贝效率,决定了计算机整机的效率。

关于冯诺依曼结构,需要强调几点:

1.CPU不和外设打交道,只和内存打交道。

2.外设输入输出的数据,不是直接给CPU,而是先要放到内存中

操作系统

操作系统就是一款软件,对软硬件资源进行管理。

对操作系统广义的认识:操作系统的内核+操作系统的外壳周边程序(给用户提供使用操作系统的方式)

对操作系统狭义的认识:只是操作系统的内核

为什么要有操作系统?

提供对软硬件资源进行管理(手段),为用户提供一个良好(稳定、安全、高效)的运行环境(目的)。

这里要引入一个非常重要的理念,任何管理(包括操作系统)都要遵循:

先描述,再组织!

这里的描述是指对管理对象进行描述,例如,在之前写通讯录时,先要定义一个包括各种联系人信息的结构体,然后使用数组这种数据结构进行组织。

在上图中,可以看到,操作系统为上面用户操作提供了系统调用接口,用户不能直接改变操作系统里的内容。

系统调用和库函数概念

在开发角度,操作系统对外会表现为一个整体,但是会暴露自己的部分接口,供上层开发使用,这部分由操作系统提供的接口,叫做系统调用。

系统调用在使用上,功能比较基础,对用户的要求相对也比较高,所以,有心的开发者可以对部分系统调用进行适度封装,从而形成库,有了库,就很有利于更上层用户或者开发者进行二次开发。

操作系统管理硬件

1.使用struct结构体对硬件描述

2.使用链表或其他数据结构组织起来

进程

所谓进程,是程序执行的一个实例,正在执行的程序。从内核观点看,进程是担当分配系统资源(CPU时间,内存)的实体。

进程信息被放在一个叫做进程数据块的数据结构中,是进程属性的集合。一般称之为PCB(process control block),Linux操作系统下的PCB是:task_struct。

当加载一个程序时,对应的代码和数据被加载到内存中,但是代码和数据不是进程,只是进程的一部分,进程还包括对应的PCB。

存在PCB的原因是:OS要对进程进行管理,先描述,再组织。这样对进程的管理,就变成了对链表的增删查改!

总结一下,进程=PCB+自己的代码和数据,对Linux操作系统来说,进程=内核task_struct结构体+程序的代码和数据。

理解一个概念:如何理解系统动态运行?

调度运行进程,本质就是让进程控制块task_struct进行排队!

查看进程

几点预备小知识

a)./xxxx,本质就是让系统创建进程并运行,我们自己写的代码形成的可执行文件=系统命令=可执行文件,在linux中的大部分执行操作,本质都是运行进程!


b)每一个进程都要有自己的唯一标识符,叫做进程pid

有myprocess.c这样一个文件,运行后生成myprocess的可执行文件。

#include <stdio.h>
#include <unistd.h>
int main()
{while(1){printf("I am a process!\n");sleep(1);}return 0;
}

 复制当前窗口,在一个窗口运行myprocess可执行程序(./myprocess),在另一个窗口输入下面的指令:

ps ajx | head -1 && ps ajx | grep myprocess

 这条指令由&&两边的两条指令组成,&&的意思是执行两条指令,其左边:

ps ajx | head -1

这句代码是打印出进程的标题行,

其右边:

ps ajx | grep myprocess

打印出包含‘myprocess’关键字的进程。

 第二列就是进程的pid,pid的类型是unsigned int pid。


c)可以使用getpid()得到进程的pid

如果不想通过ps ajx命令去查进程的pid,那么还可以使用getpid()函数去查, 

 如上图所示,每个进程都有自己对应的pid。pid属于内核数据结构中的数据,因此用户不用直接获得,需要调用系统接口。


d)Ctrl+C是在用户层面终止进度,kill -9 pid可以用来直接杀掉进程

进程创建的代码方式

首先看一个指令:

pid getppid(void); ---获取当前进程的父进程id

从上图发现,每次启动进程后,对应地 pid都不一样,这是正常的,但是对应的ppid(父进程)是一样的!上图的父进程都是6046,于是我们很好奇谁是6046啊?

我们可以通过如下的代码查到:

ps ajx | head -1 && ps ajx | grep 6046

原来6046是bash,bash就是父进程,是命令行解释器!


接下来,我们在代码中创建子进程:使用fork()命令

fork后的指令由父子进程共享! 

1.创建一个进程,本质就是系统中多了一个进程;多了一个进程,就是多了1和内核task_struct。

2.多了的一个进程,有自己的代码和数据,父进程的代码和数据是从磁盘加载来的,而子进程的代码和数据默认情况继承父进程的代码和数据(代码由于是只读的,所以父子共用一份代码,而数据可读可写,父子各自独立,原则上要分开!)。

为什么要创建子进程

想让子进程和父进程执行不一样的代码!

看下面这段代码: 

可以看到,当id不同时,进入不同的代码,这就是多进程情况。其中,fork是一个函数,由操作系统提供,fork会返回两次。为什么会fork返回两次呢?前面说到,fork后的代码由父子进程共享,其实这样说并不准确,在fork函数内部的return语句前,子进程已经被创建,因此,return语句也是由父子进程共享,会被父子进程各执行一次,会返回两次!

需要注意的是,进程间一定要做到:进程具有独立性杀掉父进程不会影响子进程,杀掉子进程不会影响父进程!

 样例代码:依次创建多个进程

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
void RunChild()
{while(1){printf("I am child,pid:%d,ppid:%d\n",getpid(),getppid());sleep(1);}
}
int main()
{const int num = 5;int i=0;for(i=0;i<num;i++){pid_t id = fork();if(id == 0){RunChild();}}while(1){sleep(1);                                                      printf("I am parent,pid:%d,ppid:%d\n",getpid(),getppid());}return 0;
}

如上,一次创建了5个进程!


除了使用ps ajx命令查看进程外,在/proc系统文件夹中也可以查看进程信息,例如,我当前正在运行pid为11129的进程,查看/proc文件夹

可以找到名称为11129的文件夹,打开11129文件夹,

其中,cwd(current work dir)保存了当前路径,这也就是我们在使用

fopen("log.txt","w");

这个命令时,为什么可以在当前路径下创建! 

exe:进程的pcb会记录自己对应的可执行程序的路径。

每个进程在启动的时候,会记录自己当前在哪个路径下启动,就是进程的当前路径,当用上面的fopen创建log.txt时,会把当前路径cwd和log.txt拼接(cwd/log.txt)

此外,我们还可以通过chdir改变进程的路径:

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{chdir("/home/ghs/Linux");FILE* fp = fopen("log.txt","w");fclose(fp);(void)fp;//ignor warningwhile(1){printf("I am a process,pid:%d\n",getpid());sleep(1);                                                              }return 0;
}

通过chdir("/home/ghs/Linux");这行代码将该进程的工作路径改到/home/ghs/Linux下,

通过查看进程的pdb,发现进程路径确实已经改变!

进程状态

Linux的进程状态

进程状态,就是内核数据结构struct task_struct内部一个   属性,一个进程可以有几个状态:

R:进程运行状态

S:休眠状态,可中断睡眠,进程在等待“资源(显示器,网卡等)”就绪

T/t:让进程暂停,等待被进一步唤醒,T-纯粹的暂停,t-遇到断点处进程暂停

D:深度睡眠状态,不可被杀,不可中断,在这种状态下,要么进程自己醒来,要么重启(断电),是Linux系统比较特有的一种进程状态。

X:死亡状态

Z:僵尸状态

这里补充一点关于kill的指令:

kill -l   --- 查看kill的所有命令
kill -9 pid  --- 杀掉进程
kill -19 pid  --- 暂停进程
kill -18 pid  --- 继续进程

实际上 ,我们之前都用过暂停进程-打断点。

僵尸进程和孤儿进程

僵尸进程(Z):已经运行完毕,但是需要维持自己的退出信息,在自己的进程task_struct会记录自己的退出信息,未来让父进程进行读取,如果没有父进程,僵尸进程会一直存在!

进程退出时,进程的代码和数据会被释放掉,但是进程的task_struct会一直被维持住,直到等待父进程来读取,如果一直不读取,那就会出现内存泄漏问题。

孤儿进程:父进程如果先退出,子进程就会变成孤儿进程,孤儿进程一般都会被1号进程(OS本身)进行领养的。孤儿进程要被领养的原因是:依旧要保证子进程正常被回收。

但是,在我们已经启动的所有进程中,怎么从来没有关心过僵尸进程呢?内存泄漏??  

原因:直接在命令行中启动的进程,它 的父进程是bash,bash会自动回收新进程的Z! 

进程的运行、阻塞和挂起

运行: 进程在运行队列中,该进程的状态就是R状态。

一个进程一旦持有CPU,会一直运行到这个进程结束吗??

不会! 当代内核是基于时间片进行轮转调度的,每个进程都有一定的时间片,即使进程为执行结束,只要时间片一到,会自动把这个进程从CPU上剥离下来,再从新列入到运行队列的尾部,让下一个进程再来进行CPU的调度,按照这中方式,让所有进程在一定时间内都得到调度。让多个进程以切换的方式进行调度,在一个时间段内同时得以推进代码,就叫做并发。但是,Linux不是这样调度的!只是OS教材调度算法的一种。

另外,任何时刻,都同时有多个进程在真的同时进行,叫做并行

阻塞态:等待,等待键盘资源是否就绪,键盘上有没有被用户按下的按键,按键数据交给进程。

不是CPU才有运行队列,各种设备也有自己的wait_quene,当CPU中运行队列的某个进程需要等待键盘资源时,这个进程需要被放到设备的wait_quene,该进程就绝对不会被CPU调度了,把这种进程叫做阻塞。当从键盘上输入后,进程就会从设备的wait_quene重新链入CPU的运行队列中,之后该进程再次被调度时,会继续进程该进程的后续代码。 

阻塞和运行的状态变化,往往伴随着pcb被链入不同的队列中!入队列的不是进程的代码和数据,而是进程的task_struct。  

挂起态:当OS内存特别吃紧时,OS会将一些S状态的进程对应的代码和数据放到磁盘上的swap分区(挂起到外设),这样就会为OS腾出一定的内存空间,以解决燃眉之急,当该进程需要被调度时,再把对应的代码和数据换到内存中,这样,操作系统在辗转腾挪之间更合理地使用内存资源(其实这种属于阻塞挂起)。 

 关于进程切换的话题

假设有两个进程,分别是task_struct1和task_struct2,当task_struct1的时间片到了,需要切换到task_struct2,CPU的寄存器会保存1进程的临时数据。CPU内部的所有寄存器中的临时数据,叫做进程的上下文。进程在切换时,最重要的一件事是:上下文数据的保护和恢复

CPU内的寄存器:寄存器本身是硬件,具有数据的存储能力,CPU的寄存器硬件只有一套!!

CPU内部的数据,可以有多套,有几个进程,就有几套和该进程对应的上下文数据。

寄存器!=寄存器内容

进程的优先级

概念

指定进程获取某种资源的先后顺序。

进程是用task_struct结构体来描述的进程控制块,这个结构体有很多内部字段,优先级就是就是结构体内的一个字段(int prio,一个整数或若干个整数),优先级的本质就是一个数字。Linux中,优先级数字越小,优先级越高。

下面我们区分一组概念:优先级 vs 权限

权限:能不能获取某种资源

优先级:已经能了,我们获取资源的顺序是什么

为什么要有优先级?

答:进程访问的资源(CPU)始终都是有限的,而系统中进程大部分情况都是有较多的。

操作系统关于调度和优先级的原则:分时操作系统,保证基本的公平。如果进程因为长时间不被调度,就造成了饥饿问题。

Linux的优先级的特点&&查看方式

在Linux系统中,用ps -al命令查看进程优先级,会输出一下几个内容:

其中的PRI就代表进程的优先级,NI表示进程优先级的修正数据,nice值。

新的优先级=优先级+nice,达到对于进程优先级动态修改的过程。

nice值并不能让你任意调整,而是有范围的!是[-20,19],一共40个数字。

每次调整优先级,都是从80开始。

其他概念

竞争性: 系统进程数目众多,而CPU资源只有少量,甚至1个,所以进程之间是具有竞争属性的。为了高效完成任务,更合理竞争相关资源,便具有了优先级

独立性: 多进程运行,需要独享各种资源,多进程运行期间互不干扰

并行: 多个进程在多个CPU下分别,同时进行运行,这称之为并行

并发: 多个进程在一个CPU下采用进程切换的方式,在一段时间之内,让多个进程都得以推进,称之为并发

命令行参数

我们来想一个问题,我们经常写的main函数需要带参吗?main函数既然是函数,那么谁来调度main函数呢?

实际上,main函数有参数:

int main(int argc,char* argv[])

main函数的参数可带可不带。但是,这次要讨论的问题是要带上这些参数(int argc,char* argv[])的意义及用法:

argv是一个字符指针数组,其中每一个元素都指向一个字符串,agrc是argv数组的元素个数

为了具体看一下argv指向的字符串,我们设计如下代码:

#include<stdio.h>
#include<unistd.h>int main(int argc,char* argv[])
{size_t i=0;for(i=0;i<argc;i++){printf("argv[%d]:%s\n",i,argv[i]);}return 0;
}

上面我们在命令行输入的叫做命令行字符串,argv是一个变长的数组,其长度和命令行字符串中的元素个数有关。在命令行中,虽然收到“./myprocess -a -b -c -d”这一行字符串,但是系统会帮助我们把这个字符串打散,把这个字符串中的空格设置成‘\0’,

 以上我们了解了命令行参数的概念,可是现在有两个问题我们比较关心:

1.为什么要这么干?

本质:命令行参数本质是交给我们程序的不同的选项,用来定制不同的程序功能,命令行会携带很多的选项。如下面的代码:

#include<stdio.h>
#include<unistd.h>int main(int argc,char* argv[])
{if(argc != 2){printf("Usage:%s -[a,b,c,d]\n",argv[0]);return 1;}if(strcmp(argv[1],"-a")==0){printf("this is function1\n");}else if(strcmp(argv[1],"-b")==0){printf("this is function2\n");}else if(strcmp(argv[1],"-c")==0){printf("this is function3\n");}else if(strcmp(argv[1],"-d")==0){printf("this is function4\n");}else{printf("no this function\n");}return 0;
}

2.这些工作时谁干的?

#include<stdio.h>
#include<unistd.h>
int g_val=10000;
int main()
{printf("I am father process,pid:%d,ppid:%d,g_val:%d\n",getpid(),getppid(),g_val);sleep(5);pid_t id = fork();if(id == 0){//childwhile(1){ printf("I am child process,pid:%d,ppid:%d,g_val:%d\n",getpid(),getppid(),g_val);sleep(1);}}else{//fatherwhile(1){ printf("I am father process,pid:%d,ppid:%d,g_val:%d\n",getpid(),getppid(),g_val);sleep(1);}}return 0;
}

从上面这段代码中,可以看到,子进程也可以看到父进程g_val的值,因此,可以得出结论:

父进程的数据,默认能被子进程看到并访问!

 在上面这段代码中,父进程的ppid是431,这个431其实是bash(之前也说过),因此,另一个结论:

命令行中启动的程序,都会变成进程,其实都是bash的子进程!

因此,我们在命令行中输入的字符串“./myprocess -a -b -c -d”其实默认是输入给父进程bash(命令行解释器)的!!因此,父进程bash(命令行解释器)要对后面输入的命令行字符串进行解释,即将命令行字符串作为参数传给main函数的argv参数,因此,是父进程bash做的这些工作

环境变量 

在执行ls -a等这些命令时(本质是一段程序,见下图),本质和运行我们自己的./myprocess一样,我们自己也可以带上-a选项,

但是凭什么ls -a运行时不需要带上路径,而我们自己运行的程序需要带上./这样的路径?

原因就在于,Linux中,存在一些全局的设置,表明,告诉命令行解释器,应该去哪些路径下去寻找可执行程序。

PATH:环境变量。如果想要打印环境变量的内容,需要用$PATH打印,环境变量有很多,PATH只是一个环境变量。系统中的很多配置,在我们登录Linux系统的时候,已经被加载到bash进程中(内存)

这个环境变量中,我们以:为分隔符,由:划分出来的一块区域是一个路径。

bash在执行命令的时候,需要先找到命令,因为未来要加载!那么去哪找呢?bash维护了一批路径(如PATH),这批路径就是在bash在执行命令时默认的搜索路径。比如:在执行ls命令时,需要在上面的一批路径下去找,第一个路径下没找到就要去第二个路径下去找,依次类推,如果全都没找到,那么就报错(command not found ),找到了就加载并运行这个程序,显示程序执行的结果。总结一下,执行ls不用带路径的原因是ls所在的usr/bin路径是在PATH环境变量当中的

那么今天我们有一个需求:我想执行我们的命令,和系统指令一样,怎么做呢?

我们可以简单粗暴地把我们命令拷贝到PATH环境变量中,

其实,这就相当于我们把myprocess安装到系统里,但是强烈不推荐这样做,污染了别人的指令集

所以,我们把刚才复制的myprocess删掉,相当于卸载!

我们还可以温柔一点,不要直接把myprocess安装到系统里:由于PATH是一个环境变量,那么我么可以直接对其赋值,如下:

但是,这样相当于把PATH的内容进行了覆盖,导致其他的大部分系统指令不能用了,因此,决不能这么干!!!(如果这么做了也没关系,重新登录系统即可,原因就是我们这种环境变量只是内存级的) 

那么,为了把我们命令的路径添加到环境变量中而不覆盖原有的路径,可以这样:

PATH=$PATH:/home/ghs/Linux/linux/lesson14

 可以看到,myprocess的路径已经被加到PATH环境变量里了。

$PATH:表示在原有路径后面添加,而不覆盖之前的。

这样,无论带不带./都可以执行了,myprocess可以在PATH环境中搜索路径!

可是,现在修改后的PATH是内存级的,如果让它永久有效怎么办呢?

我们需要知道,最开始的环境变量不是在内存中,而是在系统的对应的配置文件中。这个配置文件在那里呢?使用cd ~回到用户家目录,ls -la,会有.bash_profile.bashrc两个文件,在登录linux系统时,会将这两个文件里的内容加载到bash进程中,环境变量默认是在配置文件中的

打开.bash_profile文件,  

那么,我们将.bash_profile中的PATH修改,就可以在每次登录系统时加载我们修改过的PATH,如下图:

然后关闭Xshell,重新登录即可(修改的.bash_profile,需要使用  su - 用户名  登录):

 常见环境变量

PATH:指定命令的搜索路径

HOME:当前登录用户的家目录(即用户登陆到Linux系统中时,默认的目录)

PWD:当前工作路径

SHELL:查看命令行解释器shell是谁

HISTSIZE:可以查看用户之前输入的命令条数,上翻查看 

(通过echo $环境变量名称查看哦) 

我们可以通过env指令查看更多的环境变量:

如果我们想建立自己的环境变量,可以使用(export name=value):

export THIS_IS_MY_ENV=helloghs

可以这样取消创建的环境变量(unset name):

unset THIS_IS_MY_ENV

上面创建取消环境变量只会在内存级别上修改,并不会影响配置文件,因此重新登录即可恢复原状。

然后,我们还可以这样创建本地变量(name=value):

这样创建变量后,不能在env里查到,因此叫本地变量。

整体理解环境变量和系统

获取环境变量代码实现

在C语言中,存在一个环境变量environ,,这是一个二级指针:

#include<stdio.h>
#include<unistd.h>int main()
{int i=0;extern char** environ;for(i=0;environ[i];i++){printf("env[%d]->%s\n",i,environ[i]);}}

通过以上代码可以获取环境变量,

我们惊奇地发现,上面打印出来的内容不正是刚才shell内部的环境变量吗?我们当前执行的程序./myprocess其实是bash的子进程,因此,环境变量默认也是可以被子进程拿到的!也就是说,环境变量刚开始不在子进程中,其实,环境变量们,默认在bash内部

那么,我们如何理解上面那段打印环境变量的代码呢?

磁盘当中有系统级的配置文件,这些配置文件本身包含了环境变量,后来某一天登录了Linux系统,那么就要在内存里给系统分配一块内存,创建一个进程,这个进程就是bash(命令行解释器),然后将磁盘上的配置文件(包括环境变量)导入到了bash当中,然后再命令行中启动了我的程序(./myprocess),也就是在内存当中创建一个子进程,子进程可以拿到环境变量。

可是,环境变量可是很多的,bash内部是如何组织的?实际上,在系统启动时,会维护一张表,这张表是char* env[],这个数组中每个元素都是一个字符指针,指向一个字符串,这个字符串就是一个个环境变量,

这让我们联想到了main函数参数,bash进程启动的时候,默认会给子进程形成两张表,分别是argv[]命令行参数表和env[]环境变量表,bash通过各种方式交给子进程agrv[]从用户输入的命令行来,env[]从OS的配置文件来!

所以,为了让用户快速找到环境变量表,所以系统提供了environ这个二级指针,这个指针是全局的,可以被子进程看到,指向char* env[]这张表,这张表的最后一个元素一定是NULL(所以上面代码循环会结束),因为这个表内部元素是char*类型,因此environ这个变量是char**。

那么,除了使用上面那段代码中environ这个全局变量获取环境变量表之外,还有一种方式,main函数除了有int agrc,char* argv[]之外,还可以接受第三个参数char* env[],通过这个参数打印环境变量:

#include<stdio.h>
#include<unistd.h>int main(int argc,char* argv[],char* env[])
{int i=0;for(i=0;env[i];i++){printf("env[%d]->%si\n",i,env[i]);}return 0;
}

 环境变量再理解

环境变量具有系统级的全局属性,因为环境变量本身会被子进程继承下去,每个子进程都可以查到环境变量。 

还可以使用getenv函数获取某个函数变量:

#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
int main(int argc,char* argv[],char* env[])
{char* pth = getenv("PATH");if(pth == NULL) return 1;printf("path:%s\n",pth);return 0;
}

总结一下,共有3种方法获取环境变量:

1.extern char** environ;

2.通过main函数参数

3.getenv("PATH")

我们通过export导入一个环境变量:

export myval=11111

确实有这样一个变量,可是很奇怪的是,export不也是一个命令吗?不应该创建子进程吗?那创建的变量myval就不应该被父进程看到啊,可是上面看到确实导给了bash,事实上,export和echo命令都不是常规命令,是内建命令,80%的命令都是bash创建子进程执行的,但是,还有剩下20%的命令是直接由bash亲自执行的,

从这个例子中,可以看到,虽然把PATH改变了,但是部分命令仍可以用,这部分指令就是内建指令,由bash亲自执行。

我们再谈一个问题,在命令行当中,创建一个变量HELLO=666666,虽然可以用echo查到HELLO的存在,但是它不在环境变量表里,

#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
int main(int argc,char* argv[],char* env[])
{char* pth = getenv("HELLO");if(pth == NULL) return 1;printf("path:%s\n",pth);return 0;
}

 并且,运行这个程序,没有打印出任何内容,说明HELLO没有被这个子进程继承。像这种没有用export修饰过的,而直接创建的变量叫做本地变量。本地变量只可以echo、export这样的内建命令使用。

为了将其导入环境变量表,使用export HELLO将其导入,

同时,运行上面程序,发现HELLO这个变量已经可以被子进程获取。 

综上,可以得出结论:本地变量只在bash内部有效,无法被子进程继承下去,导成环境变量,此时才能获取

进程地址空间

从代码看现象

我们先来从一段代码入手:

#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>int main()
{int g_val = 100;printf("father is running,pid:%d,ppid:%d\n",getpid(),getppid());pid_t id = fork();if(id == 0){//childint cnt = 0;while(1){printf("I am child process,pid:%d,ppid:%d,g_val=%d,&g_val:%p\n",getpid(),getppid(),g_val,&g_val);sleep(1);cnt++;if(cnt == 5){g_val = 300;printf("I am child process,change %d -> %d\n",100,300);}}}else{//father while(1){printf("I am father process,pid:%d,ppid:%d,g_val=%d,&g_val:%p\n",getpid(),getppid(),g_val,&g_val);sleep(1);}}return 0;
}

运行这段代码:

我们发现,子进程的g_val改变了,父进程的g_val并没有改变,这个可以理解,因为父子进程具有独立性。但是,最让我们感到震惊的是,红框里的地址竟然一样!怎么可能出现地址一样,但是内容不同呢?我们可以初步得出一个结论,这个地址绝对不是物理地址!这种地址在系统层面上将其称之为虚拟地址

现在,我们尝试理解一下虚拟地址

我们启动每一个进程时,除了要将代码和数据加载到内存之外,系统会为每一个进程创建一个叫做地址空间的东西,如下图:

在地址空间上的并不是实际地址,而是虚拟地址,还有一个叫做页表的东西将虚拟地址和物理地址之间建立映射关系,然后,未来上层使用虚拟地址访问时,OS会拿着虚拟地址查页表转化成物理地址,然后才能访问到数据。当创建子进程时,子进程也会有另外的对应的PCB、地址空间和页表,如果有100个子进程,那么会有100个地址空间,那么如何对这100个地址空间进行管理呢?先描述,再组织!地址空间本质上就是内核中的一个结构体对象, 子进程会把父进程的很多内核数据结构全拷贝一份,但是内存中的数据只有一份,父子进程的地址空间和页表一样,所以指向同一块内存,这种现象叫做浅拷贝。后来,子进程进行写入,通过地址空间找到虚拟地址,然后查找页表,找到实际地址,但是这个实际地址父进程也在用,所以会在物理内存中重新开辟一块空间,将旧的数据拷贝到新空间中,将新开辟空间的物理地址放到子进程页表中,重新构建映射,转而对应的目标指向新的空间,这个工作做完之后,继续进行刚才要进行的写入操作,至此,子进程写入操作完成,这些操作由OS自主完成,称为写时拷贝。父子进程页表中的虚拟地址相同,只是对应的实际地址不同。这就是为什么上面程序运行后,g_val的值不同,而地址相同。

地址空间细节理解

我们知道,进程之间是具有独立性的,如果父子进程是不写入的,那么未来一个全局变量,默认是被父子共享的,代码也是共享(只读的),为什么要这么干呢?可以可以把数据在创建子进程的时候,全部给子进程拷贝一份?这样貌似才符合独立性。我们想一想,很多数据可能子进程不会修改,这部分数据还可能占据很大空间,上来不管子进程改不改动数据,上来直接先拷贝一份数据,你能保证拷贝的数据都是要修改的吗?如果一些数据根本不会被修改,那直接共享就行了啊,再拷一份那不就是浪费内存资源了吗?所以,OS采用写时拷贝这种策略,本质是一种按需申请,这样就不会过度浪费空间。

那么,写时拷贝这种策略会不会比较慢呀?那如果按照所有数据都拷贝一份,那是不是也拷贝了?那写时拷贝这种策略要拷贝的数据量要≤全部拷贝一次的数据量。因此,写时拷贝通过调整拷贝的时间顺序,可以达到有效节省空间的目的。 

如何理解地址空间

什么是划分区域

地址空间的本质是内核的一个struct结构体!内部很多的属性都是表示start,end的范围。

为什么要有地址空间

1)将无序变成有序,让进程以统一的视角看待物理内存以及自己运行的各个区域!

2)进程管理模块和内存管理模块解耦

3)拦截非法请求

如何理解虚拟地址

在最开始的时候,地址空间和页表里面的数据从哪里来呢?程序里面本身就有地址(虚拟地址)!所以,地址空间里的地址就是从程序里面读来的。

学习完上面的内容后,我们不难理解,为什么同一个id既可以等于0,又可以大于0。 

Linux内核进程调度队列

Linux系统中每一个CPU都有一个运行队列,

上图是Linux2.6内核中进程队列的数据结构。

优先级

普通优先级: 100~ 139(我们都是普通的优先级,想想nice值的取值范围,可与之对应!)
实时优先级: 0~ 99(不关心)

活动队列

时间片还没有结束的所有进程都按照优先级放在该队列
nr_active: 总共有多少个运行状态的进程

queue[140]: 一个元素就是一个进程队列,相同优先级的进程按照FIFO规则进行排队调度,所以,数组下标就是优先级!

从该结构中,选择一个最合适的进程,过程是怎么的呢?

1. 从0下表开始遍历queue[140]

2. 找到第一个非空队列,该队列必定为优先级最高的队列

3. 拿到选中队列的第一个进程,开始运行,调度完成!

4. 遍历queue[140]时间复杂度是常数!但还是太低效了!

bitmap[5]:一共140个优先级,一共140个进程队列,为了提高查找非空队列的效率,就可以用5*32个比特位表示队列是否为空,这样,便可以大大提高查找效率!

过期队列

过期队列和活动队列结构一模一样

过期队列上放置的进程,都是时间片耗尽的进程

当活动队列上的进程都被处理完毕之后,对过期队列的进程进行时间片重新计算

active指针和expired指针

active指针永远指向活动队列

expired指针永远指向过期队列

可是活动队列上的进程会越来越少,过期队列上的进程会越来越多,因为进程时间片到期时一直都存在的。

没关系,在合适的时候,只要能够交换active指针和expired指针的内容,就相当于有具有了一批新的活动进程!

总结一下,在系统当中查找一个最合适调度的进程的时间复杂度是一个常数,不随着进程增多而导致时间成本增加,我们称之为进程调度O(1)算法。
 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/282180.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【自然语言处理七-经典论文-attention is all you need】

然语言处理七-经典论文-attention is all you need 摘要原文译文小结 1&#xff1a;引言原文译文小结 2&#xff1a;背景原文译文小结 3&#xff1a;模型架构原文译文小结 3.1 编码器和解码器原文译文小结 3.2 注意力原文译文小结3.2.1 缩放点积注意力原文总结 3.2.2 多头注意力…

用例图画法

介绍 在软件工程中&#xff0c;用例图是一种用于描述系统功能和与之交互的参与者&#xff08;Actors&#xff09;之间关系的图形表示方法。 绘图步骤 确定参与者&#xff08;Actors&#xff09;&#xff1a;识别系统中的各个参与者&#xff0c;这些参与者可以是人、其他系统或外…

【JS】for in可能遇到的问题

问题一&#xff1a;for in 打印属性顺序与定义顺序不一致 先来做一道题&#xff0c;请说出打印结果 const obj {a2: aaa,2: aaa,1: aaaa,a1: aaa, }for(let key in obj){console.log(key) }结果&#xff1a; 1 2 a2 a1 属性的书写顺序不一定就是对象遍历时的顺序。这涉及到…

边缘自动隐藏窗体,透明度切换,同步父窗体标签切换winform

一、实现功能 默认的标签栏(superTabControl) 可以设置隐藏,即可实现全屏最大化。通过列表切换打开的标签页。用于定制B/S模式系统显示更个性,自定义样式,简介 安全 兼容性好。 二、主要代码 private void Time_Tick(object sender, EventArgs e) {获取主屏

《深入解析 C#》—— C# 3 部分

文章目录 第三章 C#3&#xff1a;LINQ及相关特性3.1 自动实现属性&#xff08;*&#xff09;3.2 隐式类型 var&#xff08;*&#xff09;3.3 对象和集合初始化3.3.1 对象初始化器3.3.2 集合初始化器 3.4 匿名类型3.4.1 基本语法和行为3.4.2 编译器生成类型3.4.3 匿名类型的局限…

#Linux(文件系统概念)

&#xff08;一&#xff09;发行版&#xff1a;Ubuntu16.04.7 &#xff08;二&#xff09;记录&#xff1a; &#xff08;1&#xff09;查看文件系统情况df&#xff0c;man df查看df命令的功能 &#xff08;2&#xff09;查看文件系统的类型 df-T &#xff08;3&#xff09;df …

四川易点慧电子商务抖音小店:安全可靠,购物新选择

在数字化浪潮席卷全球的今天&#xff0c;电子商务已成为人们生活中不可或缺的一部分。四川易点慧电子商务抖音小店作为新兴的电商平台&#xff0c;以其安全可靠、便捷高效的特点&#xff0c;逐渐赢得了广大消费者的青睐。今天&#xff0c;就让我们一起走进四川易点慧电子商务抖…

前端项目部署后,如何提示用户版本更新

目录 前言解决方案1、public目录下新建manifest.json2、写入当前时间戳到manifest.json3、检查版本更新4、woker线程5、入口文件引入 可能出现的问题好书推荐 前言 项目部署上线后&#xff0c;特别是网页项目&#xff0c;提示正在操作系统的用户去更新版本非常 important。一般…

【开发环境搭建篇】Redis客户端安装和配置

作者介绍&#xff1a;本人笔名姑苏老陈&#xff0c;从事JAVA开发工作十多年了&#xff0c;带过大学刚毕业的实习生&#xff0c;也带过技术团队。最近有个朋友的表弟&#xff0c;马上要大学毕业了&#xff0c;想从事JAVA开发工作&#xff0c;但不知道从何处入手。于是&#xff0…

UniTask 异步任务

文章目录 前言一、UniTask是什么&#xff1f;二、使用步骤三、常用的UniTask API和示例1.编写异步方法2.处理异常3.延迟执行4.等待多个UniTask或者一个UniTas完成5.异步加载资源示例6.手动控制UniTask的完成状态7.UniTask.Lazy延迟任务的创建8.后台线程切换Unity主线程9.不要返…

力扣爆刷第102天之hot100五连刷96-100

力扣爆刷第102天之hot100五连刷96-100 文章目录 力扣爆刷第102天之hot100五连刷96-100一、136. 只出现一次的数字二、169. 多数元素三、75. 颜色分类四、31. 下一个排列五、287. 寻找重复数 一、136. 只出现一次的数字 题目链接&#xff1a;https://leetcode.cn/problems/sing…

C语言字符函数与字符串函数:编织文字的舞会之梦(下)

欢迎来到白刘的领域 Miracle_86.-CSDN博客 系列专栏 C语言知识 先赞后看&#xff0c;已成习惯 创作不易&#xff0c;多多支持&#xff01; 目录 七、strncpy的使用以及模拟实现 八、strncat的使用以及模拟实现 九、strncmp的使用以及模拟实现 十、strstr的使用以及模拟…

设计模式之抽象工厂模式解析

抽象工厂模式 1&#xff09;问题 工厂方法模式中的每个工厂只生产一类产品&#xff0c;会导致系统中存在大量的工厂类&#xff0c;增加系统的开销。 2&#xff09;概述 a&#xff09;产品族 和 产品等级结构 产品等级结构&#xff1a;产品的继承结构&#xff1b; 产品族&…

华为ensp中ospf基础 原理及配置命令(详解)

CSDN 成就一亿技术人&#xff01; 作者主页&#xff1a;点击&#xff01; ENSP专栏&#xff1a;点击&#xff01; CSDN 成就一亿技术人&#xff01; ————前言———— OSPF 的全称是 Open Shortest Path First&#xff0c;意为“开放式最短路径优先”。是一种内部网关协…

银行OA系统|基于SpringBoot架构+ Mysql+Java+ B/S结构的银行OA系统设计与实现(可运行源码+数据库+设计文档)

推荐阅读100套最新项目 最新ssmjava项目文档视频演示可运行源码分享 最新jspjava项目文档视频演示可运行源码分享 最新Spring Boot项目文档视频演示可运行源码分享 2024年56套包含java&#xff0c;ssm&#xff0c;springboot的平台设计与实现项目系统开发资源&#xff08;可…

Occupancy 训练策略

损失函数 Dice Loss Dice Loss 是一种用于图像分割的损失函数&#xff0c;其灵感来自于Dice 系数&#xff0c;是一种衡量两个样本相似度的方法。Dice 系数定义为&#xff1a; Dice 系数 2 * TP / (2 * TP FP FN) 其中&#xff1a; TP&#xff1a;预测为正且真实值为正的像…

网易有道 3/22

一面还比较常规&#xff0c;二面真的是压力爆炸&#xff0c;还是感觉自己太菜了 一面计网部分直接就是垮了&#xff0c;二面的话面试官水平很高&#xff0c;根本就和我不是一个级别的&#xff0c;三言两语就知道了我的项目大致情况&#xff0c;然后不断拷打 项目问完了又问了…

OpenGL学习笔记【3】—— GLAD配置

一、为什么用GLAD 由于OpenGL驱动版本众多&#xff0c;它大多数函数的位置都无法在编译时确定下来&#xff0c;需要在运行时查询。所以任务就落在了开发者身上&#xff0c;开发者需要在运行时获取函数地址并将其保存在一个函数指针中供以后使用。取得地址的方法因平台而异&…

超声波清洗机是用来干什么的?好用眼镜超声波清洗机不能错过

在快节奏的现代生活中&#xff0c;每一项提高效率和清洁效果的技术都值得我们关注。超声波清洗机就是这样一种技术的完美体现&#xff0c;它通过高频声波在液体中产生微小的气泡&#xff0c;这些气泡在压力作用下迅速爆炸&#xff0c;产生的强大冲击力能够深入物品的微小缝隙&a…

阿里云权益中心2024年五大上云优惠权益解析,助力用户优惠上云

上云首选&#xff0c;普惠好价&#xff0c;2024年阿里云通过权益中心为个人和企业用户提供五大上云优惠权益&#xff0c;为开发者和企业提供多款新老同享、续费同价产品&#xff1b;超150款免费试用产品&#xff1b;初创企业最高可得最低3500元&#xff0c;最高100万上云抵扣金…