2. 进程管理
2.1 Linux进程
- 进程是程序执行时的一个动态实体,包含程序计数器、全部CPU 寄存器的值和进程堆栈中存储着的一些临时数据,如子程序参数、返回地址及变量等,反映的是当前处理器的活动状态。 而程序是仅包含指令和数据的一段静态代码。
- Linux 是一个多处理操作系统,进程拥有独立的权限和单一职责,每个进程都运行在各自独立的虚拟地址空间中,只有通过内核控制下的进程通信机制(管道、信号、信号量、消息队列等),它们之间才能发生通信。
- 从内核的观点看, 进程的目的就是担当分配系统资源(CPU 时间、内存等)的实体。 进程管理的最终目的就是在各进程顺畅执行的条件下,合理分配系统资源给不同的进程。
- 子进程刚被创建时,是父进程地址空间的一个(逻辑)备份,与父子进程共享程序代码,但它们分别拥有独立的数据备份。因此子进程对堆和栈中的数据进行修改时,对父进程的数据是不会有影响的。
2.2 进程描述符
-
内核对进程的优先级、进程的状态、地址空间等采用进程描述符表示。在 Linux 内核中,进程用一个相当大的称为
task_struct
的结构表示。下面是从 linux-2.6.29\include\linux\sched.h 中摘抄出来的进程描述的部分信息:struct task_struct { volatile long state; /* 进程状态, -1:不能运行, 0:运行, >0:停止 */ void *stack; atomic_t usage; unsigned int flags; /* 指示符,进程创建:PF_STARTING,退出:PF_EXITING,在分配内存:PF_MEMALLOC */ unsigned int ptrace; int lock_depth; /* BKL lock depth */ /* 每个进程都会被赋予优先级(称为 static_prio),但实际优先级是基于多因素动态决定的,值越低优先级越高。 */ int prio, static_prio, normal_prio; unsigned int rt_priority; const struct sched_class *sched_class; struct sched_entity se; struct sched_rt_entity rt; unsigned char fpu_counter; s8 oomkilladj; /* OOM kill score adjustment (bit shift). */ unsigned int policy; cpumask_t cpus_allowed; struct list_head tasks; /* 提供链接能力,包含prev指针指向前一个任务,next指针指向下一个任务 */ /* 进程地址空间由mm和active_mm表示,mm代表进程内存描述符,active_mm代表前一进程内存描述符(为改进上下文切换时间的一种优化) */ struct mm_struct *mm, *active_mm; /* task state */ struct linux_binfmt *binfmt; int exit_state; int exit_code, exit_signal; int pdeath_signal; /* The signal sent when the parent dies */ unsigned int personality; unsigned did_exec:1; pid_t pid; pid_t tgid; struct task_struct *real_parent; /* real parent process */ struct task_struct *parent; /* recipient of SIGCHLD, wait4() reports */ struct list_head children; /* list of my children */ struct list_head sibling; /* linkage in my parent's children list */ struct task_struct *group_leader; /* threadgroup leader */ struct list_head ptraced; struct list_head ptrace_entry; struct pid_link pids[PIDTYPE_MAX]; struct list_head thread_group; struct completion *vfork_done; /* for vfork() */ int __user *set_child_tid; /* CLONE_CHILD_SETTID */ int __user *clear_child_tid; /* CLONE_CHILD_CLEARTID */ ... };
2.3 进程状态
- 进程描述符中 state 字段描述进程当前的状态。它由一组标志组成,其中每个标志描述一种可能的进程状态。在 2.6 内核中,进程只能处于这些状态中的一种。下面分别对这些状态进行描述。
- 可运行状态(
TASK_RUNNING
):进程处于运行(系统当前进程)或者准备运行状态(等待系统将 CPU 分配给它)。 - 等待状态(
WAITING
):进程在等待一个事件或者资源。 Linux 将等待进程分成两类:可中断的等待状态(TASK_TNTERRUPTIBLE
)与不可中断的等待状态(TASK_UNINTERRUPTIBLE
)。前者可被信号中断,后者直接在硬件条件等待,并且任何情况下都不可中断。 - 暂停状态(
TASK_STOPPED
) : 进程被暂停, 通常是通过接收一个信号(SIGSTOP、SIGTSTP、 SIGTTIN 或 SIGTTOU)转为暂停状态。正在被调试的进程可能处于停止状态。 - 僵死状态(
EXIT_ZOMBIE
) : 进程的执行被终止, 但其父进程还没有执行wait4()
或waitpid()
系统调用返回有关该死亡进程的信息。
- 可运行状态(
2.4 进程调度
- Linux 进程调度指的是在所有可运行状态的进程中选择最值得运行的。每个进程的
task_struct
结构中的policy
、priority
、counter
和rt_priority
这 4 项,是选择进程的依据。- policy :进程调度策略,用于区分普通进程和实时进程,实时进程优先于普通进程运行;
- priority :进程(包括实时和普通)的静态优先级;
- counter:进程剩余时间片,起始值就是 priority 的值;因为 counter 用于计算一个处于可运行状态的进程值得运行的程度( goodness),所以 counter 也被看做是进程的动态优先级。
- rt_priority:实时进程特有的优先级别,用于实时进程间的选择。
- Linux进程分类
Linux 在执行进程调度的时候,对不同类型的进程采取的策略也不同,一般将 Linux
分为以下 3 类:- 交互式进程:当有用户输入时,这类进程必须很快地激活。通常要求延迟在 50~150 毫秒。典型的交互式进程有控制台命令、文本编辑器、图形应用程序等。
- 批处理进程(Batch Process):这类进程一般在后台运行,所以不需要非常快地反应,经常被调度期限制。典型的批处理进程有编译器、数据库搜索引擎和科学计算等。
- 实时进程:这类进程对调度(时间)有非常严格的要求,不能被低优先级进程阻塞,在很短时间内需做出反应。典型的实时进程有音视频应用程序、机器人控制等。
- Linux进程优先级
Linux 系统中每一个普通进程都有一个静态优先级,它被调度器作为参考来调度进程。在内核中调度的优先级区间为**[100,139]**,数字越小,优先级越高。一个新的进程总是从它的父进程继承此值。此外, Linux 进程优先级还包括动态优先级、实时优先级等,各个进程优先级描述如下:- 静态优先级(priority):被称为“静态”是因为它不随时间而改变,只能由用户进行修改。它指明了在被迫和其他进程竞争 CPU 之前,该进程所被允许的时间片的最大值(20)。
- 动态优先级(counter): counter 即系统为每个进程运行而分配的时间片。 Linux用它来表示进程的动态优先级。 当进程拥有 CPU 时, counter 就随着时间不断减小,当它递减为 0 时,标记该进程将重新调度。它指明了在当前时间片中所剩余的时间量(最初为 20)。
- 实时优先级(rt_priority):它的变化范围是从 0~99。任何实时进程的优先级都高于普通的进程。
- Base time quantum:是由静态优先级决定,当进程耗尽当前 Base time quantum,kernel 会重新分配一个 Base time quantum 给它。静态优先级和 Base time quantum的关系如下所述 :
- 当静态优先级<120:
Base time quantum(ms) = (140 – priority) * 20
- 当静态优先级>= 120:
Base time quantum(ms) = (140 – priority) * 5
- 当静态优先级<120:
- Linux进程的调度算法
- 时间片轮转调度算法(round-robin): SCHED_RR 用于实时进程。系统使每个进程依次地按时间片轮流执行的方式。
- 优先权调度算法: SCHED_NORMAL 用于非实时进程。每次系统都会选择队列中优先级最高的进程运行。 Linux 采用抢占式的优级算法,即系统中当前运行的进程永远是可运行进程中优先权最高的进程。
- 先进先出调度算法(FIFO): SCHED_FIFO 用于实时进程。采用 FIFO 调度算法选择的实时进程必须是运行时间较短的进程,因为这种进程一旦获得 CPU 就只有等到它运行完或因等待资源主动放弃 CPU 时,其他进程才能获得运行机会。
2.5 进程地址空间
-
Linux 的虚拟地址空间为 0~4GB,其分为内核空间和用户空间两部分。将最高的 1GB(从虚拟地址 0xC0000000~0xFFFFFFFF)留给内核使用,称为“内核空间”;较低的 3GB(从虚拟地址 0x00000000~0xBFFFFFFF)留给用户进程使用,称为“用户空间”。因为每个进程可以通过系统调用进入内核,因此, Linux 内核空间被系统的所有进程共享,实际上对于每个进程来说,它仍然可以拥有 4GB 的虚拟空间。
-
虚拟地址空间并不是实际的地址空间,在为进程分配地址空间时,根据进程需要的空间进行分配, 4GB 仅仅是最大限额而已,并非一次性将 4GB 分配给进程。一般进程的地址空间总是小于 4GB 的,可以通过查看
/proc/pid/maps
文件来获悉某个具体进程的地址空间。 -
进程的地址空间并不对应实际的物理页, Linux 采用Lazy 的机制来分配实际的物理页(Demand paging 和“写时复制(Copy On Write)的技术”),从而提高实际内存的使用率。虚拟页和物理页
的对应是通过映射机制来实现的,即通过页表进行映射到实际的物理页。因为每个进程都有自己的页表,因此可以保证不同进程的相同虚拟地址可以映射到不同的物理页,从而为不同的进程都可以同时拥有 4GB 的虚拟地址空间提供了可能。 -
内核是系统中优先级最高的部分,所以内核函数申请动态内存时系统不会推迟这个请求;但用户进程申请内存空间时,进程的可执行文件被装入后,进程不会立即对所有的代码进行访问。因此内核总是尽量推迟给用户进程分配动态空间。内核分配空间时,通过
__get_free_pages()
或alloc_pages
从分区页框分配器中获得页框; 通过kmem_cache_alloc()
或kmalloc()
函数使用 slab 分配器为对象分配块;通过vmalloc()
或vmalloc32()
函数获得一块非连续的内存区。 -
与进程地址空间有关的全部信息都包含在内存描述符的数据结构
mm_structs
中。 进程描述符的 mm字段就是指向这个结构。 -
进程地址空间得创建与删除
-
内核调用
copy_mm()
函数建立新进程的所有页表和内存描述符,来创建进程的地址空间。static int copy_mm(unsigned long clone_flags, struct task_struct * tsk) { struct mm_struct * mm, *oldmm; int retval; tsk->min_flt = tsk->maj_flt = 0; tsk->nvcsw = tsk->nivcsw = 0; tsk->mm = NULL; tsk->active_mm = NULL; /*如果是内核线程的子线程则直接退出,即 mm 和 active_mm 均为 NULL*/ oldmm = current->mm; if (!oldmm) return 0; /*内核线程,只是增加当前进程的虚拟空间的引用计数*/ if (clone_flags & CLONE_VM) { /*如果共享内存,将 mm 由父进程赋值给子进程,两个进程将会指向同一块内存*/ atomic_inc(&oldmm->mm_users); mm = oldmm; goto good_mm; } retval = -ENOMEM; mm = dup_mm(tsk); /*完成了对 vm_area_struct 和页面表的复制*/ if (!mm) goto fail_nomem; good_mm: /* Initializing for Swap token stuff */ mm->token_priority = 0; mm->last_interval = 0; /*内核线程的 mm 和 active_mm 指向当前进程的 mm_struct 结构*/ tsk->mm = mm; tsk->active_mm = mm; return 0; fail_nomem: return retval; }
-
内核调用
exit_mm()
函数释放进程的地址空间static void exit_mm(struct task_struct * tsk) { m_release(tsk, mm); /*得到读写信号量*/ down_read(&mm->mmap_sem); core_state = mm->core_state; if (core_state) { struct core_thread self; /*释放读写信号量*/ up_read(&mm->mmap_sem); self.task = tsk; self.next = xchg(&core_state->dumper.next, &self); if (atomic_dec_and_test(&core_state->nr_threads)) complete(&core_state->startup); for (;;) { set_task_state(tsk, TASK_UNINTERRUPTIBLE); if (!self.task) /*take 字段可以查看函数 coredump_finish()*/ break; schedule(); } __set_task_state(tsk, TASK_RUNNING); down_read(&mm->mmap_sem); } atomic_inc(&mm->mm_count); BUG_ON(mm != tsk->active_mm); /* more a memory barrier than a real lock */ task_lock(tsk); tsk->mm = NULL; up_read(&mm->mmap_sem); enter_lazy_tlb(mm, current); /*释放用户虚拟空间的数据结构*/ clear_freeze_flag(tsk); task_unlock(tsk); mm_update_next_owner(mm); /*递减 mm 的引用计数并是否为 0,如是,则释放 mm 所代表的映射*/ mmput(mm); }
-