目录
(1)我们用命令行的方式给我们的bash进程加上一个特殊的环境变量
本文主要讲解进程以相关知识,是小编学习这部分的知识总结,本文以通俗易懂为主旨,提高各位学习系统知识的兴趣,若有错误,恳请及时联系小编;
进程:是一个程序对某个数据集的执行过程,是分配资源的基本单位;
以上为书本上的概念,如若这么理解进程,我认为有点笼统了,没有特别深刻,接下来我们从操作系统的角度来理解进程;
任务管理器都用过吧?我们点掉进程这一页,如上如所示,我使用CCtalk是一个进程,pft编辑器是一个进程,谷歌浏览器也是一个进程;例如我们的谷歌浏览器吧,我们在桌面上有这个软件的快捷方式,我们右键点击并查看属性,我们可以看到我们实际装在了下图这个路径下,我们找到这个目录;
果不其然,我们在该路径下找到了该文件,我们双击便可运行该软件,实际上,我们是将这个可执行程序加载进内存中,然后才可以运行,因为可执行程序要想运行就必须先加载进内存中,这是由于冯诺依曼体系结构决定的,如果这么讲解还不是很理解可以点击下方链接,查看下面的文章;
CPU只和我们的内存打交道,因此程序被加载进内存后,我们CPU就可以从内存中拿走可执行程序执行了吗?在回答这个问题之前,我们得回答下面这个问题;
问题一:我们得计算机内存中只能有一个可执行程序吗?
答案肯定是否定的,我们想都不用想都可以回答出来,若只有一个程序,那我们平常不都可以边听歌,边打游戏吗?并且我们的计算机同一段时间可以有大量的可执行程序储存在内存中等待执行;
问题二:我们的计算机同一时刻,一个CPU上可以执行多个可执行程序吗?
答案也是否定的,我们的计算机在某一个时刻,某个CPU只能执行某一个程序,只不过通过时间片轮转的方式,在某一段时间执行多个程序,这也是并发的概念;
问题三:既然内存中有大量的可执行程序,而我们CPU资源是有限的,我们的操作系统必然就要管理这些可执行程序的执行,那么我们的操作系统是如何管理这些可执行程序的呢?
答案是先描述,再组织。我们先将加载进内存中的可执行程序描述起来,这里的描述可以理解为用结构体记录关于这段程序的信息,如标识符、状态、优先级、程序计数器、内存指针等等信息,然后通过某种数据结构将这些结构体对象组织起来,如链表、顺序表等;其中我们描述其生成的结构体我们称其为PCB控制块,在Linux中,这个结构体叫task_struct;
我们进程就是由的PCB控制块和可执行程序的代码与数据组成的,概念中提到进程是某个数据集的执行过程。这里的执行过程,是某一刻时间,因为PCB中的数据是会发生改变的,如其中有记录下一条执行指令的地址,每当执行完当前指令都会进行更新;
此时,我们就可以回答在第一小结我们遗留下的问题,我们可执行程序被加载进内存中,必须先新建一个对应的PCB控制块,我们通过这个PCB控制块记录这段可执行程序的信息,然后用某个队列来维护这些PCB控制块来排队获取CPU资源,我们称这个队列为就绪队列;
前面我们介绍了进程的基本概念,并对其产生了深刻的理解,现在我们来见一见所谓的进程,在这之前,我们首先学习进程的第一个属性,其标识符,这个标识符是一个整型数字,在当前机器上是唯一的,我们称其为pid,我们可以通过 getpid 系统调用获取当前进程的pid;顺便介绍另一个系统调用接口 --- getppid,获取当前进程的父进程的pid;
我们通过man查询2号手册,查询上述系统调用相关信息,如上图所示,我们需要包含两个头文件,我们可以写出如下代码;
我们可以通过ps -axj指令来查看所有进程的信息,并用grep语句过滤出当前进程信息;我们不难看出我们运行的进程pid为13354;
实际上,在我们的 /proc 目录下会自动生成一个名为 13354 的目录记录当前进程信息;如下图所示;
如下面这两个文件,cwd简称current work directory,当前工作目录,是当前进程的可执行程序所工作的目录,exe文件是可执行程序的路径;
在Linux下,若我们想在我们自己的程序中创建进程,我们就要通过fork函数来完成该任务,下面我们来介绍关于fork函数的使用;
以上为我们通过man指令查询结果,我们使用前,我们需要加入头文件 unistd.h,并且该函数无参数,有一个pid返回值,我们先不管这些直接调用一个该函数查看效果,我们运行以下代码;
我们编译运行,结果如下图所示;
我们惊奇的发现,我们退出函数打印了两次;没错,另一个就是我们创建出的子进程打印的结果;
我们继续通过man指令来查询我们的fork函数的使用,如下图所示;
粗略的翻译一下,如果函数调用成功,子进程的pid会返回给父进程,0会返回给子进程;如果函数调用失败,-1会返回给父进程,子进程不会被创建,错误码被设置;
这么看如果我们函数调用成功会返回两个返回值?从C语言、C++来看这是不可能的呀,我们的语法上都规定只能返回一个返回值,可这个函数却有两个返回值,这也太牛了吧;具体原理我们暂不介绍,我们直接实践一下,是否如我们所料;
运行结果如下图所示;
我们通过 if 进行了执行流分流,我们同时执行了 if 语句中的两个分支;我们现在看到的现象是fork函数之后又两个执行流的现象,且我们通过 if 语句使其看同一份代码,而执行不同的语句,这是我们初步的认识;
问题:为什么是给父进程返回子进程的pid,给子进程返回0,而不反过来呢?
首先,我们得清楚认识,一个子进程只能有一个父进程,而一个父进程可以有多个子进程,那么作为父进程,是否需要对子进程进行管理呢?那么管理我们怎么管理呢?我们若接收得是0,多个子进程之间父进程无法对它们进行区分。
fork函数是如何返回两个返回值的呢?return语句只能返回一个值吗?接下来,我们来一起探讨这个问题;
- pid_t fork()
- {
- // 1、子进程的创建
- ......
-
-
- // 2、返回返回值
- return pid;
- }
虽然我们不知道fork函数是如何创建子进程的,但是我们能知道fork函数具体干了两件事,一个是创建子进程,一个是返回返回值,这两步是必做的,那么问题来了;
问题:第一步执行完毕后,fork函数的主要逻辑是否完成??
答案无疑是肯定的,当我们执行完创建完子进程后,此时就已经有两个执行流了!!!我们之前看到的现象是fork函数执行完毕后才有两个执行流这种理解是错误的!!既然我们fork函数内子进程创建完毕后就有两个执行流,那么创建完毕后的代码应该是共享的!所以这两个执行流就会返回两个值!!故fork函数有两个返回值;
我们发现,在我们之前代码中,我们用 id 接收fork返回值,id居然有两个值,这里我们怀疑者两个 id 并不是同一个 id变量,因此我们尝试打印这两个 id 的地址,代码如下;
我们运行上述代码,结果如下;
神奇的一幕发生了,我们发现这两个id值是相同的,正如我们所料,而它们的地址也相同,这是与我们前面所学的知识完全相悖;关于这部分的内容,我们必须先了解进程的地址空间,因此,我们放到后面这篇文章进行解释;
一个进程的生命期可以划分为一组状态,这些状态刻画了整个进程;这是出现在教材中的概念;
在操作系统这门课程中,我们将进程状态划分为五种状态,也有七种状态,其中其中状态是在原本五种状态的基础上增加了两种挂起状态;如下图所示;
想必大家经常看到如上这张图,上图中七个椭圆形就是进程的七种状态,上面每个箭头就是状态之间相互切换;下面我来以此分析以下几种状态之间的相互转换
NULL->新建态:程序加载进内存后就创建相关PCB控制块等;
新建态->就绪态:新建好进程后就进入就绪态队列中排队;
就绪态->运行态:队头的进程将得到调度,进入CPU准备运行,进入运行态;
就绪态->挂起就绪态:由于内存较为紧张,将就绪态的进程换入磁盘的交换区中暂存;
运行态->就绪态:由于时间片到了被迫让出CPU,重新进入就绪队列中;
运行态->阻塞态:由于运行过程中碰到IO事件,主动让出CPU进入阻塞态;
运行态->终止态:知道到程序的末尾,退出程序,进入终止态;
运行态->挂起就绪态:一个优先级较高的程序的挂起事件结束,需要抢占CPU,而此时内存恰好较为紧张,此时运行态中的进程可能会由运行态直接进入挂起就绪态;
阻塞态->就绪态:进程所需IO事件已经完成,此时会由阻塞态进入就绪态的等待队列进行排队;
阻塞态->挂起阻塞态:由于内存紧张,而导致操作系统将内存中阻塞态的进程挂起到磁盘中的交换区来缓解内存紧张的问题;
挂起阻塞态->阻塞态:当内存紧张的问题得到缓解,且我们处于阻塞挂起态的进程优先级较高时,操作系统可能会将这个进程放入内存,等待IO处理;
挂起阻塞态->挂起就绪态:等待IO事件的发生实际上不需要将代码和数据调入内存,而当我们等待事件得到满足后,我们会由挂起阻塞态转换成挂起就绪态;
挂起就绪态->就绪态:当就绪队列中没有进程 或 挂起就绪态进程优先级比就绪态进程优先级更高时,操作系统会将进程由挂起就绪态转换成就绪态;
都说操作系统是计算机界的一门哲学,是计算机的理论知识,在Linux中,没有像上述那样设计进程状态,在操作系统代码实现上,与上述理论知识也可能存在一些细微的差异;一共有如下5种状态;
R:运行状态,这里的运行状态包括在就绪队列中等待的状态,我们统称为运行态;
S:睡眠状态,这里的睡眠状态指的是可中断睡眠,是在等IO事件的完成;
D:磁盘休眠状态,这里的睡眠状态指的是不可中断睡眠,这个状态下通常会等待磁盘IO完毕;
T:停止状态,我们通常可以发送 SIGSTOP 信号停止进程,也可发送 SIGCONT 信号使其继续运行;
t:停止状态,这里的停止状态一般为调试阶段停止程序;
X:死亡状态,这个状态一般指的是进程运行完毕后的一个状态;
Z:僵尸状态,僵尸状态指的是当前进程退出后,父进程并没有对其进行wait,此时该进程运行结束后会进入僵尸状态,若父进程不对子进程进行回收,子进程将会一直处于僵尸状态;
下面,我将展示下面部分状态,首先,我们写出了如下代码;
接下来我们通过ps指令对这个进程状态进行监视,具体shell脚本编写如下;
-
- while :; do ps -axj | head -1 && ps -axj | grep test | grep -v grep; sleep 1; echo "----------------"; done;
-
我们首先运行代码,将不断打印 hello world,接着我们在打开一个ssh会话,输入我们的shell脚本,结果如下图所示;
我们发现大部分都是属于等待态(S),只有很少一部分是运行态(R),这是为什么呢?我们的代码不是不断的打印吗?不应该一直处于运行态吗?
实际上,我们上述代码,大部分事件都在IO,因为IO所消耗时间远远大于运行的时间,而IO一般都会进行等待IO事件完成,因此我们这个进程大部分都处于等待状态;
补充:
在上述状态中,我们看到的是R+和S+,这个加号代表该进程为前台进程,所谓前台进程就是我们在命令行中以前台的方式运行,此时我们无法继续输入命令,直至该程序运行结束,而如果我们以后台的方式运行,我们依旧可以像命令行中输入指令,且会做出响应;(运行时后加 & 即以后台进程的方式运行该程序)
我们再演示一下停止状态;我们可以使用 kill -l 查看有哪些信号,如下图所示;
我们分别用19号信号(暂停)与18号信号(继续)来测试我们的T状态,首先,我们运行程序并监视状态,接着发送19号信号,如下所示;
我们对指定进程发送19号信号后,我们的程序停了下来,进程状态也由S+转换成了T状态;接着我们再尝试发送18号信号试试;
我们在发送18号信号后,我们不难发现,我们的程序有运行起来了,状态也随之改变,这里有一个细节,这里重新运行起来的程序不在是前台程序,而是后台程序了!这也就意味着我们可以输入命令,如我们输入发送9号信号终止这个进程,大家可以试试;
由于刷屏过快,我输入的指令被刷上去了,但却是可以执行我输入的指令,也成功干掉了这个进程;
我们再来试试由gdb打断点引起的程序停止,如下图所示,我们使用gdb进行调试,在遇到断点后,状态就变成了 t ;
首先,我们编写如下代码,主要是让子进程运行3秒后退出,父进程一直执行;
运行该代码,并观察其运行状态图,如下所示;
我们发现前三秒,子进程未退出时,处于S状态,接着3秒后,子进程退出,且父进程未对其进行回收,此时变处于僵尸状态;
僵尸进程会导致资源长期不释放而引起的资源泄漏,我们必须让父进程对其进行资源回收,后面会有介绍;
前面我们讨论了若子进程退出,而父进程还在运行且不对子进程进行回收会造成子进程称为僵尸进程,那么当父进程退出子进程仍在运行的时候呢?没错,此时子进程就会成为孤儿进程,孤儿进程由1号进程领养,也当然由1号进程来进行回收,我们在上述代码上进行修改,我们可以得到如下代码;
我们发现在运行3秒后,父进程退出了,子进程由1号进程进行管理;
所谓进程优先级是CPU资源分配给进程时的先后顺序,优先级高的进程优先获取CPU资源,反之则落后于其他进程获取CPU资源;
我们都知道计算机上可能会有大量的进程,而我们的CPU资源是有限的,就是由于大量的进程和少量的CPU资源,因此产生了资源的竞争,因此就有了优先级的概念;
新优先级 = 老优先级 + nice值
我们可以通过 ps -al 指令查看进程优先级情况;其中如下图所示;
UID:执行者的身份;
PID:进程pid;
PPID:父进程的pid;
PRI:老的优先级(初始始终为80);
NI:nice值(老的优先级[80] 加上nice值则为新的优先级,取值范围 -20 ~ 19);
我们可以通过top指令修改优先级;如我们将进程test优先级修改为89;
1、输入top指令
2、输入r并输入要修改进程的pid值
3、输入要修改的nice值(由于想把优先级值改为89,故输入10)
4、再次查看优先级
如我们所料,优先级的值被修改成了89;同样我们输入负数还可以把优先级值改小,不过都在80的基础上修改;
注意:
1、优先级的值越小,优先级越高,反之优先级越低;
2、nice值得范围是-20 ~ 19,因此优先级得值的范围是 60 ~ 99;
竞争性:由于CPU数量的有限,进程间存在竞争性;
独立性:各个进程相互独立,不妨碍彼此执行,因此进程具有独立性;
并行:多个CPU同一时刻执行多个进程称之为并行;
并发:一个CPU一段时间内执行多个进程称之为并发;
我们现在大多数系统都是采用分时系统;每个进程在CPU内执行一段时间后出来,通常这段时间都非常短;那么我们下一次切换到该程序时是如何保证从上一次运行结束的位置接着运行呢?
我们的CPU通常会配套一套寄存器,这些寄存器用于保存运行程序时产生的临时数据,若我们在程序离开时将寄存器中的数据保存起来,下一次运行时可直接那上次保存的数据进行恢复操作,此时便可接着上次运行结果继续运行了;
环境变量指的是操作系统中,用来指定操作系统运行环境的一些变量;这个是基本概念,这么说想必你多多少少还是会有一些懵圈,接下来,我们一个问题来介绍我们的环境变量;
问题:我们都知道我们平常在Linux下运行的指令,如ls等都是用C语言写的可执行程序,那么为什么我们调用系统的指令不需要路径,而我们调用自己写的可执行程序需要指定路径呢?
实际上,这是因为我们在环境变量PATH中添加了ls指令所在路径,因此我们无需指定路径就可以运行系统指令;我们可以通过 echo $PATH 查看PATH变量;
我们查看了ls指令的路径,接着我们查看环境变量PATH,我们发现其中也有ls的路径;实际上,我们在执行命令时,首先会在PATH的所有路径中查找是否有ls指令,若有则执行,若无则会报错,找不到指定路径;
我们可以直接在命令行输入 env 获取当前进程(一般是bash)的环境变量;
我们可以看到,我们的环境变量是以键值对的形式存在,等号左边的是key,等号右边的是值;这里简单的介绍几个环境变量;
PATH:指定命令搜索路径
HOME:当前用户的主工作目录
SHELL:SHELL的路径
USER:当前用户
PWD:当前所在路径
实际上,我们的main函数有三个参数,第一个参数为命令行参数的个数,第二个参数为命令行参数字符串数组,第三个参数就是环境变量字符串数组,这里主要讲解第三个参数如下代码;
我们的环境变量数组总是以NULL结束,故我们可以是用遍历的方式,判断是否遍历到了NULL,若遍历到了NULL,则停下来,此时我们便可以取出所有的环境变量了;
我们也可以使用全局变量environ来获取我们的环境变量,其中environ这个全局变量在头文件 unistd.h 中,以下为man手册查询结果;
因此,上述代码还可以改成这样;
我们依旧输出了所有环境变量;
我们可以直接通过某个环境变量的可以获取这个环境变量的值;我们通常使用这种方法,以下为man手册查询结果;
因此,我们可以这样查询PATH环境变量,如下代码所示;
我们运行上述代码,结果如下图所示;
我们刚刚通过 echo 以命令行的方式来查看我们的环境变量,同样,我们也可以以命令行的方式修改我们的环境变量,我们通过 export 来对环境变量进行修改;假设,我们想要使我们刚才写的可执行程序test也可以不带路径就可以运行,我们可以做如下操作;
此时,我们再查看我们的环境变量,这时我们的环境变量就有我们当前路径了;
同样,这时我们就可以不带路径执行我们的 myproc 这个可执行程序了;
注意:我们这里修改的环境变量在下一次登陆时会被重置,除非我们更改相关配置文件,否则仅在此次更改有效;
我们可通过函数putenv来对环境变量做修改,首先介绍putenv,声明如下;
int putenv(char* str);
其中str参数为环境变量键值对,也就是 key=value 这种类型字符串,若我们的key已经存在原来的环境变量中,则直接更改value,若不存在则添加这个新的环境变量;关于返回值,若返回0则表示调用成功,若返回非0则表示调用失败;
我们可以敲出如上代码进行添加环境变量的添加;运行后结果如下;
有些小伙伴们发现若使用env,则发现我们并未添加这个环境变量;这是由于我们的环境变量设置仅在当前进程设置,而我们的命令行在bash这个进程中,而我们新运行的程序时bash的子进程,其中命令行修改环境变量的方式是修改bash进程的环境变量;
我们的环境变量是具有全局性的吗,所谓的全局性通常体现在其会被子进程继承,我们可以通过如下实验来验证这一猜想;
实验:
我们运行上述代码,果然我们找到了这个环境变量;
我们修改了Bash进程的环境变量,而我们写的程序都是在Bash下运行的,故我们运行的可执行程序都是Bash的子进程;若我们在子进程中找到了我们在Bash进程中添加的特殊环境变量,说明环境变量会继承给子进程;