2.1 Linux 进程概述
在早期的多机处理系统中, 并发执行程序的话(由于并发程序的速度不同与资源竞争导致了程序执行的间断性),存在数据计算结果的不可再现性,这样的程序失去了其意义。
后来呢为了使得程序能并发执行且有效地对并发程序进行控制与描述,引入了进程的概念。
进程是如何产生的?
在程序运行前, 操作系统会为之分配一个PCB(进程控制块:系统通过PCB对进程控制与描述), PCB与程序段和程序相关的数据一起构成了进程实体。
进程的实质就是进程实体的一次运行过程。
进程和程序本身的区别
程序是静态的,是保存在磁盘上的指令的有序集合,进程是一个动态的概念
它是一个运行着的程序,包括了进程的动态创建,调度和消亡的过程,是Linux的基本调度单位。
进程 process:是 os 的最小单元 ,os 会为每个进程分配大小为 4g 的虚拟内存空间,其中 1g 给内核空间, 3g 给用户空间{代码区 数据区 堆栈区}
操作系统和进程间的联系
- 系统通过进程控制块来描述进程的动态变化,进程控制块有进程的基本信息,控制信息和资源信息。
- 操作系统内核通过进程来控制对 CPU 和其他系统资源的访问,并决定进程的CPU 使用和运作时间。
Linux 内核通过一个被称为 进程描述符的 task_struct 结构体来管理进程,这个结构体包含一个进程所需的所有信息。
它定义在 \kernel\msm-4.4\include\linux\sched.h 文件中,
struct task_struct {
进程描述信息
进程标识符
进程的用户标识符
进程控制信息
1.进程状态 2. 优先级
文件和文件系统
内存管理
信号处理
}
2.1.1 进程的标识符
- OS 会为每个进程分配一个唯一的整数 ID, 作为其标识号(pid)。
- 进程描述符结构体也存放了其父进程的 ID(ppid)
- 其次,所有的进程的祖先进程是同一个进程,叫做 Init 进程,ID 号为 1,Init 是内核自举后启动的一个进程,负责引导系统,启动守护进程并且允许必要的程序。
通过 标准 c 的函数可以获取当前进程的 pid 和 ppid
2.1.2 进程的用户 ID 和 组ID (进程的运行身份)
进程的用户ID 和 组ID
- 进程的用户标识了其进程的权限控制, 默认情况下,谁启动了进程,该进程的身份就有该用户的身份
- 使用 getuid() 和 getgid() 能得到进程的真实用户 ID 和真实组 ID。
进程的有效用户ID 和 有效用户组ID
-
内核对进程用户执行的ID 进行检查时,检查的是其有效用户ID 和 有效用户组ID,默认情况下与真实用户ID 和 真实组ID 其是相等的
-
使用 geteuid 和 getegid 能得到进程的有效用户ID 和 有效用户组ID
-改变有效用户ID 使得每个用户对文件的有效ID
chmod u+s myfife:为用户设置s权限,具有文件所有者的权限
chmod g+s Code:为用户组设置s权限,具有用户组的权限,可以对该目录下的文件执行权利,Code为目录
chmod o+t myfife:为其他用户设置t权限,说明其他用户不能对其进行删除操作
设置当前进程的有效用户 ID 和 真实用户ID
让进程获取到更强大的权限。
int setuid(uid_t uid);
int seteuid(uid_t euid);
sudo 权限是改变的用户的真实id
2.1.3 进程的状态
2.1.4 Linux 下的进程结构
Linux 是多进程的系统,
-
进程间有并行性和互不干扰性,进程间是各自分离的任务,每个进程拥有各自的权力和责任。
-
每个进程都运行在各自独立的虚拟地址空间,因此,即使一个进程发生了异常,它也不会影响到系统的其他进程。
2.1.5 进程相关命令
1. ps -elf 查看系统中的进程。ps 命令是一个采样的信息。
2. ps -aux 可以查看进程的CPU和内存的占用率。
3. echo $$打印当前bash的进程ID。
4. top命令 动态的显示进程的信息,
展示的是系统中CPU占用率最高的20个进程。
5. kill命令,给进程发信号,
使用方式:kill -信号的编号 进程ID。通过kill -l可以查看所有信号。
6. nice命令按照指定的优先级运行进程,
renice命令可以修改进程的nice值,nice值的范围:-20-19。
1. nice -n 可执行程序
2. renice -n 指定的nice值 -p 进程ID
2.1.6 进程的调度策略
-
先来先服务调度算法FCFS:队列实现,非抢占,先请求CPU的进程先分配到CPU,可以作为作业调度算法也可以作为进程调度算法;按作业或者进程到达的先后顺序依次调度,对于长作业比较有利;
-
优先级调度算法(可以是抢占的,也可以是非抢占的):优先级越高越先分配到CPU,相同优先级先到先服务,存在的主要问题是:低优先级进程无穷等待CPU,会导致无穷阻塞或饥饿;
-
时间片轮转调度算法(可抢占的):按到达的先后对进程放入队列中,然后给队首进程分配CPU时间片,时间片用完之后计时器发出中断,暂停当前进程并将其放到队列尾部,循环 ;队列中没有进程被分配超过一个时间片的CPU时间,除非它是唯一可运行的进程。如果进程的CPU区间超过了一个时间片,那么该进程就被抢占并放回就绪队列。
-
最短作业优先调度算法SJF:作业调度算法,算法从就绪队列中选择估计时间最短的作业进行处理,直到得出结果或者无法继续执行,平均等待时间最短,但难以知道下一个CPU区间长度;缺点:不利于长作业;未考虑作业的重要性;运行时间是预估的,并不靠谱 ;
-
高相应比算法HRN:响应比=(等待时间+要求服务时间)/要求服务时间;
-
多级队列调度算法:将就绪队列分成多个独立的队列,每个队列都有自己的调度算法,队列之间采用固定优先级抢占调度。其中,一个进程根据自身属性被永久地分配到一个队列中。
-
多级反馈队列调度算法:目前公认较好的调度算法;设置多个就绪队列并为每个队列设置不同的优先级,第一个队列优先级最高,其余依次递减。优先级越高的队列分配的时间片越短,进程到达之后按FCFS放入第一个队列,如果调度执行后没有完成,那么放到第二个队列尾部等待调度,如果第二次调度仍然没有完成,放入第三队列尾部…。只有当前一个队列为空的时候才会去调度下一个队列的进程。与多级队列调度算法相比,其允许进程在队列之间移动:若进程使用过多CPU时间,那么它会被转移到更低的优先级队列;在较低优先级队列等待时间过长的进程会被转移到更高优先级队列,以防止饥饿发生。
2.1.7 会话,进程组,前台进程和后台进程
1. 会话,控制终端,会话中的首进程是bash进程,一个会话下面可以有多个进程组。包括一个前台进程组和若干个后台进程组。
2. 前台进程组,可以接受控制终端上传输的数据。
3. 后台运行一个进程,在执行程序时后面加一个&符号,变成了后台进程。,
后台进程在会话结束后会自动结束。
4. 通过jobs命令可以看到当前会话下面的后台作业,每个作业都有一个编号。可以通过fg+作业编号把后台运行的作业拉回到前台。拉回到前台之后,就可以通过控制终端跟前台进程交互。
2.2 进程的创建
2.2.1 fork 函数
#include <unistd.h>
pid_t fork(void);
在Linux 中 fork 函数实现了创建一个新进程的效果:
- 新进程是子进程, 原进程是父进程。
- 执行一次fork返回两个值,因为 fork 实现了进程的分叉复制,如下图:
原进程调用 fork 后,会从该处复制出一个新进程,新进程与原进程的执行代码是一样的,但fork的返回值有所不同。 - 父进程的返回值是子进程的 pid, 子进程的返回值是 0, 出错则返回 -1
fork 函数创建子进程的过程为:
- 使用 fork 函数得到的子进程是父进程的一个复制品,它从父进程继
承了进程的地址空间,包括进程上下文、进程堆栈、内存信息、打开的文件描述符、信号控制设定、进程优先级、进程组号、当前工作目录、根目录、资源限制、控制终端, - 子进程所独有的只有它的进程号、资源使用和计时器等。
- 通过这种复制方式创建出子进程后,原有进程和子进程都从函数 fork 返回,各自继续往下运行,但是原进程的 fork 返回值与子进程的 fork 返回值不同,在原进程中,fork 返回子进程的 pid,而在子进程中,fork 返回 0,如果 fork 返回负值,表示创建子进程失败。(vfork 函数)
写时复制的技术(Cow)
父子进程的堆栈内存变量在双方都不进行修改的情况下是指向同一虚拟地址的,也可以说目前变量归父子进程共享, 仅在修改时才会产生新的地址。
- fork 对文件操作采用的是 dup 机制,两个文件描述符指向同一个文件对象
2.2.2 exec 函数族
int execl(const char *path, const char *arg, ...)
使用 add.exe 直接覆盖掉当前进程
execl(“./add.exe” ,”add.exe” ,”3” , “4” , NULL):
exec 的工作原理与 fork 完全不同, fork 是在复制一份原进程,而 exec 函数是用 exec 的第一个参数指定的程序覆盖现有进程空间(也就是说执行 exec 族函数之后,它后面的所有代码不在执行)。
- path 是包括执行文件名的全路径名
- arg 是可执行文件的命令行参数,多个用,分割注意最后一个参数必须为 NULL。
我们输入的 bash命令可以这样理解:
bash父进程 -> fork 分割出子进程 ,
execl 将子进程转到命令为(例如ls)的进程
2.2.3 system 函数
#include <stdlib.h>
int system(const char *string);
-
system 函数通过调用 shell 程序/bin/sh –c 来执行 string 所指定的命令,该函数在内部是通过调用execve(“/bin/sh”,…)函数来实现的。
-
通过 system 创建子进程后,原进程和子进程各自运行,相互间关联较少。如果 system 调用成功,将返回 0。
-
system函数的参数还可以是一个可执行程序,例如:
system(“/home/wangxiao/1”);如果想要执行system后面进程的时候,不至于对当前进程进行阻塞,可以利用&将/home/wangxiao/1调到后台运行。
2.2.4 进程的控制和终止
进程的终止:
进程的终止有 5 种方式:
⚫ main 函数的自然返回;
⚫ 调用 exit 函数
⚫ 调用_exit 函数
⚫ 调用 abort 函数
⚫ 接收到能导致进程终止的信号 ctrl+c SIGINT ctrl+\ SIGQUIT
exit() 与 _exit() 的区别:
进程的控制
孤儿进程:
用 fork 函数启动一个子进程时,子进程就有了它自己的生命并将独立运行。
如果父进程先于子进程退出,则子进程成为孤儿进程,此时将自动被 PID 为 1 的进程
(即 init)接管。孤儿进程退出后,它的清理工作有祖先进程 init 自动处理。但在 init 进程,清理子进程之前,它一直消耗系统的资源,所以要尽量避免。
僵尸进程:
如果子进程先退出,系统不会自动清理掉子进程的环境,而必须由父进程调用 wait 或 waitpid 函数来完成清理工作,如果父进程不做清理工作,则已经退出的子进程将成为僵尸进程(defunct),在系统中如果存在的僵尸(zombie)进程过多