目录
前言
1.冯诺依曼体系结构
2.管理和操作系统
3.初识进程
1.描述进程PCB(process control block)
1.标识符pid
fork接口创建子进程
2.进程状态
3.进程优先级
2.组织进程
理解进程执行原理:
4.环境变量
1.查看环境变量
2.测试PATH
3.和环境变量相关命令与组织
1.相关命令
2.组织方式
大家好呀!这篇文章就和我一起来探索一下计算机世界下的具体机制和原理,然后就可以过渡到我们的主要目标 ——Linux下的进程啦~❥(ゝω・✿ฺ)
首先,就是我们计算机人耳熟能详的冯诺依曼体系结构啦,可谓是给大部分的计算机设备的设计铺上了一层坚实的基础。
名词解释:
输入设备: 键盘,摄像头,话筒,磁盘(读取),网卡.....
输出设备:显示器,音响,磁盘(写入),网卡......
存储器:内存
中央处理器(cpu):
运算器:算术运算,逻辑运算
控制器:cpu响应外部事件的,协调外部就绪事件(比如拷贝数据到内存)
根据上面解释的例子来看,不难发现输入输出设备的本质:
输入设备本质:产生数据的的
输出设备本质:保存和显示数据的
在这套冯诺依曼体系的基础上,我们可以细分一下不同硬件的读取输出速度:
cpu&&寄存器> 内存 > 磁盘/SSD(固态硬盘) >光盘 >磁带
由于成本和木桶原理,冯诺依曼体系设计了内存作为cpu和磁盘的中间过渡者,有着一套完整和考虑周全的设计。
木桶原理:整体效率是由最慢的那个效率挂钩的
所以,我们通过上面,就可以发现冯诺依曼体系结构的本质:
1.cpu读取数据(数据 + 代码),都是要从内存中读取。站在数据的角度,我们认为cpu不和外设直接交互
2.cpu要处理数据,需要先将外设中的数据,加载到内存。站在数据的角度,外设直接和内存打交道
输入设备 -> 存储器 input I
存储器 -> 输出设备 output O
所以,我们平时所写的程序运行的时候就必须要加载进内存里,这是由冯诺依曼体系结构决定的。比如我们使用qq这个软件聊天的时候,底部逻辑是如何实现的呢?利用冯诺依曼进行说明:
qq是一个应用程序(一个进程),需要加载进内存才能和cpu进行交互。当我们向别人聊天的时候,写入的数据通过键盘(输入设备)外设流入内存,在进过程序控制(cpu执行)指定内存输出数据到网卡,网卡通过网络传输到对方计算机网卡内,在输入到它的内存内,由程序控制输入到对方的显示屏上。
那么有了这个冯诺依曼体系的计算机就纯靠用户进行管理执行吗?那这些成本也都太高了。
那么,在了解了计算机的整体规划之后,那么cpu这一部分是如何连接起下层硬件和上层用户以及软件继续协作和管理的呢?那么这就要和操作系统(OS)相关了。
操作系统(Operator System)是任何计算机的基本的程序集合。
(软件 -- 程序)有它的存在才能更好的使用计算机,管理计算机。
既然OS主要是打管理,那么,怎样才能算是管理呢?
首先,我们了解一下管理这一层面的知识。
假如有一个学校,里面有校长,老师和学生。校长是如何管理学生的呢?首先,学生入校要进行等级,学校需要录入信息,然后平时学生的各种再校信息也需要录入,这样,就表明此学生的信息已经被该校管理起来了,校长在针对这些数据进行管理,需要传递消息就可以通过老师进行执行,老师也可以负责收集和采集信息。由此,可以了解到相关管理的特点:
-- 先描述,在组织。
管理者和被管理者可以不直接交流
只需要拿到被管理者的数据,才支持管理决策 -- 即对被管理者的数据进行管理
但是管理与被管理者之前需要一个提交数据,和一个执行命令的中间层次 -- 执行者
那么操作系统就好比这管理者,也就是校长,那么执行者也就是老师就相当于驱动,被管理者学生也就好比是各种的硬件。
*管理,是对被管理对象的数据的经理。
决策进行:先对被管理对象进行描述,组成成数组 -- > 管理工作就变为对数组的增删改查;
*管理:先描述,在组织Linux下的描述是:c语言缩写的struct。
那么整个操作系统的管理也自然明确:
用户:指令操作 开发操作 管理操作 c库 c++库
用户操作接口:shell外壳 lib 部分指令 (为了能够方便进行调用和会使用接口,所以就有了这些。对系统调用接口的封装 -- 第三方库)
system call:系统调用接口 (操作系统对外(用户)提供系统接口 Linux用c语言写的 本质就是c语言提供的函数)
操作系统:内存 进程 文件 驱动 -- 操作系统内核
驱动程序:
底层硬件
系统调用接口就好比银行里面的柜台,对里面进行保护和方便用户进行使用。如果直接放开用户自由存储和取出会出现不会以及不安全的情况。系统调用接口也正是如此设计。
所以,现在我们就可以明确进程实际就是操作系统内核需要执行的事情,需要cpu和内存以及其他外设相互交流的流程。
首先,自己启动一个软件就是启动了一个进程。
基本概念:程序的一个执行实例,正在执行的程序等
内核观点:担当分配系统资源(CPU时间,内存)的实体
首先,进程要被加载到内存之中,那么首先需要描述该进程的信息。
就要将其信息保存在进程控制块的数据结构中,理解为进程属性的集合,其就是process control block。
Linux下的PCB为task_struct。由于Linux内核是c语言写的,所以控制其就是一个结构体,基本格式为:
struct task_struct
{
//属性信息
}
task_struct是Linux内核的一种数据结构,用来装载到RAM(内存)里并且保存进程的信息。
那么属性信息有哪些呢?
1.利用命令行查看:
就是区分不同进程的依据条件。
首先可以通过路径 ls /proc/[pid] 进程查看。这里的pid就是一串数字,比如查看pid为1的进程:
查看该路径下的全部的:
查看标识符为1的进程需要root权限:
但是通常使用工具top(任务管理器)和ps进行查看:
top(按q退出)
ps详细选项可以查看man手册,下面使用如此选项进行查看全局的进程:
ps axj 加上管道可以配合grep、head、tail快速筛选自己想看到的。
此时pid就是此进程的标识符,ppid使其父进程。一般来说所有程序文件的父亲进程均为bash -- shell外壳程序创建子进程。
2.利用系统调用接口查看:
可以利用Linux提供的系统接口getpid函数查看当前程序(进程)的pid,getppid查看父进程pid。头文件使用
比如如下c语言程序:
1 #include
2 #include
3
4 int main()
5 {
6 printf("这是此进程的pid:%d,父进程pid为:%d\n", getpid(), getppid());
7 return 0;
8 }
由于此程序运行一结束就退出,那么此进程也就退出了,但是可以验证其父进程是否为bash,利用ps和管道 -- 使用head打出第一行。
ps axj | head -1 && ps axj | grep 24638
可以看到其父进程果然是bash进程。
那么,此普通进程可以利用系统接口创建子进程吗?当然可以。
利用pid_t fork();函数创建子进程,从此行的下一行开始,本进程和子进程共享代码块,但是数据子进程独自开辟一个空间,私有一份。(写入时拷贝)
pid_t是进程号类型,实际上就是以一整形,为了区分子进程和父进程使用不同的代码进行控制,父进程返回子进程的pid,子进程返回0,创建失败返回-1。可以利用这一点后面共享代码进行if判断,让父进程和子进程分别进行不同的工作。
有如下代码进行演示:
1 #include
2 #include
3
4 int main()
5 {
6 //创建子进程
7 pid_t pid = fork();
8 if (pid < 0)
9 {
10 perror("fork");
11 _exit(-1);//未创建成功
12 }
13 else if (pid == 0)//子进程执行区域
14 {
15 printf("本进程pid:%d,父进程为:%d\n", getpid(), getppid());
16 }
17 else{//父进程执行区域
18 printf("本进程pid:%d, 子进程为:%d\n", getpid(), pid);
19 }
20 return 0;
21 }
执行结果:
需要注意的:
父进程只有一个,但是子进程可以有多个。
父和子进程被创建出来后,先后运行不一定 -- 操作系统的调度器决定的。
是两个不同的执行流,即两个进程。
在了解Linux下的进程状态之前,首先来了解一下计算机操作原理讲的进程状态(理论状态)
首先我这里简单引用新建、运行、阻塞、挂起、退出五个状态进行说明:
(注:上述图细节尚未完善,后序补充,此时只是初识状态)
新建:
字面意思 没有入队列(实际上Linux上没有此状态,一般建立了就加载到运行队列中了)
运行:
此时储存线程属性信息的PCB(Linux下是task_struct)进入运行队列排队。此运行队列(run_queue)是由cpu创建的,用于享用cpu资源。就绪就是在排队中,执行即进程调度,此时cpu执行此进程 -- 单独执行。
阻塞:
等待非cpu资源。系统中许多进程需要用到其他资源(比如磁盘),那么此时不仅仅只有一个运行队列,当运行其进程得到需要获得其他资源时,就去对应资源区排队,获得之后方可继续在运行队列排队。例子:c语言中的scanf等待输入,否则该进程就一直在等待键盘资源。
挂起:
当内存不足的时候,OS会通过适当的置换进程的代码和数据到磁盘,此时就是挂起。当内存块不足的时候,长时间不执行的进程代码和数据换出到磁盘。这个磁盘有专门的分区:SWAP,但只是临时的,在cpu内信息的PCB还是存在。
退出:
即此进程执行完毕,终止。
在初步了解了进程之后相信大家对箭头很感兴趣吧,所以先具体将Linux下的进程状态具体的一说,箭头代表什么意思就非常好理解了。
Linux下的具体进程状态:
R(running)运行状态:此时进程在运行队列中(可能在排队或者是在调度)
首先可以通过命令行的方式进行查看:查看STAT(state--状态)
上面是为了能够查看R状态,即此时该进程被调度。该条命令是:ps axj | head -1 && ps axj | grep "ps axj"
(注意:+号表示是前台进程,即显示出来的,利用ctrl c可以停止,不是+号的话)
S(sleeping)睡眠状态:进程等待事件的完成。即等待外设资源,不在运行队列中(进程的大部分时间均在等待外设资源,运行代码的速度很快)。此睡眠状态可被中断。此状态可以包括上面的(阻塞状态和挂起状态(挂起状态可以和其他状态配合))
首先如上面演示中的s状态就是一个睡眠状态。
为了证明进程大部分时间均在等待外设资源,利用如下c语言程序和循环打印ps命令进行查看:
c程序:
#include
2 #include
3
4 int main()
5 {
6 while(1)
7 {
8 printf("你好呀!pid:%d\n", getpid());
9 }
10 return 0;
11 }
利用命令行局部变量进行循环执行ps命令以便查看其进程状态:
while :; do ps axj | head -1 && ps axj | grep test | grep -v grep;sleep 1 ; echo "-----------------------"; done
解释上面一串指令:
while : 表示循环
; 进行指令之间的分隔,即同时执行多项不同的指令
&& 类似于逻辑与,即均执行
head -1 保留第一行
| 管道,将左边得到的数据传给右边进行处理
grep 筛选,比如test就是查看名字叫此的进程 -v表示不包括含有此名字的进程即grep。
sleep 休眠,后面跟秒数
echo 传输字符串数据,默认打印到屏幕上
done 和循环有关
有了上面的基础,我们就可以看到s情况啦:
那么,中断是一个什么情况呢?
中断情况:
当计算机压力过大的时候,OS会通过一定的手段,杀掉一些休眠进程,来起到节省空间的作用。
但是有的时候特殊的进程需要等待磁盘大量写入,如果此时杀掉此进程就会出现问题。所以为了区别这种和普通休眠进程,就有了磁盘休眠状态。
D(disk sleep)磁盘休眠状态:此时进程必须自动等IO结束,不可被中断。也叫不可中断睡眠状态。
此阶段可以通过dd指令进行演示。
T(stopped)暂停状态/t调试状态:暂停进程的执行,调试代码过程中打断点也是暂停状态。
首先是一段死循环的代码(利用的是上面的c程序代码),可以通过kill指令发出某种信号让此进程暂停:
首先运行:
然后输入循环操控指令进行循环打印此进程状态:
kill -19 pid 暂停 kill -18 pid 继续
可以发现右边此进程也报出了此进程被暂停状态,可以进行查看:
使用继续命令:
此时就可以继续执行。利用ctrl c终止右边进程。
当然,也可以使用指令 kill -9 pid 杀死此pid的进程:
那么现在利用gcc -g的debug版本使用gdb工具进行代码调试会发现什么情况呢?
比如上述进程即运行到断点处,利用ps查看进程就可发现1060进程此时就是处于t暂停状态,可以特指被调试状态。
X(dead)死亡状态:即终止状态,具有瞬时性,被标识等待被cpu释放。
(代码和数据被释放,pcb数据不再维护)
由于死亡状态具有瞬时性,在任务列表无法显示,所以无法展示。
两种和子进程相关的状态:
Z(zombie)僵尸进程:一个进程退出了,但是不被运行操作系统释放,处于一个被检测的状态。(即当父进程创建一个子进程,子进程提前退出,父进程没有使用wait()系统调用接收,并且父进程还在运行。那么此时子进程就是一个僵尸进程)
一般进程是由父进程或者操作系统进行回收,如果没有回收成功,那么就是僵尸进程,此时该进程空间没有得到释放,会造成内存泄漏。
由上面的内容,我们可以通过创建子进程的方式来进行僵尸进程测试:
#include
#include
int main()
{
pid_t pid = fork();
if (pid < 0)
{
return 1;
}
else if (pid == 0)//子进程
{
sleep(1);
printf("子进程死亡,pid:%d\n", getpid());
}
else
{
//父进程
while(1)
{
sleep(1);
printf("该进程pid:%d,子进程为:%d\n", getpid(),pid);
}
}
return 0;
}
首先利用循环打印ps指令进行监控:
while :; do ps axj | head -1 && ps axj | grep test | grep -v grep;sleep 1 ; echo "-----------------------"; done
然后运行程序,会发现如下结果:
一秒后子进程就变成了Z僵尸进程。
孤儿进程:此进程是一个特殊的状态,即其父进程比自己先退出,那么此时此进程为了之后便于回收与释放,所以被1号进程init领养(系统本身进程),但是此时对于其ctrl c就没有用了,因为父进程不是本身执行的进程了,利用kill信号进行结束进程。
利用上面的信息,同样可以使用创建子进程来检测这种状态,观察子进程的ppid是否由原来的父进程变成init进程即可。
c语言代码实现:
#include
#include
int main()
{
pid_t pid = fork();
if (pid < 0)
{
return 1;
}
else if (pid == 0)//子进程
{
while(1)
{
sleep(1);
printf("子进程,pid:%d,父进程为%d\n", getpid(),getppid());
}
}
else
{
//父进程
sleep(1);
printf("该进程pid:%d,子进程为:%d\n", getpid(),pid);
}
return 0;
}
同样使用whilebash进行循环查看ps,然后运行test:
就可以发现一秒后父进程结束,子进程没有,被pid为1的init进程进行托管。此时在右边页面的ctrl c无法停止进程,需要用kill信息进行终止即可。
进程优先级指的是cpu分配资源的先后顺序。
优先级越高,优先分配cpu的资源。
cpu的资源有限,必须通过某种方式竞争资源。
在Linux,ps -l 可以查看到当前路径进程的PRI和NI信息:
那么,在此可以先对Linux下的进程优先级下定义:
(PRI)优先级 = 老的优先级(PRI) + nice(NI)
PRI:优先级,是整数,越小优先级越高。
NI:优先级修正值,nice
那么上面的公式有什么意义呢?用来调整优先级的。
下面就随便利用一个进程来进行调整:(我使用的是c语言程序 -- 最好单进程的死循环)
使用top任务管理器进行修改优先级:
首先运行自己的进程,找到其对应的pid后,在top输入r:
输入对应的pid,然后就可以设置NI值,首先可以通过输入L查找进程名进行查看默认PRI和NI:
然后设置NI:NI输入范围[-20, 19]超出范围以断点最大值为准。并且普通用户只能往大的升,要想提高进程优先级,即让PRI变小,需要使用root权限哦~
权限变小:
此时执行sudo top让权限变大:
可以发现进程为6382的test进程由30变为20了,不是输入的0吗?那么这就说明了上面的公式老的PRI就是以一开始默认的PRI来进行变化的。
通过以上测试,可以得到结论:
权限越大,PRI值越小(反比)。并且每次变化时都是以最开始默认的PRI开始变化的。权限要变大,还必须要root权限才可执行。设置的NI(nice)值只能在[-20, 19]内进行变化。
想想看为什么nice值会限定在如此区域内呢? -- cpu的策略并不是强调谁更优先,而是尽量的平均分配。
实际上在描述进程的时候已经提过了,在Linux下:
所有运行在系统里的进程均以task_struct链表的形式存在内核里。
那么,现在了解一下进程整个的流程是如何进行的吧:
首先,我们需要了解如下的相关概念:
独立性:多进程运行,需要独享各种资源,多进程运行期间互不干扰。(包括父子进程)
竞争性:进程数目众多,cpu资源很少。便就有优先级,合理分配资源。
并行:多个进程在*多个cpu分别同时进行运行。(如果有两个cpu,那么在某一时刻,会有两个进程同时进行)
并发:
时间片:给每个进程相对一定的时间进行使用cpu资源。时间一到选择下一个进程。
抢占与出让:此时正在享用cpu资源并且时间片没有完但是此时来了更高优先级,那么就会发生抢占。即cpu执行一个进程在时间片或者抢占出让影响下,在一端时间内较为均衡的分配资源。所以,在一个cpu下,多个进程在一段时间内同时得以推进。(一个时刻就是一个进程)线程切换:cpu内部有很多寄存器。如果此时一个进程正在运行,那么cpu内的寄存器--硬件(用户进程可显寄存器,系统内部不可显寄存器)一定保存此进程的临时数据。此临时数据就是该线程的上下文。此上下文数据绝对不能丢弃,一旦被丢弃,那么此线程就要被报废。
那么此时如果时间片到了,或者被抢占要么出让,该进程没有运行完,那么此时进程就需要带走自己的上下文数据,暂时切下,等待下一次运行直接填充上下文数据继续执行上次中断的步骤。
cpu内部寄存器存储的上下文只有一份,但是上下文可以有多份,分别对应不同的进程。
了解到上面的概念后,那么最开始的线程状态的那张图也就可以完善了:
图如果还有不完善的地方欢迎评论区补充呀!
上面了解完基本的进程概念后,现在我们可以涉足一些环境变量之类的概念:
环境变量(environment variables)一般是指在操作系统中用来指定操作系统运行环境的一些参数。在系统中具有全局属性。
比如平时c/c++语言在链接动态库的时候,我们不知道库在哪里,但是也可以自动的链接到,这就是环境变量的用处。
常见的环境变量有哪些呢?
PATH:指定命令的搜索路径
HOME:指定用户的主工作路径(登录到Linux默认的路径,比如root就是/root,用户就是/home/用户名)
SHELL:当前shell,即操作系统外壳程序,在centOS下主要是/bin/bash。
echo $[name] [name]是你想查看环境变量的名字,比如如下测试:
可以发现其实指令也就是一个一个程序(进程),那么他们运行的时候就不用带路径,而我们自己写的程序就必须带路径,如下:
which [指令名] 可以查找指令的路径。下面测试下路径能否使用指令:
能够使用指令,那我们随意写的一个自己的c/c++程序执行就必须要使用路径:
.就表示是当前路径下,直接像指令那样只用名字是不行的。那么怎么做才可以像指令那样的效果呢?
可以发现,在之前的PATH中,会发现里面包含着/usr/bin路径,此路径下就是存放指令的程序文件:
那么是不是我们将我们程序的路径放入PATH指定路径下就可以像上面指令那样只打名字就可以运行呢?
答案是当然可以,利用指令export设置一个新的环境变量或者添加即可:
export PATH=$PATH:[test程序所在路径]
此时就可以发现在原本的PATH指定命令路径下多了一个我们自己添加的,这个时候我们直接写该命令名字即可:(注意不要使用test,使用mytest吗,test系统内存在其名的程序)
总结:
可以使用export指令修改PATH指定命令路径,让自己的程序也能直接打名字运行,使用方式:export PATH=$PATH:[命令路径] ,其中PATH的格式,路径直接以:作为分隔符,最后一个没有分隔符。但是此时修改的并没有写入文件,是暂时性的,重启服务器就恢复了。
env 显示所有的环境变量:
export 设置一个新的环境变量 (通过echo $环境变量名 进行查看内容)
set 显示本地定义的shell变量和环境变量:
unset 清楚环境变量:
如上就是一个环境变量表,这就是其的组织方式,并且,每个程序都会收到此表。环境表是一个字符指针数组,每个指针指向以'\0'结尾的环境字符串。
那么,在实际的程序里,是如何实现的呢?
对于c语言来说,main函数最多可以有三个参数。
int argc, char* argv[], char* env[]
其中env就是环境变量指针的储存数组 -- 查看系统给此进程传递的环境变量;
argc、argv:命令行参数。
argc是接受传入的多少个选项,argv接收的就是字符数组指针了,首先默认第一个就是此进程名字。
特别的,这些环境变量是继承过来的,即:
所有的环境变量都会被子进程继承,普通的进程都是从bash父进程获取的环境变量。
bash的环境变量从操作系统来。
1.演示环境变量指针:
#include
#include
int main(int argc, char* argv[], char* env[])
{
int i = 0;
while(env[i])
{
printf("%s\n", env[i++]);
}
return 0;
}
此时运行就会出现如下结果:
这样就可以在程序里面查看到环境变量啦,那么命令行参数是如何回事呢?
2.演示命令行参数:
#include
#include
int main(int argc, char* argv[])
{
printf("argc:%d\n", argc);
int i = 0;
for(; i < argc; i++)
{
printf("%s\n", argv[i]);
}
return 0;
}
此时默认运行此程序不加任何参数就是 argc为1,且命令行数组里面存的一个字符串就是该进程名字,后面跟上参数就会统计不同的有几个,以空格区分:
3.使用系统调用查看环境变量:
当然了,除了给main函数传参外,可以使用系统接口:getenv("环境变量名")就可以查看(需要头文件stdlib.h),比如我在程序里想查看PATH,就写入PATH即可,展示出如下结果:
可以发现查询结果一致,代码如下:
int main()
{
printf("%s\n", getenv("PATH"));
return 0;
}