目录
冯 · 诺依曼体系结构
操作系统:Operator System(OS)
进程的基本概念
进程标识符
通过系统调用创建进程-fork初识
进程状态
僵尸进程
孤儿进程
进程优先级
环境变量
和环境变量相关的命令
环境变量的组织方式
main函数的三个参数
在说冯诺依曼体系结构之前,我们先来了解这么一个常识:
我们的电脑或者手机,
总的来说,其体系结构都是由 软件+硬件 构成。
而硬件部分,有 像我们所说的磁盘、键盘、网卡等等硬件设施,构成整体的硬件框架结构。
而软件部分,最核心、最重要的,就是我们的操作系统了。
软硬件结合,构成我们的计算机的体系结构。
就像下图所示:
那么,我们的计算机是怎么将软件和硬件结合在一起的呢?
这个时候,我们就需要来了解一个体系结构——冯诺依曼体系结构
我们基本所有的计算机系统,都遵循这样这么个结构。
这个结构其实也很简单,其可以用图示表示成这样:
首先,我们可以知道,
输入设备一般可以是包括 键盘,硬盘,网卡,鼠标,扫描仪, 写板等
输出设备一般可以是包括 显示器,打印机,硬盘,网卡等
解释一下,这里有的设备既可以作为输入设备,也可以作为输出设备。
比如刚刚所举的 网卡,硬盘 等。其实很好理解,待会我们下面会举例。
而这里的存储器,指的就是内存
运算器,可以简单理解为就是用于计算的那些东西(简单举例子,就是加减乘除等等)
控制器,可以理解为 作用是控制着 数据信号 传导
我们一般习惯把 输入设备和输出设备 统称为 外设。
我们从这个图中可以得到这样一些看似简单,但是很有用的信息:
说的直白一点,CPU是内部,而外设是外部,内部和外部要建立联系,那就必须要经过哨兵——内存。
再或者说,CPU是女方,外设是男方,而男方和女方想要牵手成功,就需要媒婆,而该内存就是扮演了媒婆这样一个角色。
理解到这个意思就行。
那有人会问,为什么要有内存呢?
难道就不能让CPU直接从外设读取信息,然后处理吗?
我们来回答一下这个问题:
我们结合这一张图来说明:
在金字塔顶端的说明其价格贵,在底端的说明其价格便宜。
在金字塔顶端的说明其运算速度快、效率高,在底端的说明其运算速度较慢、效率低。
我们讲得更加直白简单一点:
在CPU中,运算速度都是以纳秒为计算单位的;
在内存中,运算速度是以微秒为计算单位的;
在硬盘(SSD固态硬盘等)中,运算速度是以毫秒为单位的。
如果没有内存,那么CPU的运算速度是那么高,但是呢,硬盘的运算速度相较而言又是那么低,你们猜,最终的运算速度是要依照谁来定呢?
当然是硬盘。
这就类似于木桶原理。
可能CPU在运算的时候,0.001%的时间用来计算,99.999%的时间都是在用来等待了。
这样的话,不仅计算的效率变低,而且由于大部分的时间都在用来等待,会造成极大的浪费。
可是CPU又是那么的贵,所以CPU就总是只有那么一点点。
这样,便诞生了内存。在性价比的方面,做了一个折中。
这样,使得计算机的运算效率不仅不会那么低,并且还使得计算机不会那么贵,让普通人也能够用得起。
当然,在CPU中还有着寄存器、缓存等概念,它们的主要作用也同样是提高运算效率来的。
就好比你跟不上我,那我就先放在缓存(或者寄存器),然后我继续做我的事,不至于让我在原地静静地傻等。
好。我们解释完了该体系中的相关的常识性概念之后,我们再来用动态的眼光,看看这个体系结构:
我们先从硬件的角度来看:
想要数据信号从输入设备进入到输出设备中,
我们可以认为:
其先是经过了输入缓冲区(对标你的scanf在输入一行数据的时候,其就是先加载到了输入缓冲区中),当你敲下回车的那一瞬间,缓冲区刷新,数据就被加载到了内存(也就是存储器)中。(当然,刷新缓冲区的方式不仅仅有按回车这一种方法)
然后内存再将数据传给CPU,让CPU处理。
数据信号从CPU中出来,先进入内存当中,然后先是进入输出缓冲区中,也叫预写入
当程序终止、或者是遇到 ‘\n' 、fflush函数 等时,缓冲区刷新,就会将数据载入到输出设备当中。
简单比划一下就是这样:
如果从软件的角度来去看,这个工作是由谁来做的呢?
答案是:操作系统。(Operator System 简称OS)
也就是说,是操作系统完成数据的加载、输出等等工作。我们接下来,就会详细地介绍它的作用和功能。
这也从另一个角度来说就是:从软硬件的角度,内存的存在、缓存的特性都是可行的。
我们来通过举一个例子的方式,即解释从你登录上qq开始和某位朋友聊天开始,数据的流动过程。
我们忽略网络细节。
简单图示如上。请看下面的详细过程:
当你输入一条消息,没有发送的时候,可以认为其实在硬盘或者是输入缓冲区中,消息发送后,其 先会载入到内存中,(这其实也与你的QQ一直都是处于运行状态,即一直在内存运行中相匹配)紧接着,经过你的电脑的CPU处理,再经过内存,输出到你的网卡当中;
然后,通过网络(这里我们先忽略网络相关细节),传送到你的同学的电脑的网卡当中。数据通过你同学电脑的网卡,加载到你同学电脑的内存当中,经过CPU处理,再经过内存,输出到你的同学的电脑的显示器上。
在这里,对于你的电脑的主机而言,你的键盘相当于输入设备,网卡相当于输出设备;
你的同学的电脑而言,他的网卡相当于输入设备,而显示器则相当于输出设备。
好,对于冯诺依曼体系结构,我们暂时先说到这里。
我们下面继续来说操作系统。
首先,我们需要了解什么是操作系统:
任何计算机系统都包含一个基本的程序集合,称为操作系统(OS)。笼统来说,其包括内核(进程管理,内存管理,文件管理,驱动管理)和其他程序(例如函数库, shell程序等等)
说人话,就是操作系统是一套用来搞管理的软件。
管理什么?
管理软硬件
-> 硬件:包括冯诺依曼中的所有设备;
->软件:安装、卸载等,在系统层面,其包括文件、进程、驱动;
下面是一个比较好的管理分层的图的例子。
我们结合这一张图, 来举一个例子:
我们把操作系统看成是校长,底层的硬件看作是学生,那么驱动程序的存在就相当于是充当了导员的角色。
校长和学生,一般不见面,通过导员进行管理。但是,校长(操作系统)可以通过导员(驱动系统)拿到学生(底层硬件)的信息。
在这简单的层级关系里,校长(操作系统)就是来管理的,管理整个学校(整台计算机系统),用有决策权——简单理解可以认为是学生在哪一个班,正常上学或者勒令退学(对内存中的进程、文件等掌有生杀等等大权)
而当信息量和庞大的时候,校长(操作系统)就会将所有的学生的信息都描述起来(由于LInux就是用C写的,所以其用的就是结构体来完成的),然后可以通过链表等数据结构的方式,来将这些学生组织起来。
用6个字,就是:
先描述,再组织。
总结 一下,就是要:
描述被管理对象
组织被管理对象
1. 描述起来,用struct结构体
2. 组织起来,用链表或其他高效的数据结构
最后补充一点:为什么要存在OS,OS的存在有什么样的意义。
OS是为了与硬件交互,管理所有的软硬件资源、为用户程序(应用程序)提供一个良好的执行环境这样的需求而诞生的。你想一下,如果没有OS,你打个lol,2s退出一次,1s电脑重启一次...你还玩个锤子哈
再简单说一下系统调用和库函数的概念,这个我们后面还会继续说:
在开发角度,操作系统对外会表现为一个整体,但是会暴露自己的部分接口,供上层开发使用,这部分由操作系统提供的接口,叫做系统调用。说人话,就是有操作系统给我们提供的接口。(比如我们下面的fork函数,后面将要学习的exec系列的函数等)
系统调用在使用上,功能比较基础,对用户的要求相对也比较高,所以,有心的开发者可以对部分系统调用进行适度封装,从而形成库,有了库,就很有利于更上层用户或者开发者进行二次开发。说直白点,就是我们的prinf,scanf等等,都算是这样经过二次开发后提供给用户使用的。
课本概念:程序的一个执行实例,正在执行的程序等
内核观点:担当分配系统资源(CPU时间,内存)的实体
还是说人话,我们从两个方面去解释:
1、类比刚刚操作系统的管理,那么操作系统对于进程的管理,
依然遵循六字原则:先描述,再组织。
2、对于一个普通的文件而言,其有文件属性(即文件大小、所有者、创建日期等等)+文件内容。
那么类比于进程,其也是包含了两方面:进程属性+对应的文件。
结合我们刚刚说的,一个程序文件加载进了内存,就变成了进程。
那么这个进程是就会被操作系统管理。
如果同时存在了多个进程,那么这些进程就会通过”先描述,再组织“的方式被管理。
那操作系统是如何来描述这些进程的呢?
答案是:还是用一个结构体来完成。其有专门的名称——PCB(Process Control Block)
通过查看Linux源码,我们会发现,有一个叫做task_struct的结构体,专门用来完成该项工作。
PCB相较于task_struct的概念,就好比这样(如下图)
那么,我们现在对于 进程的认识可以这样来说明:
进程,就是可执行程序和需要管理进程的数据结构的集合
task_ struct内容分类
- 标示符: 描述本进程的唯一标示符,用来区别其他进程。
- 状态: 任务状态,退出代码,退出信号等。
- 优先级: 相对于其他进程的优先级。
- 程序计数器: 程序中即将被执行的下一条指令的地址。
- 内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针
- 上下文数据: 进程执行时处理器的寄存器中的数据。(我们之后会再说)
- I/ O状态信息: 包括显示的I/O请求,分配给进程的I/ O设备和被进程使用的文件列表。
- 记账信息: 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。
- 其他信息
接下来,我们就要详细地挑选task_struct里的重要内容来进行讲解。
不过在此之前,首先,我们来说一下,如何查看进程:
有两种方式都可以:
1、进程的信息可以通过 /proc 系统文件夹查看
如:要获取PID为1的进程信息,你需要查看 /proc/1 这个文件夹
2、大多数进程信息同样可以使用top和ps这些用户级工具来获取
举个例子:
我们写一个死循环好了,目的就是让该程序一直运行下去。
然后让其编译运行,就会在屏幕上不停地打印hello Linux!
我们复制一个会话
这样,等会程序运行的时候,我们就能够检测到了。
好,现在我们让程序开始运行。
关于ps的选项,参见下图:
然后后面的grep就是过滤一下要或者不要的信息。
读者可以自行尝试一下如果不过滤会出现什么样的效果。
关于程序进程的相关选项,我们下面会说。
对于一个进程,OS为了能够方便识别它们,在创建PCB的时候,每个人都会给其一个id。这就是进程标识符。
对于标识符,我们有pid和ppid之分。
pid指的是子进程,
ppid指的是父进程。
我们在程序中可以通过getppid() 和 getpid() 来实现。
我们还是通过一个小程序来举例:
我们不难发现,这里的ppid和pid都是一串数字,实际上就是编号。
其中ppid指的是父进程,pid指的是子进程。
我们等会可以结合下面fork函数的例子来看。
我们这里仅仅是了解一下,fork怎么用,达到会用的标准就行。
先来介绍:
我们输入
man fork
可以看到,fork没有参数,返回值为pid_t的类型,作用是创建一个子进程。头文件为
实际上,我们需要知道,fork是有两个返回值的。
为什么呢?
我们可以这样来理解一下:
同时,需要注意:子进程的返回值是0;而父进程的返回值是子进程的id。
我们来看这样一个程序(来看实操):
总共9行代码
我们来看运行结果。
我们会发现,第二个printf的内容执行了两次。因为子进程在fork里创建后,子进程和父进程都会执行第二个printf。
也就是说,子进程和父进程是独立运行的。
那我们再来举一个例子,来看:
那么这下我们让代码执行,其会产生什么结果呢?
由于父子进程我们无法准确让谁先跑,谁后跑,所以我们加上一个sleep来以示区分。
我们会发现一个现象:子进程的ppid就是父进程的pid。这样也就能说明一个问题——为什么有父子这样的叫法了。
那父进程的ppid又是谁的呢?
答案是-bash的,就是操作系统的。(bash相当于操作系统手下的一个助手)
而bash也是一个进程。如下图
那bash能不能挂?
当然不能!bash是不能挂的!
而由bash通过创建子进程的形式,和我们刚刚用fork创建的子进程的形式是基本一致的。
为了弄明白正在运行的进程是什么意思,我们需要知道进程的不同状态。一个进程可以有几个状态(在Linux内核里,进程有时候也叫做任务)
我们可以来看一看Linux内核源代码里是怎样定义的:
*
* The task state array is a strange "bitmap" of
* reasons to sleep. Thus "running" is zero, and
* you can test for combinations of others with
* simple bit tests.
*/
static const char * const task_state_array[] = {
"R (running)", /* 0 */
"S (sleeping)", /* 1 */
"D (disk sleep)", /* 2 */
"T (stopped)", /* 4 */
"t (tracing stop)", /* 8 */
"X (dead)", /* 16 */
"Z (zombie)", /* 32 */
};
R运行状态(running) : 并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队列里。(解释一下,R状态表明的就是运行状态,但其不一定表明的就是正在运行)
S睡眠状态(sleeping): 意味着进程在等待事件完成(这里的睡眠有时候也叫做可中断睡眠
(interruptible sleep))。(也就是等待状态)D磁盘休眠状态(Disk sleep)有时候也叫不可中断睡眠状态(uninterruptible sleep),在这个状态的进程通常会等待IO的结束。(处于D状态的操作系统是杀死不了的,而如果大量的程序由于在IO而进入D状态,很有可能会使整个服务器崩溃——过年抢红包,偶尔的王者荣耀的服务器崩溃就和这有关)
T停止状态(stopped): 可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运行。
X死亡状态(dead):这个状态只是一个返回状态,你不会在任务列表里看到这个状态。
我们可以通过这样的指令来查看进程状态:
ps aux
ps axj //上面的或者下面的
像我们刚刚看到的,比如:
通过这里的我们能够看到,我们这里的hello程序是处于一个休眠的状态。
这与IO等待有关系,因为IO是要和硬盘显示器发生交互,前面说过这个过程是很慢的, 就是说其0.1秒在运行,0.9秒都在等待,所以当你在查看其状态的时候,其处于S状态——即休眠状态。
我如果把printf的内容去掉,就是让它一直死循环,就直接这样:
我们再来看:
我们让其运行,
我们看到,其为R状态,就是可以理解为是running 状态。这里的+表示其实在前台运行的,就是说我按 ctrl c 可以将程序终止
如果我要在后面加上&,其就变成是在后台运行的了。
这个时候,我们再按ctrl c,然后再调用后台观察,我们可以看到,R后面的+小时了,并且无论我们怎么按,其都是无法停止。
这个时候,我们如果想要终止它,可以用kill命令
后面的27459就是当前进程的进程编号。
我们这个时候再看,就会发现进程没了。
这个图,可以参考一下,看中文就可以了。
我们再来说两种特殊的进程——僵尸进程和孤儿进程
——特殊的Z状态:
我们来举一个例子:
可以看到,按照我们的思路,子进程会在5秒之后退出,然后父进程是一个死循环。我们来看看这两个的进程在运行过程中的运行状态是怎样的。
我们编译后 ./hello让其执行
在另外的 一个窗口中,我们制作一个脚本,用于每1秒查看其进程状态
脚本如下:
while :; do ps aux | grep hello | grep -v grep;sleep 1; echo "#################";done
我们从上面的监视可以看出,子进程一开始和父进程一样,都是S状态,几秒钟后,其变成了Z状态。这里的Z就是僵尸状态。
那么为什么会有僵尸状态呢?
原因很简单。我们在创建一个进程的时候,是操作系统创建的。但是,进程在结束时,也需要有人来“收回”。
所以说,这里的子进程就是在等着父进程将其收回,读取它的退出状态。所以一直就是处于僵尸状态。
进程的退出状态必须被维持下去,因为他要告诉关心它的进程(父进程),你交给我的任务,我办的怎么样了。
可父进程如果一直不读取,那子进程就一直处于Z状态?是的!
维护退出状态本身就是要用数据维护,也属于进程基本信息,所以保存在task_struct(PCB)中,换句话说, Z状态一直不退出, PCB一直都要维护?是的!
那一个父进程创建了很多子进程,就是不回收,是不是就会造成内存资源的浪费?是的。
因为数据结构对象本身就要占用内存,想想C中定义一个结构体变量(对象),是要在内存的某个位置进行开辟空间。
所以僵尸状态会导致什么?
就是我们之前强调很多遍、耳熟能详的——会导致内存泄漏。
那如何避免呢?实际上可以用wait。这个我们后面再说到进程等待的时候再说。
孤儿进程,实际上和僵尸进程是两个方面,结合“孤儿”以及我们刚刚所说,不难猜出,僵尸进程是父进程一直在干活,子进程先挂了;那孤儿进程就是父进程先没了。
我们不再做过多的赘述,简单理解就是父进程没了,子进程还在。
有兴趣的读者可以自行尝试一下。
那么孤儿进程难道就没有父进程了吗?
答案并不是这样的。
这个时候,往往操作系统会来帮你。
将你的孤儿进程领养。
被谁领养?
一般都是一号进程。
孤儿进程被1号init进程领养,当然要有init进程回收喽。
我们说,进程的运行和办事情一样,也是有先后缓急之分的。
哪个进程先执行,哪个进程后执行,这是由其优先级所决定的(注意和权限区分一下,权限是能不能的问题,而优先级是已经能的基础上先后的问题)
我们如果输入ps -l,
会看到这样的信息:
这里的PRI是最终的PRI,是影响优先级的重要因素。一般而言,PRI越小,优先级越高,PRI越高,则相反。
解释一下其他几个有价值的值是什么意思:(其实我们部分在上面已经说过了)
UID : 代表执行者的身份
PID : 代表这个进程的代号
PPID :代表这个进程是由哪个进程发展衍生而来的,亦即父进程的代号
PRI :代表这个进程可被执行的优先级,其值越小越早被执行
NI :代表这个进程的nice值
而PRI是怎么算的呢?
我们可以这样认为:
PRI (最终) = PRI(开始)+ NI(这里的NI意为nice值)
而PRI(开始)的值基本上都是80。我们在上面看到的PRI是最终的PRI
这样,当nice值为负值的时候,那么该程序将会优先级值将变小,即其优先级会变高,则其越快被执行
所以,调整进程优先级,在Linux下,就是调整进程nice值
nice其取值范围是-20至19,一共40个级别。
需要强调一点的是,进程的nice值不是进程的优先级,他们不是一个概念,但是进程nice值会影响到进程的优先级变化。
可以理解nice值是进程优先级的修正修正数据
我们可以通过top命令,进入top后按“r”–>输入进程PID–>输入nice值 的方式,来将已有的nice值进行修改。
我们这里再补充一下其他的概念:
竞争性: 系统进程数目众多,而CPU资源只有少量,甚至1个,所以进程之间是具有竞争属性的。为了高效完成任务,更合理竞争相关资源,便具有了优先级
独立性: 多进程运行,需要独享各种资源,多进程运行期间互不干扰
并行: 多个进程在多个CPU下分别,同时进行运行,这称之为并行
并发: 多个进程在一个CPU下采用进程切换的方式,在一段时间之内,让多个进程都得以推进,称之为并发
再说一点就是,每一个进程在CPU上运行的时候,都会有一个时间片。当时间片到的时候,进程就会从CPU上被强行扒下来。
环境变量(environment variables)一般是指在操作系统中用来指定操作系统运行环境的一些参数
就比如说,在Linux系统中,我们知道,ls、touch等指令,实际上也是一个个可执行程序。但是当我们输入ls指令,touch指令啥的,为啥可以不带路径?
这就和环境变量有关了。
环境变量中有一个PATH项:
而我们当需要执行ls touch 等指令的时候,会优先去这些路径下寻找。
如图:
也就是说,这些指令早已经被存储了起来,即存储到了这些路径下的文件夹下。并且这些路径早已经被当成环境变量,它可以认为是整个操作系统调用指令的“全局变量”。、
我们输入env,可以看到我们所有的环境变量。
那这么说,如果我将我的可执行程序添加到环境变量里,是不是就可以不带路径直接执行了呢?
确实如此。
不过一般不建议这么做,因为会污染原有的环境变量。
那么我们最起码要说说如何创建、删除环境变量的吧
和环境变量相关的命令
1. echo: 显示某个环境变量值
2. export: 设置一个新的环境变量
3. env: 显示所有环境变量
4. unset: 清除环境变量
5. set: 显示本地定义的shell变量和环境变量
就直接在命令行中去试试就可以了。我在这里就不试了,感兴趣的读者可以自行去尝试一下。
就这么一张表,相信能看懂。意思其实很简单,其就是用指针数组的形式来存储的。
这是我们在命令行中所说的一些关于环境变量的内容。
那要是在程序中呢?
我们在这里,先说一个知识点,叫做main函数的参数。
可能很多读者会看到书上曾经写过main函数的参数,实际上,main函数有三个参数,只不过我们平时不写,系统已经帮我们默认了。
我们借此机会,将其讲解一下:
这三个参数是这样的:
int main(int argc, char* argv[], char* env[])
其中,前两个是命令行参数,最后的那个是环境变量参数。
我们来通过例子的方式讲解:
我们创建一个文件myfile.c,这是代码:
1 #include
2 int main(int argc,char* argv[],char* env[])
3 {
4 int i = 0;
5 for(;i
然后我们正常编译,来看:
如果我们这样去运行:
得到的就是这样。
如果我们这样去运行:
得到的结果是这样:
如果我们这样运行,得到的结果是这样:
以此类推,在此就不过多举例了,我们已经能够发现这其中的规律了:
就是在命令行中,我们怎样去执行的,那么我们就会看到怎样的结果。
也就是说,我们加了多少个-x选项,这里的argc就是其个数。
而argv[i]存储的正是每一个参数的-x选项的内容。
如果没有带选项,那么默认就是我们的文件名。
这个用法实际上让人很容易联想到strtok函数(这个我们后面再说)
那有什么运用场景呢?
可以想一想,我们的ls后面的那些选项,内部的逻辑是不是会存在着if(argv[i] == '-a')这样的逻辑呢?
好啦,前两个变量说完了,它们都是命令行参数,第一个是和程序执行时后面带着的 -x 的个数有关系(x可以为a,b,c...),而第二个参数正是存储每一个 -x 参数的(每一个-x都是一个字符串,它们的首元素地址存储到指针数组argv中)。
那后面的变量?
就是环境变量参数?
是的!
我们来看这样一段代码:
我们可以看到,其将我们的环境变量全部打印了出来。
这和我们刚刚在命令行中直接用env打印出来的是一样的。有兴趣的小伙伴可以一试。
同样,也可以用第三方变量environ获取、
就像这样:
这样后运行的结果和刚刚的是一模一样的。这里的environ可以理解为存储着指针数组的二级指针。
通过系统调用设置环境变量
我们同样还可以通过系统调用设置环境变量
通过这样,我们就可以获得环境变量PATH。
运行后和我们刚刚直接 echo $PATH 的结果是一样的
另外,我们再强调一下,环境变量是具有全局性的,也就是说,环境变量也是可以被子进程继承下去的。
好啦,本节内容到此就结束啦~~~记得关注呦