【Linux】Linux进程概念
🥕个人主页:开敲🍉
🔥所属专栏:Linux🍊
🌼文章目录🌼
1. 冯诺依曼体系结构
2. 操作系统(Operator System)
2.1 操作系统的概念
2.2 设计OS的目的
3. 系统调用和库函数概念
4. 进程
4.1 进程的概念
4.2 描述进程 - PCB
4.3 组织进程
4.4 查看进程
4.5 通过系统调用获取进程提示符
4.6 根据系统调用创建进程 - 初识fork()
5. 进程状态
5.1 进程状态查看
5.2 Z(zombie) - 僵尸进程
5.2.1 僵尸进程的危害
5.3 孤儿进程
6. 进程优先级
6.1 优先级基本概念
6.2 查看系统进程
6.2.1 PRI 和 NI
6.3 查看进程优先级的命令
6.3.1 用 top 更改已经存在的进程的NI
6.4 其它概念
7. 环境变量
7.1 环境变量基本概念
7.2 查看环境变量的方法
7.3 和环境变量相关的命令
7.4 环境变量的组织方式
7.5 通过代码如何获取环境变量
7.6 通过系统调用获取环境变量
7.7 环境变量具有全局性
8. 程序地址空间
8.1 程序地址空间回顾
8.2 程序地址空间剖析
1. 冯诺依曼体系结构
我们常见的计算机,如笔记本;我们不常见的计算机,如服务器。大部分都遵守冯诺依曼体系:
截至目前,我们所认识的计算机,都是由一个个的硬件组成的:
输入单元:如键盘、鼠标、等
中央处理器(CPU):含有运算器和控制器等
输出单元:显示器、打印机等
关于冯诺依曼,必须强调几点:
① 图中的存储器指的是内存。
② 不考虑缓存情况,这里的CPU能且只能对内存进行读写,不能访问外设(输入、输出设备)。
③ 外设(输入或输出设备)要输入或者输出数据,也只能写入内存或者从内存中读取。
总结,所有设备都只能直接和内存打交道。
2. 操作系统(Operator System)
2.1 操作系统的概念
任何计算机系统都包含一个基本的程序集合,称为操作系统(OS)。笼统的理解,操作系统包括:
内核:进程管理、内存管理、文件管理、驱动管理
其他程序:函数库、shell程序等
2.2 设计OS的目的
① 与硬件交互,管理所有的软硬件资源。
② 为用户程序(应用程序)提供一个良好的执行环境。
定位
在整个计算机软硬件架构中,操作系统的定位是:一款纯正的 “搞管理” 的软件。
如何理解"管理"
① 管理的例子:在学校中就是层级分明的,例如:校长管理辅导员,辅导员管理学生。校长不会直接去管理一个一个的学生,而是交由辅导员管理,校长管理辅导员间接管理了所有的学生。
② 描述被管理对象:对于学生,有非常多的属性,例如:姓名、性别、班级、学号、身高....,辅导员要管理学生就是管理学生的所有属性,而为了方便将每个学生的属性都管理起来,使用 struct 进行管理,struct 中存储的是学生的属性,每个 struct 变量对应一个学生。这样,每个学生就被"描述"了,管理学生时,就是在管理学生的属性。
③ 组织被管理对象:每个学生被描述后,就需要将所有学生纳入一个系统进行统一管理,例如用链表将所有学生链接,对链表的增删查改就是对学生属性的管理。
总结
计算机管理硬件就是:
描述被管理对象
组织被管理对象
3. 系统调用和库函数概念
① 在开发角度,操作系统对外会表现为一个整体,但是会对外公开部分接口,供上层开发使用,这部分由操作系统提供的接口,叫做系统调用。
② 系统调用在使用上,功能比较基础,对用户的要求相对也比较高,所以,有心的开发者可以对部分系统调用进行适度封装,从而形成库,有了库,就很有利于更上层用户或者开发者进行二次开发。
4. 进程
4.1 进程的概念
课本概念:程序的一个执行实例,正在执行的程序等。
内核观点:担当分配系统资源(CPU时间,内存)的实体。
4.2 描述进程 - PCB
进程信息被放在一个叫做进程控制块的数据结构中,可以理解为进程属性的集合。
课本上称之为PCB(process control block),Linux操作系统下的PCB是:task_struct。
task_struct的一种
在Linux中描述进程的结构体叫做task_struct。
task_struct是Linux内核的一种数据结构,它会被装载到RAM(内存)里并且包含着进程的信息。
task_struct内容分类
① 标示符: 描述本进程的唯一标示符,用来区别其他进程。
② 状态: 任务状态,退出代码,退出信号等。
③ 优先级: 相对于其他进程的优先级。
④ 程序计数器: 程序中即将被执行的下一条指令的地址。
⑤ 内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针
⑥上下文数据: 进程执行时处理器的寄存器中的数据[休学例子,要加图CPU,寄存器]。
⑦ I/O状态信息: 包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。
⑧ 记账信息: 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。
⑨ 其它信息...
4.3 组织进程
所有的运行在系统里的进程都以 task_struct 链表的形式存在内核中。
4.4 查看进程
查看进程可以查看系统的 /proc 文件,使用命令:
ls /procu
还可以使用 ps -aux 来查看当前所有进程u:
ps -aux
还可以使用 top 指令查看:
top
4.5 通过系统调用获取进程提示符
当前进程 id(PID):系统调用getpid()
当前进程的父进程 id(PPID):系统调用 getppid()
4.6 根据系统调用创建进程 - 初识fork()
① 运行 man fork 来认识fork:man fork
② fork有两个返回值:子进程返回 0 给子进程,父进程返回子进程的 pid 给父进程
③ 父子进程代码共享,父子各自开辟一块空间拷贝这份代码(采用写时拷贝)
参考测试代码
1 #include <stdio.h> 2 #include <sys/types.h>3 #include <unistd.h>4 5 6 7 int main()8 {9 int ret = fork();10 if(ret<0)11 {12 perror("fork()");13 return 1;14 }15 else if(ret==0)16 printf("我是子进程,我的PID:%d\n",getpid());17 else18 printf("我是父进程,我的PID:%d\n",getpid());19 sleep(1);20 return 0;21 }
5. 进程状态
① R 运行状态(Running):运行状态并不意味着程序一定在运行,它表明进程要么是在运行中要么是在运行队列中。
② S 睡眠状态(Sleeping):意味着进程正在等待某个事件的完成,完成这个事件后进程才会重新运行或者回到运行队列。
③ D 磁盘休眠状态(Disk sleep):也可以称为 不可中断睡眠状态,在这个状态的进程通常会等待 IO 的结束。
④ T 停止状态(stopped):可以通过发送 SIGTOP 信号给进程来停止 (T) 进程。这个被暂停的进程可以通过发送 SIGCONT 信号来让进程继续运行。
⑤ X 死亡状态(dead):这个状态只是一个返回状态(子进程结束后返回给父进程一个信息),在任务列表里无法看到这个状态。
5.1 进程状态查看
ps -aux || ps -ajx
5.2 Z(zombie) - 僵尸进程
① 僵尸状态 (Zombies) 是一个比较特殊的状态,当子进程退出并且父进程没有读取到子进程的退出的返回代码时,子进程就会变成僵尸进程(Z)。
② 僵尸进程会以结束状态保持在进程表中,并且会一直在等待父进程读取退出状态代码。
因此,只要子进程退出了,父进程还在运行,并且父进程没有读取子进程的退出状态,子进程就会变成僵尸进程。
僵尸进程模拟代码:
1 #include <stdio.h> 2 #include <sys/types.h>3 #include <unistd.h>4 #include <stdlib.h>5 6 7 int main()8 {9 pid_t id = fork();10 if(id==0)11 {12 int sum = 5;13 while(sum--)14 {15 printf("我是子进程,pid:%d,ppid:%d\n",getpid(),getppid());16 sleep(1);17 }18 }19 else20 {21 while(1)22 {23 printf("我是父进程,pid:%d,ppid:%d\n",getpid(),getppid());24 sleep(1);25 }26 }27 return 0;28 }
5.2.1 僵尸进程的危害
① 进程的退出状态必须被维持下去,因为他要告诉关心它的进程(父进程),你交给我的任务,我办的怎么样了。可父进程如果一直不读取,那子进程就一直处于Z状态?是的!
② 维护退出状态本身就是要用数据维护,也属于进程基本信息,所以保存在task_struct(PCB)中,换句话说,Z状态一直不退出,PCB一直都要维护?是的!
③ 那一个父进程创建了多个子进程,都不回收,是不是就会造成资源的浪费?是的!因为数据结构对象本身就要占用内存,想想C中定义一个结构体变量(对象),是要在内存的某个位置进行开辟空间。当然,这还可能会导致资源泄露。
5.3 孤儿进程
① 父进程如果提前退出,子进程还未退出,那么这个子进程就称为孤儿进程。
② 孤儿进程并不是说这个进程就没有了父进程,当一个子进程称为孤儿进程后,将会由一个系统的进程来管理它。可以理解为:一个孩子失去了父亲,则它就会去到孤儿院接受照顾。
孤儿进程模拟代码:
1 #include <stdio.h>2 #include <sys/types.h>3 #include <unistd.h>4 5 6 int main()7 {8 gid_t id = fork();9 if(id==0)10 {11 while(1)12 {13 sleep(1);14 printf("我是子进程,pid:%d,ppid:5d",getpid(),getppid());15 }16 }17 else18 {19 int sum = 5;20 while(sum--)21 {22 printf("我是父进程,pid:%d",getpid());23 sleep(1);24 }25 }26 return 0;27 }
6. 进程优先级
6.1 优先级基本概念
① cpu资源分配的先后顺序,就是指进程的优先权(priority)。
② 优先权高的进程有优先执行权利。配置进程优先权对多任务环境的linux很有用,可以改善系统性能。
③ 还可以把进程运行到指定的CPU上,这样一来,把不重要的进程安排到某个CPU,可以大大改善系统整体性能。
6.2 查看系统进程
在 Linux 或者 Unix 系统中,用 ps -l 命令则会输出以下几个内容:
我们注意到其中几个很重要的信息,如下:
① UID:表示执行者的身份
② PID:这个进程的代号
③ PPID:这个进程的父进程的代号
④ PRI:代表这个进程被执行的优先级,数字越小优先级越高
⑤ NI:表示这个进程优先级的调整值
6.2.1 PRI 和 NI
PRI还是很好理解的,就是进程的优先级,数字越小优先级越高。那么 NI 呢?
NI 上面说过是进程优先级的调整值,这意味着,每个进程的优先级都是可以改变的。优先级的调整为:PRI(new) = PRI(old) + NI。因此,当 NI 为负数时,优先级的数字变小,则优先级变高;反之,优先级降低。
在 Linux 下,NI的取值范围是:-20~19,一共40个等级。需要强调的是,NI 的值并不代表进程的优先级,但是 NI 的值影响进程的优先级。
6.3 查看进程优先级的命令
6.3.1 用 top 更改已经存在的进程的NI
输入命令 top
进入 top 后,输入 r -> 输入要修改的进程的 PID -> 输入想要改为的 NI 值
6.4 其它概念
① 竞争性:系统进程数目众多,而 CPU 资源只有少量,甚至只有1个,因此进程之间都是相互竞争的关系。为了高效完成任务,更合理竞争相关资源,便有了优先级。
② 独立性:多进程运行,需要独享各种资源,多进程运行期间互不干扰。
③ 多个进程在多个CPU下同时运行,称之为:并行。
④ 并发:多个进程在一个CPU下采用进程切换的方式运行,在一段时间内同时运行多个进程,称之为:并发。
7. 环境变量
7.1 环境变量基本概念
① 环境变量(environment variables)一般是指在操作系统中用来指定操作系统运行环境的一些参数。
② 如:我们在编写C++代码的时候,在链接的时候,从来不知道我们所链接的动、静态库在哪里,但是照样可以链接成功,生成可执行程序,原因就是有相关环境变量帮助编译器进行查找。
③ 环境变量通常具有某些特殊用途,还有在系统当中通常具有全局特性。
7.2 查看环境变量的方法
env:查看所有环境变量
echo $NAME:(NAME:你要查看的环境变量的名称)
PATH环境变量
写一个简单的程序来打开解析PATH的口子:
1 #include <stdio.h> 2 3 4 int main()5 {6 printf("hello world!\n");7 return 0;8 }
我们写了一个打印 "hello world" 的简单程序,并生成了一个 code 的可执行程序。接下来我们使用 ./code 命令来运行它:
运行成功了,这什么奇怪的,但是你有没有想过这么一个问题:为什么运行自己的可执行程序要在前面加上 ./呢?为什么不能直接输入 code执行呢?并且我们在执行系统的命令时可以发现我们并不需要在前面加上 ./ 就能执行,这又是为什么呢?
需要解决上面的疑惑,就需要我们解析PATH环境变量了,先来看看PATH里面都有什么:
可以看到,PATH中貌似是一大串的路径,这是什么东西呢?这其实就是命令的查询路径。
当我们执行系统命令,如:ls、touch、mkdir、rm等等 时,系统就会默认去到 PATH 中的这一连串路径中查询是否存在这个命令。如果查询成功,则执行命令;否则,提示 command not found。
这时候再回到我们 code 的问题上来:为什么我们执行自己的可执行程序时不能直接 code 呢?因为系统默认是去到 PATH 中的一连串路径下查找,code在我们当前自己创建的路径下,系统找不到,无法执行;为什么我们执行自己的可执行程序要在前面加上 ./ 呢?因为加上 ./ 意味着我们告诉系统这个可执行程序就在我们当前所处的路径下,到这找,不要去 PATH 中找,这样系统就能够找到。
那如果我们就想要直接使用 code 命令就能直接执行怎么办呢?很简单,既然你是去到 PATH 中的一连串路径中查找,那么我就把我的 code 随便拷贝到其中一个路径中,让你能够找到,这样就能够直接 code 执行了:
另外还有一种方法:将当前路径加入到 PATH 路径中,这样系统在查询的时候依然可以查询到:
HOME环境变量
HOME环境变量用来查看当前用户家目录的路径:
7.3 和环境变量相关的命令
① echo:显示某个环境变量的内容
② export:设置一个新的环境变量
③ env:显示所有环境变量
④ unset:删除要删除的某个环境变量
⑤ set:显示本地定义的shell变量和环境变量
7.4 环境变量的组织方式
每个程序都会收到一张环境变量表,环境变量白哦是一个字符指针数组,每个指针指向一个以 "\0" 为结尾的字符串。
7.5 通过代码如何获取环境变量
main函数的三个参数:
argc:argv中元素的个数
argv:命令行中输入参数的个数,比如在命令行中输入 ./code -a,那么argc == 2,argv中就存着 ./code 和 -a。
env:存储环境变量
获取 env 中环境变量的方法还有:使用 environ
libc中定义的全局变量environ指向环境变量表,environ没有包含在任何头文件中,所以在使用时 要用extern声明。
7.6 通过系统调用获取环境变量
getenv(NAME):(NAME:环境变量名)获取某个环境变量,头文件<stdlib.h>
7.7 环境变量具有全局性
环境变量具有全局性,可以被子进程继承:
这里我们使用 export 命令写入一个自己定义的环境变量,随后我们写一个程序来看看 MYENV 是否能被子进程调用:
可以看到,子进程是可以拿到环境变量 "MYENV" 的,说明不论是系统环境变量还是自己定义的环境变量都可以被子进程继承。
8. 程序地址空间
8.1 程序地址空间回顾
之前我们在学习C语言时,一定都见过空间布局图:
但是在学习语言时我们可能并不对它非常理解。
下面我们来写段代码
1 #include <stdio.h> 2 #include <sys/types.h>3 #include <unistd.h>4 5 int g_val = 0;6 7 int main()8 {9 gid_t id = fork();10 if(id<0)11 {12 perror("error");13 return 1;14 }15 else if(id==0)16 {17 printf("我是子进程,我的pid:%d 我的ppid:%d g_val:%d &g_val:%p\n",getpid(),getppid(),g_val,&g_val);18 }19 else20 {21 printf("我是父进程,我的pid:%d g_val:%d &g_val:%p\n",getpid(),g_val,&g_val);22 }23 return 0;24 }
输出结果
解释:定义了一个全局变量g_val;创建了一个子进程继承父进程,子进程和父进程都打印了g_val的值和地址,发现它们都一样,这很正常,反而如果不一样才是不正常的。
接下来我们对代码稍加改动:
1 #include <stdio.h> 2 #include <sys/types.h>3 #include <unistd.h>4 5 int g_val = 0;6 7 int main()8 {9 gid_t id = fork();10 if(id<0)11 {12 perror("error");13 return 1;14 }15 else if(id==0)16 {17 g_val = 100;//子进程修改 g_val 的值18 printf("我是子进程,我的pid:%d 我的ppid:%d g_val:%d &g_val:%p\n",getpid(),getppid(),g_val,&g_val);19 }20 else21 {22 sleep(5);//这里我们让父进程sleep5秒,可以保证子进程一定先执行完,因此 g_val的值一定被修改了23 printf("我是父进程,我的pid:%d g_val:%d &g_val:%p\n",getpid(),g_val,&g_val);24 }25 return 0;26 }
输出结果
这时候我们就发现了一个诡异的事情:子进程比父进程先执行完,那么 g_val 的值一定是被修改为100了的,这时候子进程的 g_val 输出100没问题;但是父进程的 g_val 输出的却还是0?!并且子进程和父进程打印出来的 g_val 的地址一模一样,这意味着子进程和父进程都指向的是同一个空间的同一个值!为什么同一个空间的同一个值能够同时存在两种值呢?这不禁让我想起了著名的 "双缝干涉实验"。
但其实事情并没有我们想象的那么诡异,问题就出在这个地址上,虽然我们父、子进程打印出来的 g_val 的地址一样,但这里的地址并不是实际的物理地址,而是虚拟地址。
8.2 程序地址空间剖析
我们之前所说的 "程序的地址空间" 是不准确的,准确应该叫做 "进程地址空间",那该如何理解呢?看图:
由上我们也就知道了,每一个进程在创建时,不仅有 tast_struct(PCB),还有 mm_struct(虚拟空间),mm_struct就存在于 task_struct 中,并且还会创建页表,用于建立 虚拟地址 - 物理地址 的映射关系。
创作不易,点个赞呗,蟹蟹啦~