进程基本概念
- 进程调度
- 2023-03-12
- 96热度
- 0评论
进程标识
进程是程序加载到内存的执行过程。进程与程序相比用于操作系统的资源如内存空间、文件、signal等。对于进程的标识我们使用process id来标识(PID)。
线程是进程中活跃状态的实体,也是操作系统实际调度的基本单元。进程中的所有线程是共享一些资源的。在linux中,实际上不区分进程和线程,进程和线程都是task_struct结构体来描述。在linux中使用thread ID(TID)来标识进程中的线程,thread id在所属进程中是唯一的,在linux系统中也是全局唯一的。对于单线程的进程来说,process ID和thread ID是一样的;而对于多线程来说,每个线程有自己的thread ID,但是所有线程共享一个PID。
进程组是一组进程的集合,创建进程组主要是将一些拥有共同特性的进程组合起来便于管理,如可以发一个信号给一个进程组,则这个组内的进程都会收到该信号。任何一个进程都不是独立存在的,一定是属于某个进程组,当fork的时候进程就归属到创建这所属的进程组。进程组用process group ID(PGID)来标识,进程组内的所有进程都有相同的GPID,等于该组组长的PID。可以通过setpgid、getpgid、setpgrp和getpgrp等接口函数访问PGID。
会话是一个用户登录后会创建一个会话,这个会话用sesssion ID(SID)来进行标识。登录的第一个进程较会话领头进程,通常是shell/bash。领头进程PID=SID。用户登录系统后,不断提交任务给操作系统,最后退出登录,就会销毁该session。可以通过getsid,setid来操作SID。
命名空间是用来隔离内核资源的,当一个进程运行在linux系统上的时候,它就拥有了很多系统资源如PID,网络设备,文件系统等。Linux内核通过namespace可以让一些进程只能看到与自己相关的一部分资源,而另外一些进程也只能看到与他们自己相关的资源,让互不联系的进程感觉不到对方的存在。目前linux现有的namespace有如下7种:
namespace | 隔离内容 |
---|---|
Cgroup | Cgoup root directory |
IPC | System V IPC,POSIX消息队列 |
Network | 网络设备、栈、端口等 |
Mount | 挂载点 |
PID | 进程ID |
User | 用户和组ID |
UTS | 主机名和NIS域名 |
与namespace相关的函数只有三个clone,setns,unshare。分别是创建一个进程放到对应namespace,将当前进程加入到已知namespace,退出指定类型namespace并加如到创建的namespace。
PID命名空间对进程PID重新标号,即不同的namespace下进程可以有同一个PID,如下容器1的a和容器2的a。他们分别对应在内核空间是PID namespace 1和PID namespace2。内核种为所有的PID namespace维护了一个树状结构,最顶层的是系统初始化创建的,被称为root namespace,由他创建的新的PID namespace成为它的chid namespace。父节点是可以看到字节点种的进程的,可以通过信号对子节点的进程产生影响,但是子节点无法看到父节点的进程。PID namespace对容器应用特别重要,可以实现容器内进程的暂停/恢复等功能,还可以支持容器在跨主机的迁移前后保持内部进程的PID不发生变化。
进程描述
Linux系统要对进程进行操作,需要抽象出所拥有的资源,我们称为进程控制块(Process Control Block,PCB),也称为进程描述符号,Linux中使用struct task_struct结构体来进行描述。task_struct数据结构包含的内容可以归类为几类:
- 进程属性相关。
- 进程间的关系。
- 进程调度相关信息。
- 内存管理相关信息。
- 文件管理相关信息。
- 信号相关信息。
- 资源限制相关信息。
上图中列出了task_struct数据结构中包含的一些内容。在linux中,进程和线程都是使用task_struct来进行描述。
进程状态
- 就绪态:进程获得了可以运行的所有资源和准备条件
- 运行态:CPU正在运行该进程。(linux中就绪态和运行态都是TASK_RUNNING)
- 浅度睡眠:进程需要某些资源不满足而进入等待,当条件满足时转为就绪队列。
- 深度睡眠:与浅度睡眠不同的时,进程睡眠等待不受干扰,不响应信号,如SIGKILL信号无法终止。
- 暂停:进程运行停止
- 僵死:进程已经消亡,但是task_struct数据结构还没有释放,父进程通过wait来获取子进程消亡原因。僵死进程已经放弃了几乎所有的内存空间,不会再执行代码,也不能被调度。之所以产生僵死进程时因为其父进程没有调用wait函数来等待子进程结束(父进程没来收尸,就变僵尸了),如果父进程异常退出了,那么init进程会自动接手这个子进程,也就是说父进程还活着,但是并没有调用wait来清除子进程。
进程间的关系
linux内核启动时,会创建一个init_task进程,这是系统中所有进程的祖先,称为进程0或idle进程,当系统没有进程调度时,调度器就会运行idle进程。在smp中,每个cpu都有一个进程0。在执行kernel_init函数后,会启动进程1(用户第一个进程),进程1是所有用户进程的祖先,可以通过pstree来查看进程关系。
上图中,procd等同于init,有用openwrt的1号进程使用的是procd,当然也可以改成init。
Linux系统中task_struct数据结构使用4个成员来描述进程间的关系,如下:
- real_parent: 指向创建当前进程的进程描述描述符,如果创建的进程不存在,则指向init进程。
- parent:指向进程当前的父进程,通常和real_parent一致。
- children:指向其子进程,所有的子进程被链接到这个链表上。
- sibling:指向兄弟进程,所有的兄弟进程链接成一个链表。
获取当前进程
系统运行时,调度操作的数据结构就是task_struct,因此在系统调度时要运行进程,必现要找到对应进程的task_struct结构体。Linux内核提供了current宏来方便快速找到当前要运行或正在运行的task_struct数据结构。
current的实现和具体的架构有关,通常有两类实现方式。在ARM32系统中,存放了一个thread_info的数据结构在内核栈里面,current宏通过arm32的SP寄存器来获取当前内核栈的地址,对齐后即可获取到thread_info数据结构的指针,最后通过thread_info->task成员获取task_struct数据结构。
在linux 5.0内核中,新增了一个配置选项CONFIG_THREAD_INFO_IN_TASK,将thread_info存放在task_struct数据结构中。在ARM64处理上,利用SP_EL0寄存器粗放囊当前进程的task_struct数据结构的地址。