进程基本概念

进程的定义:
进程最经典的定义是一个执行中的程序的实例(an instance of a program in execution),每个进程拥有独立的逻辑控制流和独立的地址空间,给应用程序提供了该程序独占处理器和和存储器的假象,使得各个程序之间能够并发执行。进程不仅仅表征了程序,其还包括了一系列的数据结构以及资源,可以用来描述程序在执行中所处的状态。
对进程和程序的理解可以举个例子:一个用户可以在同时打开5份word文档,会产生5个独立的进程,而这5个进程会共享同样的程序代码。
从内核观点来说,进程是占有系统资源(cpu时间、内存等)的实体。
进程的同义词:在Linux系统中进程也被称为任务(tasks)或是线程(threads)。这里先介绍下线程:线程和进程在Linux系统中不加区分,线程和进程实现的方式相同,只是同一个进程内的线程共享地址空间、文件等相关资源。
进程的生命周期:
进程就像人类一样:进程是由别的进程生成的,自己也可以生成一个或是多个子进程,进程最终也会消亡。进程有个共通的特点:每个进程仅有一个父进程。进程也有继承的概念,当一个子进程被创建时,由于采用了写时拷贝(copy-on-write)的方法,使得子进程和父进程共享地址空间(同一物理内存拷贝)。但需要注意的是,父进程和子进程虽然共享了代码段,但是他们拥有自己独立的数据拷贝(栈和堆),子进程对堆和栈的修改对父进程来说是不可见的。
轻量级进程多线程的运用程序:
Linux使用轻量级进程(threads)对运用程序使用多线程提供支持。同一个进程内的线程会共享地址空间,打开的文件等相关资源。因此,当进程中某个线程修改了某个资源,那么对其他的线程是可见的。所以,在对共享资源进行访问时应进行同步。在Linux中实现多线程的应用程序是将轻量级的进程与某个线程相关联。实际上,Linux操作系统的基本调度单元是线程,线程之间的调度是独立的,这使得有的线程可能在休眠时,而另一个线程在运行当中。
由于创建线程时会用到写时拷贝的的技术,这里对相关概念简要介绍。现代的操作系统提供了虚拟存储器这一重要思想,使得每个进程都有独立的地址空间。然而对于诸如C标准库的函数,如果每个进程都含有一份拷贝,那么就非常浪费物理内存了。于是,虚拟存储器提供了共享对象的机制,多个进程共享该对象的拷贝。一个对象被映射到虚拟内存中,要么是私有对象,要么是共享对象。私有对象是采用写时拷贝的机制映射到虚拟内存当中的。如果有两个进程将同一对象作为私有对象映射到虚拟内存中,这个对象所处的地址区域将被标记为只读。只要这练个进程没有对该对象进行写操作,那么该对象就可继续被两个进程共享。反之,如果有进程对其进行写操作,就会在物理内存中创建该对象所处的页面的一个拷贝。通过这样的方法,能够充分有效利用物理内存资源。
进程描述符:顾名思义,进程描述符描述了进程的相关信息,在操作系统概念中被称为进程控制块(PCB),在Linux中进程描述符是一个很复杂的数据结构,其包含了进程的相关属性以及控制信息,例如:进程所处的状态,进程的优先级,父进程,子进程,进程ID,打开的文件,地址空间等信息。Linux中进程描述符结构体为task_struct,每一个进程对应一个进程描述符,各个进程的描述表通过list_head这个双向循环链表链接起来。
进程描述符的布局:
amage                                                                                          图1  进程描述符的布局
进程的状态:进程描述符中的state域描述了进程当前所处的状态。在<linux/sched.h>文件中对进程的状态做了定义:
/*
 * Task state bitmask. NOTE! These bits are also
 * encoded in fs/proc/array.c: get_task_state().
 *
 * We have two separate sets of flags: task->state
 * is about runnability, while task->exit_state are
 * about the task exiting. Confusing, but this way
 * modifying one set can’t modify the other one by
 * mistake.
 */
#define TASK_RUNNING        0
#define TASK_INTERRUPTIBLE  1
#define TASK_UNINTERRUPTIBLE    2
#define __TASK_STOPPED      4
#define __TASK_TRACED       8
/* in tsk->exit_state */
#define EXIT_DEAD       16
#define EXIT_ZOMBIE     32
#define EXIT_TRACE      (EXIT_ZOMBIE | EXIT_DEAD)
/* in tsk->state again */
#define TASK_DEAD       64
#define TASK_WAKEKILL       128
#define TASK_WAKING     256
#define TASK_PARKED     512
#define TASK_STATE_MAX      1024
下面来看一下各种状态表达的含义:
TASK_RUNNING:表示该进程正在运行或是在就绪队列中等待被执行。
TASK_INTERRUPTIBLE:表示该进程被阻塞,直到某个条件发生才被唤醒。
TASK_UNINSTERRUPTIBLE:其特点是不对信号做出响应,即使接受到特定的信号,也不会被调度执行。其他方面与TASK_INTERRUPTILBE相同。这种状态很少使用,但在特定情况下,如进程必须等待某个事件的发生,而不允许中途被其他事务干扰,比如:进程正在打开设备文件,设备文件对应的驱动程序正在检测相关的硬件设备。此时,设备驱动程序就不能被中断,直到检测完毕。否则,该硬件设备可能会造成一个不可预测的状态。
_TASK_STOP:进程停止执行的状态。当进程接收到SIGSTOP(进程停止执行信号)、SIGTSTP(在前台运行的程序,tty上暂停键按下——ctrl+Z发送此信号)、SIGTTIN(后台进程需要输入数据)、SIGTTOUT(后台进程需要输出数据)信号时就会进入该状态。
对于SIGSTOP信号来说,当进程接收到该信号,进程就会停止运行,今后无法自我唤醒,直到接收到SIGCONT信号。内核不会自动发送SIGSTOP信号,也不用于正常的任务控制。
_TASK_TRACED:进程执行时被调试器暂停。当一个进程被另一个进程跟踪(比如调试器执行ptrace()的系统调用调试另一个程序),这就会使得被调试的程序接收到调试信号时处于这个状态。
EXIT_ZOMIE:进程已经被终止了,但还没有被父进程回收,也没有从系统中清除,等待父进程收集关于它的一些统计信息。
EXIT_DEAD:进程的最终状态,当父进程使用wait4()或者waitpid()系统调用收集了其所有的统计信息后,该进程会被系统删除,进入该状态。
图2展示了进程各个状态之间的转换关系:
Image
图2 进程状态转移图

致命信号:向进程发送这种信号后会导致内核杀死该进程,SIGKILL属于致命信号。
TASK_WAKEKILL是在接收到致命信号后唤醒该进程。
TASK_KILLABLELinux Kernel 2.6.25为进程引入的新的睡眠状态,其运行原因与TASK_UNINTERRUPTIBEL类似,但可以响应致命信号。设置该状态的起因,是在2002年OPenAFS文件系统驱动程序在阻塞所有信号后等待事件发生,但由于某种原因该事件未能发生,该进程也无法被终止,最终出现了问题,最终只能重启计算机,所以后来设置了该状态响应致命信号。来看一下这种状态的定义:
#define TASK_KILLABLE (TASK_WAKEKILL | TASK_UNINTERRUPTIBLE)
#define TASK_STOPPED  (TASK_WAKEKILL | __TASK_STOPPED)
#define TASK_TRACED   (TASK_WAKEKILL | __TASK_TRACED)
可以这么理解,TASk_KILLABLE = TASK_WAKEKILL +TASK_UNINTERRUPTIBLE.
对于这两种新加入到Linux内核中的进程状态(TASK_WAKEKILL、TASK_KILLABLE),可以保存在进程描述符的state域,也可以保存于exit_state域中。而且,只有进程在被终结时才会到达这两类状态。
设置进程状态的方法:最简单的莫过于对state域进行直接赋值,比如p->state=TASK_INTERRUPTIBLE.另外,内核也使用set_task_state对指定的进程设置状态,set_current_state为当前正在执行的进程设置状态。其实,set_task_state和set_current_state都是在<linux/sched.h>的宏定义:
#define set_task_state(tsk, state_value)          \
     set_mb((tsk)->state, (state_value))

#define set_current_state(state_value)          \
     set_mb(current->state, (state_value))
标识一个进程:每一个进程都与内核中的进程描述符一一对应,这给标识各个进程提供了非常有用的方法,即通过32位的进程描述符地址进行标识,进程描述符指针指向这个32位的地址。
进程描述符的生命周期以及存放:进程是动态的实体,其生命周期可以短的可以为几毫秒,长的甚至达到几个月。由于不确定性,内核必须能够通过进程描述符同时处理系统中所有的进程。而进程描述符是通过slab分配器动态分配的,不是放在永久分配给内核的内存区。对于每个进程,Linux在Linux内核栈内创建一个thread_info的结构体与进程描述符相关联。这个结构体在<arch/x86/asm/thread_info.h>文件内定义:
truct thread_info {
     struct task_struct     *task;          /* main task structure */
     struct exec_domain     *exec_domain;     /* execution domain */
     __u32               flags;          /* low level flags */
     __u32               status;          /* thread synchronous flags */
     __u32               cpu;          /* current CPU */
     int               saved_preempt_count;
     mm_segment_t          addr_limit;
     struct restart_block    restart_block;
     void __user          *sysenter_return;
     unsigned int          sig_on_uaccess_error:1;
     unsigned int          uaccess_err:1;     /* uaccess failed */
};
而在<arch/x86/arm/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;
#ifdef CONFIG_ARM_THUMBEE
     unsigned long          thumbee_state;     /* ThumbEE Handler Base register */
#endif
     struct restart_block     restart_block;
};
这说明thread_info结构体随硬件体系结构的不同而不同。有的寄存器数目比较多的体系架构会使用某个寄存器保存进程的task_struct的指针,而对于x86这种寄存器数目较少的体系结构,会在Linux内核栈创建一个thread_info的结构体,通过计算偏移来间接获取task_struct的指针。thread_info与内核栈在linux kernel 2.6.37后所占的空间为8192Byte,即8kb,占两个页面。处于内核模式的进程访问的栈是内核数据段,这与在用户模式下的栈有很大不同。内核控制路径很少使用栈,所以几kb的内核栈就足够了,所以8kb对于栈和thread_info是足够的了。图3表达了内核栈与thread_info之间的关系:
Image                                               图3  进程描述符与内核栈
可以看出,内核栈和thread_info共占有8kb的地址空间(地址相减),由于thread_info占有了0x34(52)字节的空间,所以栈所能扩展的空间为8192-52=8140Byte.注意,栈是向低地址增长的。
根据上面的结构,可以通过esp获得当前正在运行的进程的thread_info结构地址,再通过这个地址索引到
task_struct的地址。在linux中,使用current宏来获取当前进程的进程描述符。current可以将esp指针的低13位屏蔽掉,计算出thread_info在栈中的偏移,这个步骤是在current_thread_info中定义的,在2.6.22版本以前,其处理方式如下:
    movl $0xffffe000,%ecx 
    /*or 0xfffff000 for 4KB stacks*/ 
    andl %esp,%ecx 
    movl %ecx,p
在执行了这三条指令之后没,p就保存了当前进程的thread_info的指针。那么,即可访问当前进程的进程描述符。
static __always_inline struct task_struct * get_current(void)
{
    return current_thread_info()->task;
}
#define current get_current()
通过以上的代码就很清晰的知道current为什么代表当前进程的描述符了。
以上所述的方式是在2.6.22版本之前的,那么2.6.22之后(包括2.6.22版本)current宏是怎样实现的呢? Linux将当前进程task_struct的地址保存于current_task中。

DEFINE_PER_CPU(struct task_struct *, current_task) ____cacheline_aligned = &init_task;
#define this_cpu_read_stable(var) percpu_from_op("mov", var, "p" (&(var)))

static __always_inline struct task_struct *get_current(void)
{
   return this_cpu_read_stable(current_task);
}

#define current get_current()

current_task在两种情况下会被更新:
1)开机后current_task设置为init_task进程,该进程是所有进程的task_struct原形,该进程并不是通过do_fork()创建的。
2)进程上下文切换的时候会设置current_task。在上下文切换时会调用_switch_to函数切换到下一个进程next_p。_switch_to还会调用this_cpu_write(Linux3.9)将next_p进程的task_struct地址保存到current_struct中。
__notrace_funcgraph struct task_struct * __switch_to (struct task_struct * prev_p,  struct task_struct *next_p )
{
     struct thread_struct * prev = &prev_p ->thread ,
                     * next = &next_p ->thread ;
     int cpu = smp_processor_id ();
     struct tss_struct * tss = &per_cpu (init_tss , cpu);
     fpu_switch_t fpu ;
     fpu = switch_fpu_prepare ( prev_p, next_p );
     load_sp0 (tss , next);
     lazy_save_gs ( prev-> gs );
     load_TLS (next , cpu);
     if ( get_kernel_rpl () && unlikely ( prev-> iopl != next -> iopl))
          set_iopl_mask ( next-> iopl );
     if ( unlikely (task_thread_info (prev_p )-> flags & _TIF_WORK_CTXSW_PREV ||
               task_thread_info ( next_p)-> flags & _TIF_WORK_CTXSW_NEXT ))
          __switch_to_xtra ( prev_p, next_p , tss );
     arch_end_context_switch ( next_p);
     if ( prev-> gs | next -> gs)
          lazy_load_gs ( next-> gs );
     switch_fpu_finish ( next_p, fpu );
     this_cpu_write ( current_task , next_p );
     return prev_p ;
}
 
至于进程调度时切换的详细过程,留待下次分解。
参考资料:http://www.ibm.com/developerworks/cn/linux/l-task-killable/
                   《深入理解计算机系统》第二版
                   《深入理解Linux内核》第三版
                   《Linux内核设计与实现》第三版