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); }
-