文章目录
- 1. 运行队列和运行状态
- 2. 进程状态
- 3. 两种特殊的进程
- 僵尸进程
- 孤儿进程
- 4. 进程优先级
- 5. 进程切换
- 进程特性
- 进程切换
- 6. 环境变量的基本概念
- 7. PATH环境变量
- 8. 设置和获取环境变量
- 9. 命令行参数
1. 运行队列和运行状态
💕 运行队列:
进程是如何在CPU上运行的:CPU在内核上维护了一个运行队列,进行进程的管理。让进程进入队列,本质就是将该进程的task_struct 结构体对象放入运行队列之中。这个队列在内存中,由操作系统自己维护。
💕 运行状态:
运行状态
进程PCB在运行队列里就是运行状态,不是说这个进程正在运行,才是运行状态。状态是进程内部的属性,所有的属性在PCB里,进程不只是占用CPU资源,也有可能随时要外设资源。阻塞状态
进程因为等待某种条件就绪而导致的一种不推进的状态——进程卡住了,此时的进程要通过等待的方式,等待具体的资源被别人用完之后,再被自己使用。阻塞状态进程的PCB被放在硬件的等待队列中。本质是对tack_struct对象放到不同的队列中!
挂起状态
如果系统中存在许多进程,进程短期内不会被调度,代码和数据在短期内不会被执行,此时如果内存空间不足,操作系统就可以把代码和数据暂时保存到磁盘上,节省一部分空间,该进程暂时被挂起了,这就是挂起状态。对于阻塞状态和挂起状态,阻塞不一定挂起,挂起一定是阻塞。
因此,所谓的进程不同的状态,本质是进程在不同的队列之中,等待某种资源。
2. 进程状态
为了弄明白正在运行的进程是什么意思,我们需要知道进程的不同状态。一个进程可以有几个状态(在Linux内核里,进程有时候也叫做任务)。
下面的状态在kernel源代码里定义:
/*
* 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) : 并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队列里。S
睡眠状态(sleeping): 意味着进程在等待事件完成(这里的睡眠有时候也叫做可中断睡眠(interruptible sleep)。D
磁盘休眠状态(Disk sleep)有时候也叫不可中断睡眠状(uninterruptible sleep),在这个状态的进程通常会等待IO的结束。T
停止状态(stopped): 可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运行。X
死亡状态(dead):这个状态只是一个返回状态,你不会在任务列表里看到这个状态。Z
僵尸状态(zombie):这个状态是一个已经运行完的子进程,等待父进程回收他的返回值。
💕 运行状态R
:
💕 睡眠状态S
:
当我们执行可执行程序后,在使用ps命令查看进程状态时,发现他是S状态,为什么我们的程序正在运行,进程却是睡眠状态呢?
这是因为我们使用printf进行打印时,需要访问外设,但外设的速度是远远低于CPU的,所以进程大部分时间都在等待硬件资源的就绪,所以我们每次查看时,进程几乎都处于阻塞状态。
💕 磁盘休眠状态D
:
当内存中的空间不足时,操作系统会让一些进程进入挂起状态,但是,如果内存的空间严重不足时,进程挂起也解决不了问题,这个时候操作系统可能会将一些暂时挂起的进程或者没有被调度的进程杀掉。
但是如果要是此时挂起的进程正在向磁盘写入数据,或者进行一些重要的数据传递时,如果操作系统将这个进程杀死,那么磁盘就可能会将正在传递的数据丢弃,此时重要的数据就有可能因为操作系统将进程杀死导致重要数据的丢失。所以,我们的深度睡眠状态就是针对这种情况而生的。深度睡眠状态下的进程既不能被用户杀死,操作系统也无法将其杀死。只能通过断点,或者等待进程自己醒过来。
💕 暂停状态T
:
暂定状态也是阻塞状态的一种,下面我们来看一下如何让一个运行状态下的进程暂停:
我们可以使用kill的19号指令将一个进程从运行状态变为暂停状态。
如果想要使得休眠中的进程重新恢复运行状态只需要执行kill的18号命令即可:
这里我们可以看到进程又重新进入了运行状态,但是为什么进程重新进入云心状态后后面的 +
号 消失了呢?
其实,进程状态后面的 +号 表示的是这个进程是一个后台进程,如果没有 +号就表示这个进程是一个前台进程。对于前台进程我们可以使用Ctrl + c将其杀死,但是对于后台进程,我们只能使用kill命令杀死他。
💕 追踪暂停状态t
:
属于暂停状态的一种,表示进程正在被追踪。最典型的一种就是gdb调试进程的时候。
💕 死亡状态X
:
表示一个进程结束运行,他的PCB以及代码和数据全部都被操作系统回收。
💕 僵尸状态Z
:
一个进程在退出的时候,不能立即释放全部的资源,但是该进程已经不会再被执行了,该进程的PCB中存放着他的各种各样的状态码,尤其是退出状态码。
僵尸状态就是为了在进程退出的时候能够让父进程或者操作系统拿到他退出状态码,然后释放PCB的一种状态。
3. 两种特殊的进程
僵尸进程
僵死状态(Zombies)是一个比较特殊的状态。当进程退出并且父进程没有读取到子进程退出的返回代码时就会产生僵死(尸)进程。僵死进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码。所以,只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程进入Z状态。
下面我们来举例看一下僵尸进程:
当我们杀掉子进程后,由于父进程并没有读取子进程的退出状态码,所以子进程进入了Z
(僵尸状态),如果父进程一直不读取子进程的退出状态码,那么子进程就会变成僵尸进程。
僵尸进程的危害:
父进程如果一直不读取子进程的退出状态码,那子进程就一直处于Z状态。维护退出状态本身就是要用数据维护,也属于进程基本信息,所以保存在task_struct(PCB)中。换句话说, Z状态一直不退出, PCB一直都要维护。那一个父进程创建了很多子进程,就是不回收,就会造成内存资源的浪费。
孤儿进程
父进程如果提前退出,那么子进程就会被操作系统领养,此时的子进程就称之为“
孤儿进程
”,孤儿进程被1号init进程领养,最后由init进程回收。
这里我们可以看到:如果将父进程杀死后,父进程并不会进入僵尸状态,这是因为父进程在退出后会被父进程的父进程——bash
所读取他的退出状态码。还有子进程被1号进程领养后会由前台进程变成后台进程。
4. 进程优先级
CPU的资源是有限的,但是内存中有很多进程都需要占用资源,所以需要给进程指定优先级来合理的分配资源。
CPU资源分配的先后顺序,就是指进程的优先级(priority)。优先级高的进程有优先执行权利。配置进程优先权对多任务环境的linux很有用,可以改善系统性能。还可以把进程运行到指定的CPU上,这样一来,把不重要的进程安排到某个CPU,可以大大改善系统整体性能。
当我们输入ps -l/ps -al
指令后就可以看到进程优先级相关的属性:
UID
: 代表执行者的身份PID
: 代表这个进程的代号PPID
:该进程的父进程的代号PRI
:代表这个进程可被执行的优先级,其值越小越早被执行NI
:代表这个进程的nice值
下面我们重点介绍一下PRI
和NI
这两个变量,PRI(priority) 即进程的优先级,或者通俗点说就是程序被CPU执行的先后顺序,此值越小进程的优先级别越高。而NI(nice) 表示进程可被执行的优先级的修正数值。需要强调一点的是,进程的nice值不是进程的优先级,他们不是一个概念,但是进程nice值会影响到进程的优先级变化。可以理解nice值是进程优先级的修正修正数据。
PRI值越小越快被执行,那么加入NI值后,将会使得PRI变为: PRI(new)=PRI(old)+NI,这里我们需要注意的是:每个进程默认的PRI都是80,NI都是0,但是NI的波动范围是:[-20,19]
,PRI与NI的和越小,进程的优先级越高。
下面我们来看一下如何修改进程的优先级:
(1) 输入top
指令
(2) 输入r
(3) 输入进程的id
(4) 输入NI值
这里我们还需要注意的是:普通用户无法直接修改NI的值,必须切换成root用户或者使用sudo提权执行top指令。
5. 进程切换
进程特性
- 竞争性: 系统进程数目众多,而CPU资源只有少量,甚至1个,所以进程之间是具有竞争属性的。为了高效完成任务,更合理竞争相关资源,便具有了优先级
- 独立性: 多进程运行,需要独享各种资源,多进程运行期间互不干扰
- 并行: 多个进程在多个CPU下分别,同时进行运行,这称之为并行
- 并发: 多个进程在一个CPU下采用进程切换的方式,在一段时间之内,让多个进程都得以推进,称之为并发。
进程切换
一个CPU里面存在一套硬件寄存器,宏观上寄存器分为用户可见,用户不可见。
计算机调度某个进程时,CPU会把这个进程的PCB地址加载到某个寄存器,也就是说,CPU内有寄存器可以只找到进程的PCB地址。
CPU里有一个eip寄存器(PC指针),指向当前执行指令的下一条指令的地址。
而进程运行的时候一定会产生很多的临时数据,但这些临时数据只属于当前进程,虽然CPU内部只有一套寄存器硬件,但是寄存器保存的数据只属于当前进程,也就是说,寄存器硬件不是寄存器内的数据,这是两码事,寄存器被所有进程共享,但是寄存器里的数据时每个进程各自私有的。
时间片引出——进程在运行的时候占有CPU,但是却不是一直占有到进程结束,进程都有自己的时间片!因为时间片的存在,进程会出现没有被执行完就被拿下去的情况,这时候问题来了:这个进程下一次如何在次回到CPU继续运行:
进程切换的时候,需要先进行上下文保护,这里的上下文指的是CPU里的寄存器的数据,而不是寄存器,这里简单理解为临时数据保存至PCB里,而当进程恢复运行的时候,要进行上下文的恢复,该进程在次回到CPU继续运行时,重新加载恢复这些数据。
6. 环境变量的基本概念
环境变量(environment variables) 一般是指在操作系统中用来指定操作系统运行环境的一些参数。
- 如:我们在编写C/C++代码的时候,在链接的时候,从来不知道我们的所链接的动态静态库在哪里,但是照样可以链接成功,生成可执行程序,原因就是有相关环境变量帮助编译器进行查找。
- 环境变量通常具有某些特殊用途,还有在系统当中通常具有全局特性。
当我们平常执行自己的可执行程序时,必须在前面指定路径来执行,但是执行Linux中的指令时,并不需要指定路径,这是什么原因呢?
其实,在Linux下的各种指令和我们平常自己编写代码生成的可执行程序没有什么区别,他也是个可执行程序,但是因为系统中存在PATH环境变量,这些指令的地址都被存放在环境变量中,当我们调用这些指令时,系统会自定去PATH中寻找这些指令。
7. PATH环境变量
PATH是由一堆目录组成的,各目录之间用冒号 “
:
” 隔开。 当执行某个 Linux 命令时,Linux 会依照 PATH 环境变量中包含的目录依次搜寻该命令的可执行文件,一旦找到,即正常执行;反之,则提示无法找到该命令。
💕 查看环境变量:echo $PATH
💕 添加PATH环境变量:
(1) 直接添加
一般不建议使用这种方法添加,因为我们写的程序没有经过测试,容易污染指令池。
(2) 使用export命令添加
在这里我们还需要注意的是,我们不能直接写成这样:export PATH=/home/Chenjiale/lesson2-22,这样会导致把系统默认的环境变量PATH覆盖掉,我们默认的那些指令就不能直接使用了,只能通过指定路径的方式来使用。
💕 系统中的其他环境变量:
- HOSTNAME: 主机名
- USER: 当前用户名
- PWD: 当前系统路径
- HOME: 当前用户的家目录
- HISTSIZE: shell 能记忆的最多历史命令的条数
查看所有的环境变量:env
指令
下面我们来看一下环境变量是在系统中的哪个文件夹下面的:
实际上,当我们在登录 shell 时,操作系统会让我们当前的 shell 进程执行 .bash_profile
中的内容,而 .bash_profile
又会调用执行 .bashrc
,它们会将对应的环境变量导入到 shell 进程的上下文环境中。所以,如果我们上面不小心将 $PATH 覆盖掉了也不用担心,重新登录 shell 就好了。
环境变量是操作系统为了满足不同的应用场景,预先在系统内设置的一大批全局变量,这些变量往往具有特殊功能,且能够一直被 bash 以及 bash 的子进程访问。环境变量具有全局属性的根本原因是环境变量会被子进程继承。
8. 设置和获取环境变量
💕 设置和取消环境变量:
如果我们直接在命令行中定义一个变量,那么这个变量则是本地变量,本地变量只在bash进程中有效。
当然我们可以使用export
直接将本地变量设置为环境变量,同时,也可以直接使用export来定义一个环境变量。
export 已存在的环境变量
: 将本地变量设置为环境变量
export 新的变量
: 直接定义一个环境变量
如果我们不想要这个变量,可以直接使用unset
指令来取消变量,当然我们也可以使用set
指令来查看所有变量。
💕 获取环境变量:
获取环境变量除了可以使用echo $环境变量名
之外,还可以使用一个函数getenv()
来获取。
下面我们举例来演示一下:
在命令行上运行mytest时候,bash就是一个系统进程,mytest也会变成一个进程(通过fork创建父子进程),是bash的子进程。而环境变量具有全局属性的根本原因是会被子进程继承下去,因为环境变量定义给bash,而子进程会全部继承下去,这就被称为环境变量。所以环境变量具有全局性,而本地变量只会在当前进程(bash内)有效。
为了不同的应用场景,让bash替我们寻找指令路径,例如:身份认证;有些子进程需要用到这些信息,确认当前用户的信息。
下面我们来举一个例子:
这里我们一定要使用su -
来验证,我们可以使用getenv函数来获得当前的Linux用户,判断其是否具有某种权限,然后再执行对应的操作。
9. 命令行参数
我们知道在C语言中,main函数也是有参数的,不过我们平常一般都不需要手动传参,而是被系统/父进行行传参的。所以,这个参数可能会被大多数人忽略。
int main(int argc,char* argv[],char* env[])
这里我们可以先来看一下前两个参数,第一个参数是指第二个参数——指针数组中的元素个数。这里我们可以先来看一下指针数组中的每一个元素存的是什么。
其实这里的argv中的每一个元素都指向的是一个字符串,argc用来指定数组中元素的个数,他们配合可以使用-a -b -c 类似的选项功能。
下面我们来看一下最后一个指针数组中的内容:
这里我们看到的是:打印出来的就是环境变量的内容。这是因为env接收的就是父进程传递过来的环境变量的参数。
最后,还有一种获取环境变量内容的方式就是通过环境变量表environ
,这里我们直接来验证一下即可。