【Linux lesson3】进程概念
一、冯诺依曼体系结构
任何一本计算机组织与结构书籍,总是以冯诺依曼体系结构讲起,明明和如今的计算机结构有所差别,那为什么还要讲呢,它有什么重要的地方?
冯诺依曼体系结构的核心思想是将程序指令和数据存储在同一内存中,由中央处理单元(CPU)依次读取指令和数据来执行。由于IO设备读写速度过慢,而中央处理器处理速度过快,但可以存储的数据量过少,导致计算机处理速度直接和IO速度强相关,类似于木桶效应。

因此,冯诺依曼引进了存储器的概念,它的读写速度适中,存储空间适中,是IO设备与中央处理器之间的折中设备。
直至今日,计算机中已经有许多设备都沿用了这种思想,所以计算机中有各种速度和大小各不相同的设备:

二、操作系统
任何计算机系统都包含⼀个基本的程序集合,称为操作系统(OS)。笼统的理解,操作系统包括操作系统内核和外壳:

操作系统就像一个管家,对下与硬件交互,管理所有的软硬件资源,对上为用户程序(应用程序)提供⼀个良好的执行环境。

1.管理硬件
那么,操作系统是如何管理硬件(外设)的呢?答案是先描述,再组织。
首先,将各种形态各异的硬件使用一个struct结构体描述起来,将结构体中添加上各种属性,比如识别码,比如设备类型,比如调用方法。然后再用容器组织起来。那它们的属性从何而来呢?答案是由驱动程序提供,我们第一次使用某个外设时,插上USB接口的一瞬间,总会卡顿一下下,这就是操作系统在安装驱动程序,从驱动程序上获得外设属性和相对应的IO方法,每次要使用这些设备时,操作系统就会调用这些方法。
2.系统调用和库函数
再早期,开发者基本上都是各个大学的科学家,而为了降低开发的成本,为了降低开发者的门槛,操作系统暴露了一部分接口,为了给开发者用来对操作系统进行特定的操作,这些接口,就叫做系统调用。
然而,这些系统调用在使用上,不够简单,对用户的要求也比较高,因此,有心的开发者将部分系统调用进行了封装,将它们打包成为库,有了库,就方便开发者们进行二次开发。
三、进程
1.什么是进程
课本上说,进程是正在执行的程序,内核说,进程是担当系统分配资源的实体。
之前,操作系统为了管理硬件程序,采用先描述,在组织的方法,而在进程这里,操作系统也是如此。
也就是说,将来一定会有一个struct 进程含了进程的属性,同时有一个容器负责管理它们,而我们就是要学习它的属性以及它是怎么被管理的。
因此我们目前认为,进程就是它的内核数据结构+自己的代码和数据。
进程也分为常驻进程和普通进程。普通进程执行完就退出,常驻进程只有用户要退出才退出。
2.PCB
在课本中,描述进程的数据结构被称为进程控制块(PCB)而在Linux下,PCB是一个叫做task_struct的结构体。里面包含了进程的各种属性。
task_struct属性有:
- 标示符:描述本进程的唯一标示符,用来区别其他进程。
- 状态:任务状态,退出代码,退出信号等。
- 优先级:相对于其他进程的优先级。
- 程序计数器:程序中即将被执行的下一条指令的地址。
- 内存指针:包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针
- 上下文数据:进程执行时处理器的寄存器中的数据[休学例子,要加图CPU,寄存器
- 状态信息:包括显示的1/0请求,分配给进程的/O设备和被进程使用的文件列表。
- 记账信息:可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。
- 其他信息
组织进程
可以在内核源代码里找到它。所有运行在系统里的进程都以task_struct双链表的形式存在内核里。
查看进程
可以在Linux中用top等命令查看当前运行的进程。

3.进程标识符与fork
进程标识符是用于标识进程的一个数字,每个进程都有一个唯一标识符,被称为PID。
操作系统是计算机启动的第一个进程,除了它以外,其他的所有进程都是被进程启动的。负责启动自己的进程被称为父进程,自己则被称为子进程。在子进程的task_struct中也会保存父进程的进程id(PPID)。
pid_t fork()是一个系统调用,负责创建进程。当程序执行到fork时,当前进程会创建一个子进程,子进程的fork会返回0,父进程的fork会返回子进程的pid。因此,fork会有两个返回值
子进程创建过程
子进程与父进程的代码是共享的,但是数据是拷贝父进程的,因此fork以后再修改数据不会影响对方。系统为了高效,其实在子进程的创建时,会使用写时拷贝,具体的在后面。
#include <iostream>
#include <sys/types.h>
#include <unistd.h>using namespace std;int main()
{pid_t pid;pid = fork();if (pid < 0){// 创建进程失败cout << "创建进程失败" << endl;return 1;}else if (pid == 0){// 子进程cout << "我是子进程,我的pid是" << getpid() << endl;}else{// 父进程cout << "我是父进程,我的pid是" << getpid() << endl;}
}
4.进程状态
进程状态也是tesk_struct里的一个重要属性,它有关的各种状态以及转换方式在以下图片中。

值得注意的是,不同的操作系统拥有的状态各不相同,上图是教材里的,和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):表明进程在运行在中或者在运行队列里。
S睡眠状态(sleeping):表明进程正在等待某些事件的完成,例如scanf等待用户键盘输入。也可以叫做可中断睡眠。
D磁盘休眠状态(Disk sleep):也可以叫做不可中断睡眠。通常是等待磁盘的IO,是为了防止往磁盘写入一半而被中断导致的文件损坏等类似的问题,保证这些硬件操作的原子性。
T停止状态(stopped):当进程收到SIGSTOP信号时会触发T状态,进程将会被暂停,直到收到SIGCONT信号以后继续执行。
X死亡状态(dead):这个状态是一个返回状态,任务列表里看不到。
Z僵尸状态(zombie):进程退出后没有被回收时的状态,后面具体再讲。
查看进程
可以使用ps axj或ps aux 命令来查看Linux系统中的所有进程。
a:显示一个终端所有的进程,包括其他用户的进程。
x:显示没有控制终端的进程,例如后台运行的守护进程。
j:显示进程归属的进程组ID、会话ID、父进程ID,以及与作业控制相关的信息
u:以用户为中心的格式显示进程信息,提供进程的详细信息,如用户、CPU和内存使用情况等

僵尸进程
僵尸状态(Zombies)是一个比较特殊的状态。当进程退出并且父进程没有读取到子进程退出的返回代码时就会产生僵尸进程。
僵尸进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码。
所以,只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程进入Z状态
#include <iostream>
#include <sys/types.h>
#include <unistd.h>int main() {pid_t pid = fork();if(pid>0){//parentstd::cout<<"I am parent, my pid is "<<getpid()<<std::endl;sleep(10);}else{//childstd::cout<<"I am child, my pid is "<<getpid()<<std::endl;sleep(1);}
}

僵尸状态会导致进程一直不退出,所以系统就要一直维护僵尸进程,维护僵尸进程会消耗内存,因此会发生内存泄漏
那么为了解决僵尸进程引发的内存泄露问题,我们就要在每次子进程退出时父进程及时回收子进程。
孤儿进程
如果父进程退出以后都还没有回收子进程,子进程找不到返回的对象会发生什么呢?
这种情况下,子进程就被称为孤儿进程。孤儿进程会被1号进程(init/systemd)领养,成为1号进程的子进程。将来由1号进程回收。
5.进程cwd
cwd表示进程的当前工作路径,为进程的文件操作提供了路径的起点。一般是可执行文件所在目录,因此,以相对路径的方式打开各种文件默认会在当前文件下查找,因为cwd是当前文件。
6.进程优先级
在进程运行时,往往需要消耗CPU资源,而进程的优先级,就是帮助操作系统分配CPU资源的属性,方便重要的进程占有更多的CPU资源。

我们很容易注意到其中的几个重要信息,有下:
- UID:代表执行者的身份
- PID:代表这个进程的代号
- PPID:代表这个进程是由哪个进程发展衍生而来的,亦即父进程的代号
- PRI:代表这个进程可被执行的优先级,其值越小越早被执行
- NI:代表这个进程的nice值
其中,UID表示执行者的身份,进程有了执行者的身份,同时,文件里也会保存拥有者,所属组,进程在启动时就可以对比拥有者,所属组,权限,和执行者的UID来判断执行者是否有可执行权限。
PRI和NI负责控制进程的优先级。
PRI代表进程的优先级而NI代表进程优先级的修正值,真正的进程优先级=PRI+NI。NI的取值范围是-20-19
修改进程优先级
共有指令方式和代码方式两种:
用top指令修改:
top
进入top后按“r”->输入进程PID->输入nice值
使用以下两个函数修改:
#include <sys/time.h>
#include <sys/resource.h>
int getpriority(int which, int who);
int setpriority(int which, int who, int pria);这些都只能修改nice值,为什么操作系统要这么设计呢?
是为了让进程之间的优先级差距不至于太大,导致不公平。
其他相关概念
- 竞争性:系统进程数目众多,而CPU资源只有少量,甚至1个,所以进程之间是具有竞争属性的。为了高效完成任务,更合理竞争相关资源,便具有了优先级
- 独立性:多进程运行,需要独享各种资源,多进程运行期间互不干扰
- 并行:多个进程在多个CPU下分别,同时进行运行,这称之为并行
- 并发:多个进程在一个CPU下采用进程切换的方式,在一段时间之内,让多个进程都得以推进,称之为并发
7.进程切换
关于上下文数据:指的是进程在运行的某一时刻,CPU中各个寄存器保存的数据,例如:
- pc:下一条指令的地址
- ir:正在执行的命令
- eax,ebx,...:其他数据
CPU可以通过这些数据进行正确的计算和IO。
CPU其实只能一个一个进程的执行,但是为了满足需求,CPU在执行进程时,操作系统会给每个进程执行一段时间,然后再将其从CPU上剥离,然后再执行下一个进程,循环往复。这一段时间被称为时间片,而时间片由于过小我们肉眼无法察觉。这就是分时操作系统,当代计算机都是使用这种方式。
为了保证计算的正确性,每次剥离前,操作系统都会把进程的上下文数据记录在一个叫任务状态段的结构中,每次调度一个进程,都会先把它的上下文数据从内存中拷贝到CPU中。
进程调度
在很多课本中,进程的调度都讲的是FIFO:
将所有R状态的进程用一个队列维护起来,然后遵循先进先出原则,从CPU上剥离的进程到队尾,每次调度队头的进程。
这么做确实很简单但是忽略了一个问题:优先级。这样难以很好地体现优先级的作用。因此操作系统真实的调度算法如下:
在系统中,存在一个拥有140个元素的哈希表,其中,前100个元素,用于放实时进程,今天不考虑,后40个元素,刚好用于放40种优先级的普通进程。如果有优先级相同的进程,就以哈希桶的方式串在一起。这样,操作系统就可以按优先级来调度进程。但是,这还没完。
操作系统中,会存在两个这样的表,一个被称为活跃表,一个是过期表。操作系统只会从活跃表中调度进程。当有新进程启动,或者进程的时间片结束时,操作系统会把他们放到过期表中。因此,活跃表的进程越来越少,过期表越来越多。当活跃表被清空时,操作系统会将它们交换,让活跃表变成过期表,过期表变成活跃表。这样也保证了进程调度的平衡。给了低优先级进程调度的机会。
这被称为进程调度O(1)算法
四、命令行参数和环境变量
1.命令行参数
我们在看别人写的代码时经常有这样的:
int main(int argc, char* argv[])
{for(int i=0;i<argc;i++){std::cout<<argv[i]<<" ";}std::cout<<std::endl;return 0;
}这里的argc和argv就是用于输入命令行参数的

这看起来是不是很熟悉?每错,命令的选项就是通过命令行参数输入的。这就是为了让运行的选项不同,执行的结果也不同。而命令的选项都是由shell先拿到的,然后shell再创建子进程执行命令,子进程由于数据是由父进程拷贝的,所以也可以拿到命令行参数。
那命令为什么没有./呢?这就不得不提到环境变量了。
2.环境变量
环境变量一般是指在操作系统中用来指定操作系统运行环境的一些参数
如:我们在编写C/C++代码的时候,在链接的时候,从来不知道我们的所链接的动态静态库在哪
里,但是照样可以链接成功,生成可执行程序,原因就是有相关环境变量帮助编译器进行查找。
环境变量通常具有某些特殊用途,还有在系统当中通常具有全局特性
常见的环境变量:
PATH:指定命令的搜索路径
HOME:指定用户的主工作目录(即用户登陆到Linux系统中时,默认的目录)
SHELL:当前Shell,它的值通常是/bin/bash
有了这个,就可以让命令不带路径了,因为操作系统会自己去PATH下找
查看环境变量
可以用env命令查看:

也可以通过系统调用getenv查看:
int main()
{std::cout<<"PATH: "<<getenv("PATH") << std::endl;return 0;
}
大部分环境变量都具有全局性,因为其可以被子进程继承下去。
