【深入Linux内核架构笔记】第二章 进程管理

314 阅读14分钟

2.1 进程优先级

  • 进程根据重要性,划分为实时进程非实时进程
    • 硬实时进程:某些任务必须在指定时限内完成(飞机飞行控制命令)
    • 软实时进程:仍然需要快速得到结果,但是稍微晚一点没事(CD的写入操作)
    • 普通进程:没有时间约束,可以根据重要性来分配优先级(编译或计算)
  • 抢占式多任务处理(preemptive multitasking):进程的运行按时间片调度。时间片到期后,内核会让一个不同的进程运行。被抢占的进程保存运行时环境(所有CPU寄存器和页表),进程恢复执行时,进程环境可以恢复。所有进程都有机会运行,但重要的进程会得到更多的CPU时间
    • 问题:某些进程无法立即执行,不应当被调度;需要考虑支持不同的调度类别(完全公平调度、实时调度)

2.2 进程生命周期

  • 进程状态

    • 运行:进程此刻正在执行
    • 等待:进程能够运行,但由于此时CPU分配给另一个进程未执行。调度器任务切换时可以选择该进程执行
    • 睡眠:进程在等待一个外部事件无法执行。调度器在任务切换时无法选择该进程(无法立即执行的进程)
    • 僵尸:进程已经死亡,资源已经释放,但是父进程未调用wait4系统调用从进程表删除子进程,确认子进程的终结(注:僵尸进程数据在内核中占据空间极少,几乎不是问题)
  • 进程状态机切换

    • 路径4:进程已就绪,调度器给进程授予CPU时间,状态变成运行
    • 路径2:调度器回收进程CPU资源,状态变为等待
    • 路径1:进程必须等待事件,状态变为睡眠
    • 路径3:进程等待事件发生,状态先变为等待,再由调度器决定变为运行
    • 路径5:程序执行终止,状态变为终止

image.png

抢占式多任务处理

  • 操作系统分用户态内核态,通常处于用户态。用户态下进程只能访问自身数据,无法访问系统数据或功能;内核态下进程才能访问系统数据。可以通过系统调用(用户进程主动触发)、中断(外部随机触发)切换系统状态
  • 内核抢占:高优先级抢占低优先级任务
    • 普通进程:系统处于用户态,进程总是可能被抢占
    • 系统调用:系统处于内核态,其它进程无法夺取CPU时间
    • 中断:具有最高优先级,可以中止系统调用

2.3 进程表示

Linux内核进程存储在task_struct结构体中,包括:

  • 进程状态、进程ID、父进程指针、子进程指针、优先级
  • 虚拟内存信息
  • 进程身份凭据(用户ID、组ID、权限)
  • 程序代码的二进制文件、文件系统信息、打开文件信息
  • 程序运行的时间信息
  • IPC进程间通信相关信息
  • 进程所用的信号处理程序

重要成员

  • state:进程状态

    • TASK_RUNNING(R状态):进程处于就绪队列中(正在运行或在等待CPU调度运行)
    • TASK_INTERRUPTIBLE:(S状态):进程处于等待队列中,等待某事件发生(键盘输入、socket连接、信号等)。可以被外部信号唤醒和内核唤醒,唤醒后状态变为TASK_RUNNING
    • TASK_UNINTERRUPTIBLE:(D状态):进程处于等待队列中,但不能由外部信号唤醒,只能由内核亲自唤醒
    • TASK_STOPPED:(T状态):进程停止运行,如收到SIGSTOP、SIGTSTP、SIGTTIN、SIGTTOU信号时进入暂停状态。收到SIGCONT信号变为TASK_RUNNING
    • TASK_TRACED:不能算进程状态,区分当前被调试的进程
    • EXIT_ZOMBIE:(Z状态):僵尸进程,子进程已经停止运行,但是没有被父进程wait4回收子进程PCB
    • EXIT_DEAD:wait系统调用已经发出,进程完全从同移除前的状态
  • rlim:进程的资源限制数组,对进程使用系统资源施加某些限制

    • 进程资源:打开文件数目、每个UID用户最大拥有进程数、最大CPU时间(毫秒)、最大文件长度、数据段最大长度等等,每个进程包含一个限制文件/proc/self/limits
    • getrlimits:检查当前限制
    • setrlimit:增减当前限制,但不能超出rlim_max
struct rlimit {
	unsigned long	rlim_cur;    // 进程当前的资源限制(软限制)
	unsigned long	rlim_max;    // 该限制最大的容许值(硬限制)
};
# cat /proc/self/limits
Limit                     Soft Limit           Hard Limit           Units
Max cpu time              unlimited            unlimited            seconds
Max file size             unlimited            unlimited            bytes
Max data size             unlimited            unlimited            bytes
Max stack size            8388608              unlimited            bytes
Max core file size        0                    unlimited            bytes
Max resident set          unlimited            unlimited            bytes
Max processes             1024                 1024                 processes
Max open files            1024                 1024                 files
Max locked memory         unlimited            unlimited            bytes
Max address space         unlimited            unlimited            bytes
Max file locks            unlimited            unlimited            locks
Max pending signals       unlimited            unlimited            signals
Max msgqueue size         unlimited            unlimited            bytes
Max nice priority         0                    0
Max realtime priority     0                    0
Max realtime timeout      unlimited            unlimited            us

2.3.1 进程类型

  • 新进程使用forkexec系统调用产生
    • fork:生成当前进程的一个相同副本作为子进程,父进程和子进程共享同一组打开文件、工作目录、内存数据(Linux使用写时复制,此时没有复制所有内存页)
    • exec:加载新程序,用可执行二进制文件替代当前运行进程

2.3.2 命名空间

  • 命名空间:一种轻量级虚拟化的解决方案,使得一台物理机可以运行多个内核。一组进程放置到容器中,各个容器彼此隔离(docker技术的基础)
    • 本质上建立了系统的不同视图,(资源, 命名空间) 二元组是全局唯一的
    • 可按照层次关系关联,父命名空间可以看到子命名空间所有资源

image.png

  • 命名空间实现
    • 每个进程关联到自身的命名空间视图,多个进程可以共享一组子命名空间(图2-4)
      • 主要通过chroot系统调用支持
      • 通过task_struct中的nsproxy指针,把不同的进程汇聚到同一个子命名空间中
struct nsproxy {
	atomic_t count;
	struct uts_namespace *uts_ns;    //运行内核的名称、版本、体系结构等信息(uname)
	struct ipc_namespace *ipc_ns;    //进程间通信有关信息
	struct mnt_namespace *mnt_ns;    //mount的文件系统视图
	struct pid_namespace *pid_ns;    //进程PID的信息
	struct user_namespace *user_ns;  //限制每个用户资源使用的信息
	struct net 	     *net_ns;    //网络相关命名空间参数
};
struct uts_namespace {
	struct kref kref;                //引用计数器,多少地方用到命名空间
	struct new_utsname name;         //运行内核的名称、版本、体系结构等信息
};
struct user_namespace {
	struct kref		kref;    //引用计数器,多少地方用到命名空间
	struct hlist_head	uidhash_table[UIDHASH_SZ];  //命名空间内各个用户UID实例
	struct user_struct	*root_user;  //命名空间用户的资源消耗(进程、打开文件数目)
};

image.png

2.3.3 进程ID号

  • PID:进程ID,每个进程的唯一标识,内核通过forkvforkclone自动分配
  • TGID:线程组ID,同一个进程的线程组ID一样(以标志CLONE_THREAD调用clone系统调用建立)
  • PGID:进程可以组成进程组setpgrp系统调用),进程组可以简化向所有组内进程发送信号的操作。进程组内的所有pgrp属性值相同,等于该组组长的PID
  • SID:几个进程组可以合并成一个会话setsid系统调用),可以用于终端程序设计。会话组中所有进程都有相同的SID

由于引入了命名空间,ID的组织也是按层次组织的:父命名空间可见所有子命名空间的PID,反之不可见。由此引入全局id和局部id:

  • 全局id:整个系统唯一的ID号(针对内核本身和初始命名空间)
    • PID:task_struct->pid
    • TGID:task_struct->tgid
    • PGID:task_struct->signal->__pgrp
    • SID:task_struct->signal->__session
  • 局部id:子命名空间唯一的ID号,不同子命名空间的ID值可能相同

image.png

【说明】

  • 内核对pid实现了两个数据结构:struct pidstruct upid
    • struct pid:内核对PID的内部表示,包含:引用计数器(count)、使用该PID的进程列表(散列表),命名空间层次深度(level)、命名空间实例数组(numbers)
    • struct upid:PID与子命名空间的关系,包括进程ID的数值(nr)、命名空间指针(ns)、散列表(pid_chain)
enum pid_type
{
	PIDTYPE_PID,
	PIDTYPE_PGID,
	PIDTYPE_SID,
	PIDTYPE_MAX    //这里没有TGID,因为TGID==PID
};
struct upid {
	/* Try to keep pid_chain in the same cacheline as nr for find_pid */
	int nr;
	struct pid_namespace *ns;
	struct hlist_node pid_chain;
};
struct pid
{
	atomic_t count;
	/* lists of tasks that use this pid */
	struct hlist_head tasks[PIDTYPE_MAX];
	struct rcu_head rcu;
	int level;
	struct upid numbers[1];
};
  • 关联task_structpid的关系:
    • 使用attach_pid函数建立了task_structpid的双向关联
    • task_struct访问pid:在task_struct定义了一个struct pid_link pids[PIDTYPE_MAX]数组,通过task_struct->pids[type]->pid访问pid实例
    • pid访问task_struct:定义了一个pid_hash散列表,通过遍历pid->tasks[type]散列表找到task_struct
struct task_struct {
	/* PID/PID hash table linkage. */
	struct pid_link pids[PIDTYPE_MAX];
};
struct pid_link
{
	struct hlist_node node;
	struct pid *pid;
};

<kernel/pid.c>
static struct hlist_head *pid_hash;
  • 内核生成唯一PID步骤
    • 内核使用大位图分配一个空闲的PID,置1;释放则置0
    • 对于多个命名空间,每个父命名空间都要逐级生成一个局部PID,还要绑定父子命名空间的关系
    • 每个UPID实例都必须置于PID散列表中

2.3.4 进程关系

进程家族关系

  • 父子关系(children):保存所有的子进程
  • 兄弟关系(slibling):用于将兄弟进程彼此连接起来
struct task_struct{
    struct list_head children;
    struct list_head slibling;
};

2.4 进程管理相关的系统调用

2.4.1 进程复制

系统调用

  • fork:创建父进程完整副本,使用写时复制技术减少复制工作量
  • vfork:类似于fork,但不创建父进程数据的副本(因为fork实现了写时复制,速度不再具有优势,应避免使用)
  • clone:产生线程,可以对父子进程的复制精确控制

写时复制

  • 问题背景:父进程一般只是用内存页的小部分。传统fork需要对父进程的所有内存页都复制创建一个新副本,会大量使用内存并花费很长时间。且通常fork完会使用exec替代程序,因此复制所有内存页是没必要的
  • 写时复制:不复制整个地址空间,而是一开始父子进程同时指向相同的内存页,且内存页标记为只读。仅当修改内存页的时候,触发缺页异常复制内存页写入。

do_fork:核心函数,创建成功返回进程PID,失败返回错误码(负值)

long do_fork(unsigned long clone_flags,    //标志集合,SIGCHLD表示fork后发送SIGCHLD信号给父进程
	     unsigned long stack_start,    //用户状态下栈的起始地址
	     struct pt_regs *regs,         //指向寄存器集合的指针
	     unsigned long stack_size,     //用户状态下栈的大小,通常为0
	     int __user *parent_tidptr,    //指向父进程PID
	     int __user *child_tidptr)     //指向子进程PID
{
......
        // 复制进程,返回新进程PID
	p = copy_process(clone_flags, stack_start, regs, stack_size,
			child_tidptr, NULL);
......
        // 若创建了新的PID命名空间,从父命名空间中获取新进程的PID
        // PID命名空间没有改变,即新旧进程都在同一个命名空间中,则获取局部PID
        nr = (clone_flags & CLONE_NEWPID) ?
                task_pid_nr_ns(p, current->nsproxy->pid_ns) :
                        task_pid_vnr(p);
......
        // 初始化vfork的完成处理函数
        if (clone_flags & CLONE_VFORK) {
                p->vfork_done = &vfork;
                init_completion(&vfork);
        }
        // 如果使用PTRACE监控新进程,则创建新进程后会立即发送SIGSTOP信号
        if ((p->ptrace & PT_PTRACED) || (clone_flags & CLONE_STOPPED)) {
                /*
                 * We'll start up with an immediate SIGSTOP.
                 */
                sigaddset(&p->pending.signal, SIGSTOP);
                set_tsk_thread_flag(p, TIF_SIGPENDING);
        }
        // 将子进程加入调度器队列,由调度器选择它运行
        if (!(clone_flags & CLONE_STOPPED))
                wake_up_new_task(p, clone_flags);
        else
                p->state = TASK_STOPPED;
......
        // 如果使用vfork机制,启用子进程的完成机制,唤醒所有因task_struct->vfork_done变量睡眠的进程
        if (clone_flags & CLONE_VFORK) {
                freezer_do_not_count();
                wait_for_completion(&vfork);
                freezer_count();
......
	return nr;
}

复制进程do_fork的主要工作,主要是根据clone_flags生成task_struct

static struct task_struct *copy_process(unsigned long clone_flags,
					unsigned long stack_start,
					struct pt_regs *regs,
					unsigned long stack_size,
					int __user *child_tidptr,
					struct pid *pid)
{
	int retval;
	struct task_struct *p;
	int cgroup_callbacks_done = 0;

        // 检查clone_flags是否合法,下略
	if ((clone_flags & (CLONE_NEWNS|CLONE_FS)) == (CLONE_NEWNS|CLONE_FS))
		return ERR_PTR(-EINVAL);
......
        // 建立父进程task_struct的副本,复制线程信息,为子进程分配了一个新的核心态栈task_struct->stack
        // current表示当前进程task_struct的地址
	p = dup_task_struct(current);
	if (!p)
		goto fork_out;

......
        // 检查新进程创建后是否超出允许的最大进程数目
	if (atomic_read(&p->user->processes) >=
			p->signal->rlim[RLIMIT_NPROC].rlim_cur) {
		if (!capable(CAP_SYS_ADMIN) && !capable(CAP_SYS_RESOURCE) &&
		    p->user != current->nsproxy->user_ns->root_user)
			goto bad_fork_free;
	}
......
        // 初始化task_struct,下略。上面还有一系列的异常检测
	p->did_exec = 0;
	delayacct_tsk_init(p);	/* Must remain after dup_task_struct() */
	copy_flags(clone_flags, p);
	INIT_LIST_HEAD(&p->children);
	INIT_LIST_HEAD(&p->sibling);
	p->vfork_done = NULL;
	spin_lock_init(&p->alloc_lock);
......
        // 调度器设置,子进程状态设为TASK_RUNNING
	/* Perform scheduler related setup. Assign this task to a CPU. */
	sched_fork(p, clone_flags);
......
        // 根据clone_flags标志共享或复制进程信息:如CLONE_FILES表示使用父进程的文件描述符
	/* copy all the process information */
	if ((retval = copy_semundo(clone_flags, p)))
		goto bad_fork_cleanup_audit;
	if ((retval = copy_files(clone_flags, p)))
		goto bad_fork_cleanup_semundo;
	if ((retval = copy_fs(clone_flags, p)))
		goto bad_fork_cleanup_files;
	if ((retval = copy_sighand(clone_flags, p)))
		goto bad_fork_cleanup_fs;
	if ((retval = copy_signal(clone_flags, p)))
		goto bad_fork_cleanup_sighand;
	if ((retval = copy_mm(clone_flags, p)))
		goto bad_fork_cleanup_signal;
	if ((retval = copy_keys(clone_flags, p)))
		goto bad_fork_cleanup_mm;
	if ((retval = copy_namespaces(clone_flags, p)))
		goto bad_fork_cleanup_keys;

        // 复制特定于线程的数据,填充task_struct->thread的各个成员,如寄存器
	retval = copy_thread(0, clone_flags, stack_start, stack_size, p, regs);
	if (retval)
		goto bad_fork_cleanup_namespaces;

        // 设置各个ID、进程关系
        // 1. 分配新的pid实例、线程组TGID
	if (pid != &init_struct_pid) {
		retval = -ENOMEM;
		pid = alloc_pid(task_active_pid_ns(p));
		if (!pid)
			goto bad_fork_cleanup_namespaces;

		if (clone_flags & CLONE_NEWPID) {
			retval = pid_ns_prepare_proc(task_active_pid_ns(p));
			if (retval < 0)
				goto bad_fork_free_pid;
		}
	}
	p->pid = pid_nr(pid);    // pid_nr计算全局数值PID
	p->tgid = p->pid;
	if (clone_flags & CLONE_THREAD)
		p->tgid = current->tgid;
......
        // 2. 设置父进程。注意对线程来说,fork出来的子线程的父亲不是父线程,而是父线程的父进程
	/* CLONE_PARENT re-uses the old parent */
	if (clone_flags & (CLONE_PARENT|CLONE_THREAD))
		p->real_parent = current->real_parent;
	else
		p->real_parent = current;
	p->parent = p->real_parent;
......
        // 3. 线程的组长是当前进程的组长
	if (clone_flags & CLONE_THREAD) {
		p->group_leader = current->group_leader;
		list_add_tail_rcu(&p->thread_group, &p->group_leader->thread_group);
......
	}

        // PID加到ID数据结构的体系中,新进程还要加到当前进程组和会话中
	if (likely(p->pid)) {
                // 通过children链表与父进程连接起来
		add_parent(p);

		if (thread_group_leader(p)) {
			if (clone_flags & CLONE_NEWPID)
				p->nsproxy->pid_ns->child_reaper = p;
......
			set_task_pgrp(p, task_pgrp_nr(current));
			set_task_session(p, task_session_nr(current));
			attach_pid(p, PIDTYPE_PGID, task_pgrp(current));
			attach_pid(p, PIDTYPE_SID, task_session(current));
........
		}
		attach_pid(p, PIDTYPE_PID, pid);
		nr_threads++;
	}

	total_forks++;
......
	return p;

        //异常处理的资源回收逻辑,略
......
}

2.4.2 内核线程

  • 内核线程:直接由内核本身启动的线程(守护进程),可以通过ps fax看到,由方括号括起来
  • 用途
    • 周期性地将修改的内存页与页来源块设备同步(例如,使用mmap的文件映射)
    • 如果内存页很少使用,写入交换区
    • 管理延时动作(deferred action)
    • 实现文件系统的事务日志
  • 类型
    • 线程启动后一致等待,直至内核请求线程执行特定操作
    • 周期性间隔执行,检测特定资源的使用
  • 启动内核线程:调用kernel_thread函数
pid_t kernel_thread(int (*fn)(void *), void *arg, unsigned long flags)
{
......
	return do_fork(flags|CLONE_VM|CLONE_UNTRACED, 0, &regs, 0, NULL, NULL);
}
  • 特点
    • 在CPU的管态(supervisor mode)执行,而不是用户态执行
    • 只可以访问虚拟地址空间的内核部分,不能访问用户部分
  • 创建内核线程方法
    • 函数传递给kernel_thread,帮助内核调用daemonize转换为守护进程
    • kthread_create:创建新的内核线程,需要使用wake_up_process启动
    • kthread_run:创建后立即运行

2.4.3 启动新程序

  • execve系统调用:通过用新代码替换显存程序

do_execve:核心函数,读取二进制文件,并将应用程序映射到虚拟地址空间中

int do_execve(char * filename,        //可执行程序名称
	char __user *__user *argv,    //程序的参数指针
	char __user *__user *envp,    //程序的环境指针
	struct pt_regs * regs)        //寄存器
{
......
        // 打开可执行文件
	file = open_exec(filename);
	retval = PTR_ERR(file);
	if (IS_ERR(file))
		goto out_kfree;

        // 调度器的设置
	sched_exec();

        // 以下bprm的数据结构保存了新进程的各个参数(如:euid、egid、参数列表、环境、文件名等)
	bprm->file = file;
	bprm->filename = filename;
	bprm->interp = filename;
        
        //bprm_init处理了若干管理任务
        // 1. mm_alloc生成一个新的mm_struct实例管理进程地址空间
        // 2. init_new_context初始化该实例
        // 3. mm_init建立初始的栈
	retval = bprm_mm_init(bprm);
	if (retval)
		goto out_file;

	bprm->argc = count(argv, MAX_ARG_STRINGS);
	if ((retval = bprm->argc) < 0)
		goto out_mm;

	bprm->envc = count(envp, MAX_ARG_STRINGS);
	if ((retval = bprm->envc) < 0)
		goto out_mm;

	retval = security_bprm_alloc(bprm);
	if (retval)
		goto out;

        // 根据父进程构造bprm,特别是euid和egid
	retval = prepare_binprm(bprm);
	if (retval < 0)
		goto out;

        // 复制环境和参数数组内容
	retval = copy_strings_kernel(1, &bprm->filename, bprm);
	if (retval < 0)
		goto out;

	bprm->exec = bprm->p;
	retval = copy_strings(bprm->envc, envp, bprm);
	if (retval < 0)
		goto out;

	env_p = bprm->p;
	retval = copy_strings(bprm->argc, argv, bprm);
	if (retval < 0)
		goto out;
	bprm->argv_len = env_p - bprm->p;

        // 查找一种适当的二进制格式,用于执行特定文件(标准格式是ELF,用头部魔数识别)
        // 1. 释放原进程使用的所有资源
        // 2. 将应用程序映射到虚拟地址空间中,需考虑到程序各个段的处理
        // 3. 设置指令指针等寄存器,以便调度器选择该进程时能找到main函数
	retval = search_binary_handler(bprm,regs);
	if (retval >= 0) {
		/* execve success */
		free_arg_pages(bprm);
		security_bprm_free(bprm);
		acct_update_integrals(current);
		kfree(bprm);
		return retval;
	}
......
        // 异常处理,略
out_ret:
	return retval;
}

二进制格式:Linux支持运行多种二进制格式,标准是ELF格式

  • 二进制格式数据结构:理解为基类
struct linux_binfmt {
	struct list_head lh;
	struct module *module;
	int (*load_binary)(struct linux_binprm *, struct  pt_regs * regs);
	int (*load_shlib)(struct file *);
	int (*core_dump)(long signr, struct pt_regs *regs, struct file *file, unsigned long limit);
	unsigned long min_coredump;	/* minimal dump size */
	int hasvdso;
};
  • 必须实现的函数
    • load_binary:用于加载普通程序
    • load_shlib:用于加载共享库
    • core_dump:程序错误输出内存转储

2.4.4 退出进程

  • exit系统调用:终止进程,释放进程资源。核心是do_exit
  • 实现:各个引用计数器-1,如果引用计数器为0,则将相应内存区域返给内存管理模块