2.1 进程优先级
- 硬实时进程
- 软实时进程
- 普通进程
抢占式多任务处理(preemptive multitasking):被抢占进程的运行时环境,即所有CPU寄存器的内容和页表,都会保存起来,因此其执行结果不会丢失。在该进程恢复执行时,其进程环境可以完全恢复。
linux调度器:
- O(1)调度器,2.5开发的调度器,已不用
- 完全公平调度器(CFS)2.6.23开始,可以首先在不同用户之间分配(或命名空间等),接下来在各个进程之间分配
2.2 进程生命周期
- 运行
- 等待
- 睡眠
僵尸进程:说这些进程死了,是因为其资源(内存、与外设的连接,等等)已
经释放,因此它们无法也决不会再次运行。说它们仍然活着,是因为进程表中仍然有对应的表项。
进程必须由父进程在子进程终止后调用wait4(wait for)才会终结。
- 用户态
- 核心态
进程通常都处于用户状态,只能访问自身的数据,无法干扰系统中的其他应用程序,甚至也不会
注意到自身之外其他程序的存在。如果进程想要访问系统数据或功能(后者管理着所有进程之间共享的资源,例如文件系统空间), 则必须切换到核心态。
进入核心态的方法: - 中断
- 系统调用
内核的抢占调度模型建立了一个层次结构
- 普通进程总是可能被抢占
- 但中断可以中止系统调用
- 中断具有最高优先级
2.5 开始linux具有内核抢占:在紧急情况下切换到另一个进程,甚至当前是处于核心态执行系统调用(中断处理期间是不行的)。
2.3 进程表示
Linux内核涉及进程和程序的所有算法都围绕一个名为task_struct的数据结构建立
- 状态
- 虚拟内存
- 进程身份凭据
- 文件系统信息
- cpu运行时间数据
- 进程间通信信息
- 信号
- 资源限制
产生新进程的方法:fork,clone
加载程序:exec
命名空间提供了虚拟化的一种轻量级形式,使得我们可以从不同的方面来查看运行系统的全局属性
在虚拟化的系统中,一台物理计算机可以运行多个内核,可能是并行的多个不同的操作系统。而命名空间则只使用一个内核在一台物理计算机上运作,前述的所有全局资源都通过命名空间抽象起来。
命名空间建立了系统的不同视图
命名空间可以组织为层次
但父容器知道子命名空间的存在,也可以看到其中执行的所有进程
如果命名空间包含的是比较简单的量,也可以是非层次的
创建新命名空间:
- 在用fork或clone系统调用创建新进程时,有特定的选项可以控制是与父进程共享命名空间,
还是建立新的命名空间。 - unshare系统调用将进程的某些部分从父进程分离,其中也包括命名空间
struct nsproxy用于汇集指向特定于子系统的命名空间包装器的指针
struct nsproxy {
atomic_t count;
struct uts_namespace *uts_ns;
struct ipc_namespace *ipc_ns;
struct mnt_namespace *mnt_ns;
struct pid_namespace *pid_ns;
struct user_namespace *user_ns;
struct net *net_ns;
};
对命名空间中的每个用户,都有一个struct user_struct的实例负责记录其资源消耗,各个实例可通过散列表uidhash_table访问
2.3.3 进程ID
几种ID
- 进程ID(PID)
- 线程组ID (TGID)
- 进程组ID (PGID)
- 会话ID (SID)
因为命名空间,某些进程具有多个PID,凡可以看到该进程的命名空间,都会为其分配一个PID。
全局ID是在内核本身和初始命名空间中的唯一ID号
局部ID属于某个特定的命名空间,不具备全局有效性。
全局PID和TGID直接保存在task_struct中
这个系统由struct pid, struct upid, struct pid_namespace控制。关系如下
task_struct有多个pid *,分别对应不同的ID类型(PID,PGID,SID),每个pid对象针对自己可能的三种类型,又有三个链表,每个链表中的元素分别对应一个指向自己的task_struck
这里建立了双向连接:task_struct可以通过task_struct->pids[type]->pid访问pid实例。
而从pid实例开始,可以遍历tasks[type]散列表找到task_struct。
对于struct upid,nr表示ID的数值,ns是指向该ID所属的命名空间的指针。所有的upid实例
都保存在一个散列表中
pid中保存了一个数组,里面是他所有的upid,每个命名空间一个。每个upid指向对应的命名空间。
内核需要
- 给出局部数字ID和对应的命名空间,查找此二元组描述的task_struct。(find_task_by_pid_type_ns)
- 从全局upid hash table里找到对应的upid node,根据命名空间深度计算出这个node在对应pid中的数组索引,算出pid的起始地址,在从pid->task[type]中拿出第一个task实例
- 给出task_struct、ID类型、命名空间,取得命名空间局部的数字ID。(pid_nr_ns)
- task -> pid -> 遍历每个upid,直到命名空间匹配,返回对应的upid数字
内核使用位图分配唯一PID,每个命名空间都有一个位图
2.4 进程相关的系统调用
- fork是重量级调用,因为它建立了父进程的一个完整副本,然后作为子进程执行。写时复制(copy-on-write)。
- vfork类似于fork,但并不创建父进程数据的副本。用于子进程形成后立即执行execve系统调用加载新程序的情形。因为写时复制技术,vfork速度方面不再有优势,因此应该避免使用它。
- clone产生线程,可以对父子进程之间的共享、复制进行精确控制
写时复制
进程通常只使用了其内存页的一小部分
fork时,并不复制进程的整个地址空间,而是只复制其页表
只要一个进程试图向复制的内存页写入,处理器会向内核报告访问错误(此类错误被称作缺页异
常)。此时内核会创建该页专用于当前进程的副本,当然也可以用于写操作,再返回当前进程继续执行。
执行的系统调用
入口:sys_fork、sys_vfork和sys_clone
都进入与体系结构无关的do_fork函数,复制进程复制
copy_process负责复制进程的实际工作
up_task_struct: 复制task_struct结构本身,准备内核栈,以及栈中的task_info(thread_info保存了特定于体系结构的汇编语言代码需要访问的那部分进程数据。)
struct thread_info {
struct task_struct *task; /* 当前进程task_struct指针 */
struct exec_domain *exec_domain; /* 执行区间 */
unsigned long flags; /* 底层标志 */
unsigned long status; /* 线程同步标志 */
__u32 cpu; /* 当前CPU */
int preempt_count; /* 0 => 可抢占, <0 => BUG */
mm_segment_t addr_limit; /* 线程地址空间 */
struct restart_block restart_block;
}
flags包括以下两个重要标志位:
- TIF_SIGPINDING (有待决信号,内核在返回用户态之前检测到这个标志会调用信号处理函数)
- TIF_NEED_RESCHED (调度器通知核心调度器当前task需要被替换,由核心调度器在中断等返回前检查,清零,并实际执行调度)
有关copy_xyz
如果CLONE_xyz置位,则两个进程共享res_xyz
- 如果COPY_MM置位,则copy_mm让父进程和子进程共享同一地址空间。在这种情况下,两个进程使用同一个mm_struct实例 (线程实现)
- 如果COPY_MM没有置位,并不意味着需要复制父进程的整个地址空间。内核确实会创建页表的一份副本,但并不复制页的实际内容。(fork实现)
- copy_namespaces有特别的调用语义。它用于建立子进程的命名空间。(置位创建新空间)
- copy_thread与体系结构相关
2.4.2 内核线程
内核线程实际上是将内核函数委托给独立的进程,与系统中其他进程“并行”执行(实际上,也并行于内核自身的执行)。内核线程经常称之为(内核)守护进程。
两种类型:
- 类型1:线程启动后一直等待,直至内核请求线程执行某一特定操作。
- 类型2:线程启动后按周期性间隔运行,检测特定资源的使用,在用量超出或低于预置的限制
值时采取行动。内核使用这类线程用于连续监测任务。
调用kernel_thread创建一个内核线程(特定于架构,但原型相同,类似pthread_create)
内核线程特性:
- 在内核态运行
- 它们只可以访问虚拟地址空间的内核部分(高于TASK_SIZE的所有地址),但不能访问用户
空间。
task_struct里有struct mm_struct *mm, *active_mm 两个指针
mm指向当前task拥有的页表,active_mm是当前task正在使用的页表
当内核在进程上下文下运转时,mm和active_mm的值相同
对于内核线程,mm是NULL,因为内核线程不拥有页表。内核线程总是借别的进程的页表运行,但只访问内核空间的部分(这部分对每个进程都一样)
没有mm的进程又称惰性TLB进程,因为切换时内核不需要置换虚拟地址空间用户层部分的TLB
2.4.3 启动新程序
execve --> sys_execve --> do_execve
打开文件(找到inode,生成一个文件描述符,用于寻址该文件)
bprm_init接下来处理若干管理性任务
- mm_alloc生成一个新的页表
- init_new_context特定于体系结构,用于初始化mm_struct
- 而__bprm_mm_init用于建立初始的栈
然后新进程的所有参数(euid, egid, argv, env, filename … ) 被合并成linux_binprm结构
prepare_binprm为何了对有效UID和GID的处理(即SUID和SGID位)
if (mode & S_ISUID)
bprm->e_uid = inode->i_uid;
if ((mode & (S_ISGID | S_IXGRP)) == (S_ISGID | S_IXGRP))
bprm->e_gid = inode->i_gid;
Linux支持可执行文件的各种不同组织格式。标准格式是ELF(Executable and Linkable Format)
search_binary_handler用于在do_execve结束时查找一种适当的二进制格式,用于所要执行的特定文件
二进制格式处理程序执行下列操作:
- 释放原进程使用的所有资源
- 将应用程序映射到虚拟地址空间中 (text, heap, stack, env, arg …)
参考
【1】深入linux内核架构