深入理解linux内核架构 第二章 2.1-2.4

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

命名空间提供了虚拟化的一种轻量级形式,使得我们可以从不同的方面来查看运行系统的全局属性
在虚拟化的系统中,一台物理计算机可以运行多个内核,可能是并行的多个不同的操作系统。而命名空间则只使用一个内核在一台物理计算机上运作,前述的所有全局资源都通过命名空间抽象起来。
命名空间建立了系统的不同视图

在这里插入图片描述
命名空间可以组织为层次
但父容器知道子命名空间的存在,也可以看到其中执行的所有进程
如果命名空间包含的是比较简单的量,也可以是非层次的

创建新命名空间:

  1. 在用fork或clone系统调用创建新进程时,有特定的选项可以控制是与父进程共享命名空间,
    还是建立新的命名空间。
  2. 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指向对应的命名空间。

内核需要

  1. 给出局部数字ID和对应的命名空间,查找此二元组描述的task_struct。(find_task_by_pid_type_ns)
    • 从全局upid hash table里找到对应的upid node,根据命名空间深度计算出这个node在对应pid中的数组索引,算出pid的起始地址,在从pid->task[type]中拿出第一个task实例
  2. 给出task_struct、ID类型、命名空间,取得命名空间局部的数字ID。(pid_nr_ns)
    • task -> pid -> 遍历每个upid,直到命名空间匹配,返回对应的upid数字

内核使用位图分配唯一PID,每个命名空间都有一个位图

2.4 进程相关的系统调用

  1. fork是重量级调用,因为它建立了父进程的一个完整副本,然后作为子进程执行。写时复制(copy-on-write)。
  2. vfork类似于fork,但并不创建父进程数据的副本。用于子进程形成后立即执行execve系统调用加载新程序的情形。因为写时复制技术,vfork速度方面不再有优势,因此应该避免使用它。
  3. 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. 类型1:线程启动后一直等待,直至内核请求线程执行某一特定操作。
  2. 类型2:线程启动后按周期性间隔运行,检测特定资源的使用,在用量超出或低于预置的限制
    值时采取行动。内核使用这类线程用于连续监测任务。
    调用kernel_thread创建一个内核线程(特定于架构,但原型相同,类似pthread_create)

内核线程特性:

  1. 在内核态运行
  2. 它们只可以访问虚拟地址空间的内核部分(高于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内核架构

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值