文章目录
- 1、Linux 内核源码
- 2、R (running)
- 3、S (sleeping)
- 4、D (disk sleep)
- 5、T (stopped)
- 6、T (tracing stop)
- 7、Z (zomble)
- 8、X (dead)
【写在前面】
从此篇开始,就开始学习 Linux 系统部分 —— 进程,在正式学习 Linux 进程之前,我们需要铺垫一些概念,如冯诺依曼体系结构、操作系统的概念及定位、进程概念,我们会先铺垫理论,再验证理论。其次对于某些需要深入的概念我们只是先了解下。本文中的 fork 只会介绍基本使用,以及解答 fork 为啥会有 2 个返回值、为啥给子进程返回 0,而父进程返回子进程的 pid;而对于用于接收 fork 返回值的 ret 是怎么做到 ret == 0 && ret > 0、写时拷贝、代码是怎么做到共享的、数据是怎么做到各自私有的等问题会在《Linux进程控制》中进行展开。
一、冯 • 诺依曼体系结构
💦 体系结构
冯 • 诺依曼结构也称普林斯顿结构,是一种将程序指令存储器和数据存储器合并在一起的存储器结构。数学家冯 • 诺依曼提出了计算机制造的三个基本原则,即采用二进制逻辑
、程序存储执行
以及计算机由五个部分组成 (运算器、控制器、存储器、输入设备、输出设备)
,这套理论被称为冯 • 诺依曼体系结构。我们常见的计算机,如笔记本。我们不常见的计算机,如服务器,大部分都遵守冯诺依曼体系。其中:
- 输入设备:键盘、鼠标 … …。
- 输出设备:显示器、音响 … …。
- 存储器:如没有特殊说明一般是内存。
- 运算器:集成于 CPU,用于实现数据加工处理等功能的部件。
- 控制器:集成于 CPU,用于控制着整个 CPU 的工作。
各个组件之间的互通是通过 “ 线 ” 连接实现的,这可不是那种电线杆上的线,因为计算机更精密,所以使用 “ 主板 ” 来把它们关联在一起。
💦 数据流向
冯 • 诺依曼体系结构规定了硬件层面上的数据流向,所有的输入单元的数据必须先写到存储器中 (这里只是针对数据,不包含信号(通过外设直接对 CPU 交互)),然后 CPU 通过某种方式访问存储器,将数据读取到 CPU 内部,运算器进行运算,控制器进行控制,然后将结果写回到内存,最后将结果传输到输出设备中。
我们在 C/C++ 中说过,可执行程序运行时,必须加载到内存,为啥 ❓
在此之前先了解一下计算机的存储分级,其中寄存器离 CPU 最近,因为它本来就集成在 CPU 里;L1、L2、L3 是对应的三级缓存;主存通常指的是内存;本地存储(硬盘)和网络存储通常指的是外设。
如图所示,这样设计其实是因为造价的原因,对于绝大多数的消费者,你不可能说直接把内存整个 1 个 T 吧,当然,氪金玩家除外。
其中通过这个图,我们想解释的是为啥计算机非得把数据从外设(磁盘) ➡ 三级缓存(内存) ➡ CPU,而非从外设(磁盘) ➡ CPU。
原因是因为离 CPU 更近的,存储容量更小
、速度更快
、成本更高
;离 CPU 更远的,则相反。假设 CPU 直接访问磁盘,那么它的效率可太低了。这里有一个不太严谨的运算速度的数据,CPU 是纳秒级别的;内存是微秒级别的;磁盘是毫秒级别的。当一个快的设备和一个慢的设备一起协同时,最终的运算效率肯定是以慢的设备为主,就如 “ 木桶原理 ” —— 要去衡量木桶能装多少水,并不是由最高的木片决定的,而是由最短的木片决定的。也就是说一般 CPU 去计算时,它的短板就在磁盘上
,所以整个计算机体系的效率就一定会被磁盘拖累。所以我们必须在运行时把数据加载到内存中,然后 CPU 再计算,而在计算的期间可以同时让输入单元加载到内存,这样可以让加载的时间和计算的时间重合,以提升效率。
同理因为效率原因 CPU 也是不能直接访问输出单元的,这里以网卡为例,我刚发条 qq 消息给朋友,发现网络很卡,四五秒才发出去,而在这个过程,你不可能让 CPU 等你四五秒吧,那成本可太高了,所以通常 CPU 也是把数据写到内存里,合适的时候再把数据刷新到输出单元中。
所以本质上可以把内存看作 CPU 和所有外设之间的缓存
,也可以理解成这是内存的价值。
💨小结:所有数据 ➡ 外设 ➡ 内存 ➡ CPU ➡ 内存 ➡ 刷新到外设,其中 CPU 不直接和外设交互,外设只和内存交互。注意一定要区分清楚某些概念是属于 “ 商业化的概念 ” 还是 “ 技术的概念 ”。
💦 实例
对冯诺依曼的理解,不能只停留在概念上,要深入到对软件数据流理解上,请解释,你在qq 上发送了一句 “ 在吗 ” 给朋友,数据的流动过程 ?如果是在 qq 上发送文件呢 (注意这里的计算机都遵循冯 • 诺依曼体系结构,且这里不谈网络,不考虑细节,只谈数据流向) ?
☣ 消息:
☣ 文件:
本质上发消息和发文件是没有区别的。学习这里实例的意义是让我们在硬件层面上理解了它的数据流,你的软件无论是 QQ、WeChat 等都离不开这样的数据流。
二、操作系统 (Operator System)
💦 概念
操作系统是进行软硬件资源管理的软件
,任何计算机系统都包含一个基本的程序集合,称为操作系统 (OS)。笼统的理解,操作系统包括:
- 内核 (进程管理,内存管理,文件管理,驱动管理)。
- 其他程序 (例如函数库,shell 程序等等)。
为什么要有操作系统 ❓
- 最明显的原因是如果没有操作系统,我们就
没有办法操作计算机
。换句话说,操作系统的出现可以减少用户使用计算机的成本
。你总不能自己拿上电信号对应的电线自己玩吧,那样成本太高了。 对下管理好所有的软硬件
,对上给用户提供一个稳定高效的运行环境
。其中硬件指的是 CPU、网卡、显卡等;软件指的是进程管理、文件、驱动、卸载等。不管是对下还是对上,都是为了方便用户使用。
💦 计算机体系及操作系统定位
其中用户可以操作 C/C++ 库、Shell、命令、图形界面;底层可以通过操作系统接口完成操作系统工作;操作系统目前主流的功能有四大类 —— 1、进程管理
。2、内存管理
。3、文件管理
。4、驱动管理
。后面我们重点学习进程管理和文件管理,其次内存管理学习地址空间和映射关系就行了。
其次操作系统是不信任任何用户的
,所以用户不可能通过某种方式去访问操作系统,甚至对系统硬件或者软件的访问。而对系统软硬件的访问都必须经过操作系统。也就是说作为用户想要去访问硬件,只能通过操作系统所提供的接口去完成,但是操作系统提供的接口使用成本高,所以我们就有了基于系统调用的库等。就比如银行不信任任何人,你要去取钱 (硬件),你不能直接去仓库拿钱,你也不能私底下指挥工作人员 (驱动) 给你去仓库拿钱,银行规定你要拿钱,必须通过银行提供的窗口 (操作系统提供的接口) 来取钱。
也就是说我们使用 print、scanf 等库函数时,都使用了系统接口,称之为系统调用。
系统调用和库函数概念 ❓
- 在开发角度,操作系统对外会
表现为一个整体
,它不相信任何用户,但是会暴露自己的部分接口
,供上层开发使用,这部分由操作系统提供的接口,叫做系统调用。 - 系统调用在使用上,功能比较基础,对用户的
要求相对也比较高
,所以,有心的开发者就对部分系统调用进行适度封装
,从而形成库,有了库,就很有利于更上层用户或者开发者进行二次开发。类似于银行取钱时,一般都会雇佣指导人员 (库),王大爷不会取钱,就叫指导人员来指导 (调用库)。其实对于库函数的使用要么使用了 SystemCall,如 printf 函数;要么没使用 SystemCall,如 sqrt 函数。
我们学习的 C/C++ 的范畴实际上在系统提供的接口之上,当然 Java 等语言还要在往上点。所以我们经常说的 “ 跨平台性 ” 的根本原因就是因为 C语言的库对用户提供的接口是一样的,但系统调用的接口可能不一样,Windows下就用 W 的,Linux 下就用 L 的
可以看到计算机体系是一个层状结构
,任何访问硬件或者系统软件的行为,都必须通过 OS 接口,贯穿OS
进行访问操作。
💦 管理
90% 的人操作系统学不会的根本原因是不理解 “ 管理 ”。
在学校里大概有这三种角色:
- 学生 (被管理者) —— 软硬件
- 辅导员 (执行者) —— 驱动
- 校长 (管理者) —— 操作系统
现实中我们做的事情无非是 a) 做决策
。 b) 做执行
。总之你是不是一个真正的管理者取决于你做决策的占比多还是少。在现实生活中一般都有一个现象,管理者和被管理者并不见面,校长不会因为你挂科就过来 跟你谈心。
管理者和被管理者并不直接打交道,那么如何进行管理 ❓
学生和校长并不见面,但还是把学生安排的明明白白的,比如拿奖学金与否、挂科与否。原因是你的个人信息在学校的系统里面,也就是说本质管理者是通过 “ 数据 ” 来进行管理的。比如说评选奖学金,校长在系统中筛选好某系某级综合成绩排名前 3 的学生来发奖学金,这时校长把 3 位同学对应的辅导员叫过来,并要求开一个表彰大会来奖励 3 位同学,然后辅导员就开始着手执行工作。
管理者和被管理者并不直接打交道,那么数据从哪来的 ❓
就相当于在入学时你的个人信息是由执行者把你的档案录入系统。
既然是管理数据,就一定要把学生信息抽取出来,而学生信息可以用一个结构体来描述,每一名同学创建一个结构体变量,然后利用指针把所有的同学关联起来,构成一个双向循环链表。此时校长要对旷课超出一定次数的张三进行开除学籍的处分,那么校长先通知辅导员,叫张三不要来了,然后从系统中把张三的个人信息给删除掉。本质学生管理工作,就是对链表的增删查改。
也就是说操作系统并不和硬件打交道,而是通过驱动程序进行操作。操作系统里会形成对所有硬件相关的数据结构,并连接起来,所以对硬件的管理最后变成了对数据结构的管理
。
管理的本质是:a) 对信息或数据进行管理 b) 对被管理对象先描述,然后通过某种数据结构组织起来,简化为先描述,后组织
。后面我们都会围绕着这些观点学习。
三、进程 (process)
💦 概念
- 课本概念:进程就是一个运行起来的程序。
- 内核观点:进程就是担当分配系统资源 (CPU 时间、内存) 的实体。
当然对于进程的理解不能这么肤浅,我们接着来了解一下 PCB。
💦 描述进程 - PCB
- 进程信息被放在一个叫做进程控制块的数据结构中,可以理解为进程属性的集合。
- 课本上称之为
PCB (process control block)
,Linux 操作系统下的PCB
是task_struct
。 - 在 Linux 中描述进程的结构体叫做
task_struct
。task_struct
是 Linux 内核的一种数据结构,它会被装载到RAM(内存)
里并且包含着进程的信息。
📝说明
- List item
test.c 文件在运行前是一个普通的磁盘文件,而要运行它,就必须先加载到内存中,此时 OS 中就增加了一个需要管理的进程。 - List item
OS 能否一次性运行多个程序 ❓
当然可以的。 - List item
正如校长和学生的例子,OS 如何管理运行起来的程序 ❓先描述,在组织 !!!
管理进程不仅仅是把磁盘加载到内存里 (这只是第一步),其次还会在 OS 中创建一个描述该进程的结构体,这个结构体在操作系统学科或 Linux kernel 中叫做 PCB(进程控制块),说人话就是在 Linux 下这个进程控制块是用 struct (因为 Linux kernel 是用 C 语言写的) 来描述的task_struct
。其中被加载到内存中的程序就是学生,PCB 就是描述学生的属性信息。将来这些 PCB 是一定能够帮我们找到对应代码和数据的,就如同学校系统中是一定包含你的个人信息的。
其次进程多了之后,操作系统为了更好的管理,需要使用 “ 双向循环链表 ” 将所有的 PCB 进行关联起来。
所以本质我们在 Linux 中 ./a.out 时主要做两个工作,其一先加载到内存,其二 OS 立马为该进程创建进程控制块来描述该进程。OS 要进行管理,只要把每一个进程的 PCB 管理好就行了,对我们来讲,要调整一个进程的优先级、设置一个进程的状态等都是对 PCB 进行操作。
💨小结:
描述:每个进程对应的 PCB 几乎包含了进程相关的所有属性信息。
组织:OS 对进程的管理转化成了对进程之间数据结构的管理。
所以站在程序员以更深入的角度来看待进程就是等于:你的程序 + 内核申请的数据结构(PCB)。
💦 task_ struct 内容分类
- 标示符 PID:描述本进程的唯一标示符,用来区别其他进程。
ps ajx
,查看系统当前所有进程。 - 状态:任务状态,退出代码,退出信号等。
稍后我们会见到 Linux 进程的具体状态,细节下面再说。 - 优先级:相对于其他进程的优先级。
比如去食堂干饭,需要排队,而排队就是在确定优先级,这口饭你是能吃上的,只不过因为排队导致你是先吃上,还是后吃上,所以优先级决定进程先得到资源还是后得到资源。在排队打饭时有人会插队,本质就是更改自己的优先级,你插队了,就一定导致其它人的优先级降低,对其它人就不公平,所以一般不让插队。其中 CPU、网卡等等同于食堂的饭,进程等同于要干饭的人。
为啥需要排队 ❓
也就是说为啥要有优先级呢 ?假设世界上有无限的资源
,那么就不会存在优先级
了。而这里因为窗口太少了,所以优先级是在有限资源(CPU、网卡等) 的前提下,确立谁先访问资源
,谁后访问
的问题。所以优先级存在的本质是资源有限。 到目前为止,除了进行文件访问、输入输出等操作,大部分所写的代码竞争的都是 CPU 资源,比如说遍历数组、二叉树等,最终都会变成进程,然后竞争 CPU 资源,而我们后面需要竞争网络资源。
优先级 and 权限有什么区别 ❓
优先级一定能
得到某种资源,只不过是时间长短问题;而权限是决定你能还是不能
得到某种资源。 - 程序计数器 epi:程序中即将被执行的下一条指令的地址。
CPU 运行的代码,都是进程的代码,CPU 如何知道,应该取进程中的哪条指令 ❓
我们都知道语言中一般有三种流程语句 a) 顺序语句。 b) 判断语句。c) 循环语句。一般程序中默认是从上至下执行代码的。
在 CPU 内有一个寄存器,我们通常称之为eip
,也称为pc
指针,它的工作是保存当前正在执行指令的下一条指令的地址。当进程没有结束,却不想运行时,我们可以将当前 eip 里的内容保存到 PCB 里(其实不太准确,这里只是先为了好理解,后面知识储备够了,再回头校准),目前是为了恢复,具体细节后面会谈。
你说 eip 是指向当前正在执行的下一条指令的地址,那么第一次 eip 在干啥 ❓
这里是属于硬件上下文的概念,下面在谈进程切换时再学习。 - 内存指针:包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针。
CPU 不能直接去访问代码,需要通过 PCB 去访问代码。内存指针可以理解为它是代码和进程相关数据结构的指针,通过这些内存指针可以帮助我们通过 PCB 找到该进程所对应的代码。 - 上下文数据:进程执行时处理器的寄存器中的数据。
其中寄存器信息可以通过 VS 集成开发环境下查看:
代码 ➡ 调试 ➡ 转到反汇编 ➡ 打开寄存器窗口。
我们常说的什么多核处理器,如四核八线程,注意它不是指 CPU 里的控制器,而是 CPU 里的运算器变多了,所以它计算的更快。后面我们会听过一个概念叫超线程,它其实是 CPU 开启了并发指令流
的一种技术,所以它就允许有多种执行流在 CPU 上同时跑。
进程快速切换 && 运行队列 ❓
比如你是一名大二的学生, 已经上了二十几节课了,但因为身体原因,需要休一年的学,于是你就走了,而当你一年后回来时,你发现你能挂的科都已经挂完了,甚至你已经被退学了,原因是学校的资源都给你分配着呢,但因为你的一走了之,且没有跟导员打招呼而休学。所以正确方式是在你休学前,你应该跟导员打招呼,待导员向上级申明并把你当前的学籍信息(你大几、挂了几科、累计学分、先把当前正在学习的课程停了) 保存后,才能离开,一年后,你回来了,但是你在上课时并没有你的位置,老师点名册上也没有你的名字,根本原因是你没有恢复学籍,你应该跟导员说恢复学籍,然后把你安排到对应的班级,此时你就接着上次保存学籍的学习状态继续学习。
也就是说当一个进程运行时,因为某些原因需要被暂时停止执行,让出 CPU,此时当前 CPU 里有很多当前进程的临时数据,所以需要在 PCB 里先保存当前进程的上下文数据,而保存的目的是为了下一次运行前先恢复。所以对于多个进程,一个运算器的情况下,为了实现伪并行,进程对应的时间片到了,就把进程从从 CPU 上剥离下来,在这之前会把上下文数据保存至 PCB,然后再换下一个进程,在这之前如果这个进程内有曾经保存的临时数据,那么它会先恢复数据,CPU 再运行上次运行的结果,这个过程就叫做上下文保存恢复
以及进程快速切换
。
系统里当前有 4 个进程是处于运行状态的,此时会形成运行队列 (runqueue)
,它也是一种数据结构,你可以理解为通过运行队列也能将所有在运行的 PCB 连接起来,凡是在运行队列中的进程的状态都是R
,也就是说每一个 PCB 结构在操作系统中有可能是链表,也有可能是队列,这个 PCB 里面会通过某种方式包含了大量的指针结构。注意以上所有的结构都是在内核中由操作系统自动完成的,这其中细节很多,后面每个阶段我们都会对细节进行完善,其次还包括阻塞队列
、等待队列
会再详谈。 - I/O状态信息:包括显示的 I/O 请求,分配给进程的 I/O 设备和被进程使用的文件列表。
白话就是哪些 I/O 设备是允许进程访问的。 - 记账信息:可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。
白话就是你的一个进程,在调度时所使用的时间、在切换时切换的次数等。
记帐信息的意义 ❓
现实中也存在 “ 记帐信息 ”,也有一定的意义,比如每个人的年龄,每过一年,第人都会增长一岁,那么不同人累计下来的 “ 记帐信息 ” 值不同时,会有不同的状态,如六个月,你不会走路;六年,学习;二十四年,工作;八十年,有人主动让座。所以对系统来讲可以通过 “ 记帐信息 ” 来指导系统,比如有 2 个优先级相同的进程,一个累计调度了 10 秒钟,另一个累计调度了 5 秒钟,下一次则优先调度另一个进程,因为调度器应该公平或较为公平的让所有进程享受 CPU 资源。
调度 ???
调度就是在从多的进程中,选择一个去执行,好比高铁站,你能准时准点的坐上高铁,根本原因是高铁站内部有自己的调度规则。 - 其他信息。
💦 查看进程
通过系统调用获取进程标示符 ❓
- 进程 id:
PID
- 父进程 id:
PPID
我们可以使用 man 2 getpid/getppid
命令来查看人生中第一个系统调用接口:
代码一跑起来就查看当前进程的 pid and ppid
:
当然我们也可以查看当前命令行进程的父进程:
父进程和子进程之间的关系就如同村长家的儿子指明道姓要找王婆找如花媳妇,可是如花已经跟李四跑了,王婆一看生意没法做,风险太大,此时王婆就面临着两难,其一,张三是村长的儿子;其二,如花已经跟李四跑了。所以王婆就在婚介所招聘有能力说这桩媒的媒婆实习生,王婆不自己去,而让实习生去。如果事说成了,王婆脸上也有光,如果事没说成,那么对王婆也没影响。
同样的 bash 在执行命令时,往往不是由 bash 在进行解释和执行,而是由 bash 创建子进程,让子进程执行。所以一般情况我们执行的每一个命令行进程都是命令行解释器的子进程。其细节,后面再谈。
其它方式查看进程 ❓
- 可以使用
top
命令来查看进程,类似于 Windows 下的任务管理器,一般用的少。 - 可以使用
ls /proc
命令来查看,proc 在 Linux 的根目录下。
如果要查看对应进程的信息,可以使用ls/proc/pid -al
命令:
接着我们再看下1号进程
:
当然因为权限问题有部分进程不让我们看,我今天还非看不可,直接换 root 用户(我这里就拎两个看得懂的进程出来):