[linux] 进程管理

文章来自公众号 技术乱舞

原创不易,欢迎关注

原文链接 https://mp.weixin.qq.com/s/2CiH7P31hMPGDqoLFC7qCA

文章目录

各位中秋快乐!今天刚从我大神的同门那学了一句话,我对家的思念,犹如一部二十四史,不知从何说起,对你也是哦。话不多说。

在内核里,进程应该是最基础的那个了吧,谈到进程就离不开线程,接触了这么久,在个人看来进程就是线程的集合,有时候也可把线程直接看做进程,在内核里调度只是线程,也许这话说得不太对,以前会把进程和线程分开理解,现在越感觉是一个东西,只不过是从哪方面来理解罢了。

2.1 进程与线程

首先谈谈进程与线程的概念

进程:处于执行期的程序(目标码存放在某种存储介质上)

线程:是在进程中活动的对象 ,每个线程都有独立的计数器、栈、寄存器

程序本身并不是进程,进程是处于执行期的程序以及相关的资源的总称,而且完全可以存在两个或多个不同的进程执行的是同一个程序,并且两个或两个以上并存的进程还可以共享许多诸如打开的文件、地址空间之类的资源。

2.2 进程描述符

在内核里,存放进程的叫做Task List(任务队列),是一个双向循环链表,这种链表估计都很熟悉,链表上的每一个节点都是类型为task_struct结构的结构体,这就叫做进程描述符。如下图所示(自己用IPad画的,太难看 = =)。

图片

这结构比较大,该结构体完整地描述一个正在执行的程序 :打开的文件、进程的地址空间、挂起的信号、进程的状态,等其他信息。struct task_struct在<include/linux/sched.h>中,下面是部分

struct task_struct {
  volatile long state;  /* -1 unrunnable, 0 runnable, >0 stopped */
  void *stack;
  atomic_t usage;
  unsigned int flags;  /* per process flags, defined below */
  unsigned int ptrace;
  int lock_depth;    /* BKL lock depth */
  int load_weight;  /* for niceness load balancing purposes */
  int prio, static_prio, normal_prio;
  struct list_head run_list;
  struct prio_array *array;
  ...

这个结构体主要包含下面的内容:

  • 标示符 :描述本进程的唯一标识符,用来区别其他进程。
  • 状态 :任务状态,退出代码,退出信号等。
  • 优先级 :相对于其他进程的优先级。
  • 程序计数器:程序中即将被执行的下一条指令的地址。
  • 内存指针:包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针。
  • 上下文数据:进程执行时处理器的寄存器中的数据。
  • I/O状态信息:包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。
  • 记账信息:可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。

2.3 进程描述符的分配与存放

在内核中,访问任务通常需要获得指向其task_struct的指针。实际上,内核中大部分处理进程的代码都是直接通过task_struct进行的。内核中current宏指向当前进程,因此通过current宏查找到当前正在运行的进程的进程描述符的速度就显得尤为重要。硬件体系结构不同哦,该宏的实现也不同,它必须针对专门的硬件体系结构做处理。有的硬件体系结构可以拿出一个专门寄存器来存放指向当前进程task_struct的指针,用于加快访问速度。struct thread_info 在文件<asm/thread_info.h>中,定义如下:

struct thread_info {
  unsigned long    flags;            /* low level flags */
  int      preempt_count;            /* 0 => preemptable, <0 => bug */
  mm_segment_t    addr_limit;        /* address limit */
  struct task_struct  *task;        /* main task structure */
  struct exec_domain  *exec_domain;  /* execution domain */
  __u32      cpu;                    /* cpu */
  __u32      cpu_domain;              /* cpu domain */
  struct cpu_context_save  cpu_context;  /* cpu context */
  __u32      syscall;                /* syscall number */
  __u8      used_cp[16];            /* thread used copro */
  unsigned long    tp_value;
  struct crunch_state  crunchstate;
  union fp_state    fpstate __attribute__((aligned(8)));
  union vfp_state    vfpstate;
  struct restart_block  restart_block;
};

x86体系结构,其寄存器并不富余,就只能在内核栈的尾端创建thread_info结构,通过计算偏移间接地查找task_struct结构。其中关系如图所示:

图片

个人看来,其实主要理解了图上的流程就明白了分配流程。

2.4 进程状态

关于进程状态,算是老生常谈了,系统中的每个进程都逃不开这五种状态。

  • TASK_RUNNING(运行):进程是可执行的,或者正在执行。
  • TASK_INTERRUPTIBLE(可中断):进程被阻塞(睡眠),直到某个条件一旦达成,进程的状态就被设置为TASK_RUNNING状态。
  • TASK_UNINTERRUPTIBLE(不可中断):除了就算是接收到信号也不会被唤醒或准备投入运行外,与TASK_INTERRUPTIBLE类似。这种状态通常在进程必须在等待时不受干扰或等待时间很快就会发生时出现。
  • __TASK_STOPPED(停止):进程被停止执行。
  • __TASK_TRACED:被另一个进程跟踪的进程,比如通过ptrace对调试程序进行跟踪。

五种状态的转换最好用图来表示,下图所示(说实话画了好久的,对自己来说更加印象深刻)

图片

在内核里,设置进程的状态,使用 set_task_state(task,state) 函数,函数在<linux/sched.h>中定义了

#define __set_task_state(tsk, state_value)    \
  do { (tsk)->state = (state_value); } while (0)
#define set_task_state(tsk, state_value)    \
  set_mb((tsk)->state, (state_value))

2.5 进程上下文

此处只是简单的介绍进程上下文含义,如果深入理解,会有很多知识。我们都知道可执行程序是进程的重要组成部分。代码从文件里载入进程的地址空间执行。一般情况下程序在用户空间执行,当程序调用了系统调用或者触发了某个异常,陷入内核空间,那么这个时候就称为进程上下文中。

这么说好像表述的不明白,处理器上有三种状态,内核态(软),内核态(硬),用户态。

  • 内核态,运行于进程上下文,内核代表进程运行于内核空间;
  • 内核态,运行于中断上下文,内核代表硬件运行于内核空间;
  • 用户态,运行于用户空间。

“进程上下文”,可以这么理解,用户进程传递给内核的参数以及内核要保存的那一整套的变量和寄存器值和环境等。

2.6 进程的创建

对于linux进程的创建,首先,fork()拷贝当前进程创建一个子进程,子进程与父进程的区别仅仅是PID,PPID(父进程pid,子进程将其设置为被拷贝进程的PID)和一些资源和统计量。另外exec()类函数读取可执行文件并将载入地址空间开始运行。

2.6.1写时拷贝

在这只解释写时拷贝的含义,说简单点,在进程创建的时候,子进程与父进程共享资源,如果没有发生父进程或子进程需要修改内存页面,那么根本就不会进行拷贝,以只读的方式共享,否则有一方需要修改,才会进行拷贝,先进行复制再修改。

2.6.2 fork() vfork() clone()

在谈进程创建过程之前,还要首先了解了解这三个函数,Unix标准的复制进程的系统调用就是fork(),但linux不只一个,而是这三个

  • fork()创造的子进程是父进程的完整副本,复制了父亲进程的资源,包括内存的内容task_struct内容
  • vfork()创建的子进程与父进程共享数据段,而且由vfork()创建的子进程将先于父进程运行
  • clone()Linux上创建线程一般使用的是pthread库 实际上linux也给我们提供了创建线程的系统调用,就是clone()

fork()和vfork()很相似,fork()父子进程执行先后不确定并且子进程拷贝父进程的数据段,代码段,vfork()子进程先执行,且子进程与父进程共享数据段。

这三个函数调用可以用图来表示:

图片

2.6.3 copy_process()

如上图来说,do_fork()完成了创建的大部分工作,最后调用copy_process()函数定义在<kernel/fork.c>中,在linux2.6版本中,大约400多行,主要完成的工作如下图:

图片

上面两图简要概括了进程创建的整体流程。

2.7 进程的终结

关于进程的终结,主要分两部分,释放资源和删除进程描述符,这部分我更想用图来描绘。

2.7.1 释放资源

释放资源调用exit()系统调用,显式或者隐式调用,然后do_exit()完成繁琐的终止工作。如图:

图片

2.7.2 删除进程描述符

删除进程描述符通过系统调用wait()函数(挂起调用函数的进程,直到其中的一个子进程退出,并返回子进程PID),然后release_task()会被调用,最终删除进程描述符。如图:

图片

2.8 小结

这一小节,谈了谈进程,对个人来说大部分知识只是回忆性,但是在画图的过程,整理用自己的话表达出来,对操作系统中的核心概念进程有了更进一步的了解,特别是画图,花费了大量时间,从博客偷偷记录自己学习生活到现在打算到公众号,这一篇让自己知道了写文章的意义何在,于他人可以学习新知识,于自己对知识点有了新一步的理解。与进程相关的细节还会有很多,在重新整理的时候,自己也会去看看内核源代码(linux2.6),内核代码博大精深,我在路上。

欢迎关注 #公众号:技术乱舞 一起交流

灵魂碰撞