进程

进程是处于执行期的程序,在内核中通常称为task,包含以下资源:

  • 可持续程序代码(text section)
  • 打开的文件
  • 挂起的信号
  • 内核内部数据
  • 处理器状态
  • 一个或多个具有内存映射的内存地址空间
  • 一个或多个线程
  • 全局变量的数据段
  • 等等

进程提供2种虚拟机制:

  • 虚拟处理器
  • 虚拟内存

线程是在进程中活动的对象,内核的调度对象是线程,而不是进程。线程之间有独立的虚拟处理器,但是共享虚拟内存。Linux对进程和线程不做特别区分,在Linux中线程只是一种特殊的进程。

线程拥有以下独立资源:

  • 程序计数器
  • 进程栈
  • 进程寄存器

相关系统调用:

  • 常用fork()系统调用创建进程,调用后从内核返回两次,一次回到父进程,一次回到子进程。
  • 现代Linux内核中fork()clone()系统调用实现。
  • fork()后调用exec()系列函数创建新的地址空间,载入程序。
  • 调用exit()退出执行,并释放资源。
  • 父进程通过wait4()查询子进程是否终结。
  • 子进程推出后变为zombie状态,直到父进程调用wait()waitpid()

进程描述符

任务结构

进程描述符存放在内核的任务队列(task list)双向链表中,链表的每一项是一个task_struct结构体,每个task_struct在32位机器上大约占1.7KB内存。

// linux/sched.h
struct task_struct{
    unsigned long state;
    int prio;
    unsigned long policy;
    struct task_struct *parent;
    struct list_head tasks;
    pid_t pid;
    // ...
}

分配进程描述符

内核通过slab分配器动态分配task_struct,在每个内核栈的栈底创建一个thread_info结构体,其中的task字段存放指向实际task_struct的指针。

// asm/thread_info.h
struct thread_info{
    struct task_struct *task;
    // ...
}

在內存中的结构如下图所示: thread_info

进程描述符的存放

PID是pid_t类型,一般就是int。可以通过/proc/sys/kernel/pid_max查看和修改PID最大值,这个值通常是400多万,足够使用。

获取当前进程描述符使用current()宏,等价于以下语句:

current_thread_info()->task

进程状态

task_struct的state字段表示进程状态 进程有5种状态:

  • TASK_RUNNING:可执行,正在执行或在运行队列中等待执行
  • TASK_INTERRUPTIBLE:被阻塞,等待某些条件完成
  • TASK_UNINTERRUPTBLE:不可中断的阻塞,对信号不做响应,较少使用
  • __TASK_TRACED:被其他进程跟踪,例如通过ptrace对程序跟踪调试
  • __TASK_STOPPED:停止执行,不能投入运行,在收到信号SIGSTOP,SIGTSTPSIGTIN,SIGTOUT等信号后。

进程上下文

当程序执行了系统调用或出发了异常,就陷入内核空间,此时内核处于进程上下文中,内核代表进程执行。进程上下文中current宏是有效的

进程家族树

task_struct中的parent字段记录了父进程的指针,所有进程都是PID为1的init进程的后代所有组成一颗树的结构,命令pstree打印进程树。

进程创建

其他操作系统都有spawn产生进程的机制,Linux将spawn过程拆成了fork()exec()两步。

fork()拷贝当前进程创建子进程,父子进程的区别在于PID、PPID、某些资源(挂起的信号)和统计量。

exec()加载可执行文件到虚拟地址空间,开始执行。

Copy-on-Write

fork()时不会复制整个进程地址空间,而是将地址空间以只读的方式共享,延迟到写入时再拷贝。

fork()

通过clone()系统调用实现,其他的函数如vfork(),__clone()都会调用clone()clone()再调用do_fork()

vfork()

不拷贝父进程的页表项,其他和fork()相同。子进程作为父进程的一个单独的线程在地址空间里运行,不能向地址空间写入,父进程被阻塞,直到子进程退出或执行exec()

线程的实现

内核中并没有线程的概念,把所有线程当作进程实现,线程仅仅是共享某些资源的进程。每个线程有自己的task_struct

举个例子,创建1个包含4线程的进程,在Linux中只需要创建4个进程,分别分配4个对应的task_struct,设置共享某些资源。

线程创建

clone(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND, 0);

共享虚拟内存地址空间、文件系统、文件名描述符、信号处理程序。

对比fork()

clone(SIGCHLD, 0);

对比vfork()

clone(CLONE_VFORK | CLONE_VM | SIGCHLD, 0);

clone()的参数标志都定义在linux/sched.h头文件中。

内核线程 kernel thread

  • 负责执行一些内核的后台操作
  • 没有独立的地址空间
  • 只在内核空间运行,不会切换到用户空间
  • 和普通进程一样可以被调度,被抢占

用命令ps -ef查看,进程名在[]中的都是内核进程。

内核线程只能由其他内核线程创建,从kthreadadd内核进程中衍生出所有新的内核线程。

  • 创建内核线程
// linux/kthread.h
struct task_struct *kthread_create(...); // 创建后需要调用wake_up_process()运行
struct task_struct *kthread_run(...); // 宏,创建内核线程并运行
  • 推出内核线程,可以调用do_exit(),或
int kthread_stop(struct task_struct *k);

进程终止

进程可以主动终止或被动终止

  • 主动终止,进程调用exit()系统调用
    • 显式调用
    • 隐式调用,例如从main()函数返回
  • 被动终止
    • 接收到不可忽略且不能处理的信号和异常

大部分情况需要调用linux/exit.c中的do_exit()函数,这个函数不会返回,将进程的退出状态exit_state设置为EXIT_ZOMBIE僵尸态,进程不会再被调度运行。与进程相关的资源都被释放,仅剩下内核栈、thread_infotask_struct

删除进程描述符

调用wait()系列函数释放僵尸子进程的剩余资源。

孤儿进程

如果父进程在子进程之前终止,init进程会作为新的父进程,init进程会清理掉僵尸进程。