进程管理--学习笔记

来源:互联网 发布:无名智者云签到源码 编辑:程序博客网 时间:2024/05/21 16:22

这几天在公司闲暇时间看了一下Robert Love的《linux内核设计与实现》(当然,我看的是中文翻译版哈。看到英文就头痛)进程管理、调度、系统调度、中断这几章,觉得这本书真的写的很好,对我这种级别的人刚好,看起来没那么卡,不像最开始接触linux的时候就抱起《linux内核源码情景分析》来看,看得我“云里来雾里去”,现在想起来真的觉得就是一口吃成大胖子,还没学会走路就开始跑了,好了,废话少说,这里把进程管理这章好好复习一下,希望有兴趣的人一起来学习!

 

1 几个概念

什么是进程

      进程就是处理执行期的程序。但进程不局限于一段可执行程序代码,通常还包括其他资源,如存放全局变量的数据段、打开的文件、挂起的信号等,当然还包括地址空间及一个或几个执行线程。

 

什么是线程

      线程就是在进程中活动的对象。(呵呵,好抽象啊)。每个线程都拥有一个独立的程序计数器、进程栈和一组进程寄存器。内核调度的对象是线程,而不是线程。在现在的系统大都都支持多线程应用程序,稍后会看到,在linux内核中,对线程的实现和进程并无特别区分。

 

创建进程

      要注意的是程序不是进程。进程是处于执行期的程序以及它所包含的资源的总称。实际上完全可能存在两个或多个不同的进程实际上执行同一个程序。并且它们还可以共享诸如打开的文件、地址空间之类的资源。在linux系统中,调用fork()系统调用(下次专门写一篇linux系统调用的实现机制),该系统调用复制一个现有进程来创建一个全新的进程。调用fork()的进程称为父进程,新产生的进程为子进程。在返回点这个相同位置上,父进程恢复执行,子进程开始执行。通常创建新恶进程为了执行新的不同的程序,会接着调用exec()这族函数;

 

进程退出

      程序通过exit()系统调用退出执行。这个函数会终结进程并将占用的资源释放掉。父进程可以通过wait4()(内核负责实现

wait4()系统调用。linux系统通过C库通常要提供wait() waitpid() wait3() wait4()函数,下次介绍了系统调用的实现就明白C库与系统调用的关系了)系统调用查询子进程是否终结,这其实使得进程有了等待特定进程执行完毕的能力。进程退出执行后被设置为僵死状态,直到它的父进程调用wait()或waitpid()为止。

 

任务

     经常我们能听到任务(task)这个概念。其实,linux内核通常把进程也叫做任务,经常,我们一般把在内核中运行的程序叫任务,在用户空间运行的程序叫进程。

 

2 进程描述符和任务队列

     内核把进程放在叫做任务队列(task list)的双向循环链表中。链表中的每一项都是task_struct,称为进程描述符(process decriptor)的结构,该结构定义在include/linux/sched.h文件中。进程描述符包含一个具体进程的所有信息。

task_struct

     task_struct相对较大,在32位机器上,大约为1.7K字节。但它包含了内核管理一个进程的所有信息,它包含的数据能完整描述一个正在执行的程序:它打开的文件、进程的地址空间、挂起的信号、进程的状态等。

 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 */

    ....

}

 

Linux通过slab分配器分配task_struct结构,这样能达到对象复用和缓存的目的(可以避免动态分配和释放所带来的资源消耗)。在2.6内核中,各个进程的task_struct存放在内核栈的尾端。这样,通过栈指针就能计算它们的位置。在linux中,在栈底或栈顶创建了一个新的结构struct thread_info:(<asm/thread_info.h>中定义)

struct 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   preempt_count;                        /* 0 => preemptable, <0 => BUG */
    mm_segment_t  addr_limit;
    struct restart_block    restart_block;
    void __user  *sysenter_return;
    #ifdef CONFIG_X86_32
    unsigned long           previous_esp;   /* ESP of the previous stack in case of nested (IRQ) stacks */
    __u8   supervisor_stack[0];
   #endif
   int   uaccess_err;
}

 

                    进程内核栈 (说明一下:每个进程都有自己的内核。当进程从用户态进入内核态时,CPU就自动地设置该进程的内核

 ------------------------------------------最高内存地址

|                                                   |

|              栈首                               |

|                                                   |

|                                                   |

|                                                   |

|                                                   |

|                                                   |

|                                                   |

|                                                   |

|-----------------------------------------|栈指针

|                                                   |

|                                                   |

|                                                   | 

|                                                   |

|-----------------------------------------| 

|                                                   |

|                                                   |

|    struct thread_struct                |

|-----------------------------------------|最低内存地址  current_thread_info()

         thread info 有一个指向进程描述符的指针

 

 

 每个任务的thread_info结构在它的内核栈的尾端分配。结构中task存放的是指向该任务实际task_struct指针。

 

这个地方我有点纳闷,直接用task_struct不就行了吗?为什么还要thread_info,不可以把thread_info其他一些信息放到task_struct里面???

 

可以通过current_thread_info()->task返回当前进程信息结构

 

进程状态

进程描述符中state描述了进程当前的状态,有下面五种状态:

TASK_RUNNING                运行------进程可执行;它或者正在执行,或者在运行队列中等待执行(我们知道如果cpu单核的话,一次只能执行一个任务,其他的任务在队列中等待执行)

TASK_INTERRUPTIBLE      可中断----进程正在睡眠,等待某些条件达成。一旦这些条件达成,内核就会把进程状态设置为运行。此状态也会因为接收到信号而提前被唤醒并投入运行

TASK_UNINTERRUPTIBLE  不可中断----除了不会因为接收到信号而投入运行,其状态与可中断状态相同。使用得较少

TASK_ZOMBIE                  僵死-------该进程结束了,但是父进程还没有调用wait4()系统调用。为了父进程能够获知它的消息,子进程的进程描述符仍然保留着

TASK_STOPPED                停止-------进程停止执行;进程没有投入运行也不能投入运行。通常发生在接收到SIGSTOP SIGSTP SIGTTIN SIGTTOU等信号的时候

 

设置当前进程的状态

采用set_current_state(state)或set_task_state(curent, state)函数。上面我们说了current_thread_info()->task很容易就获取了当前进程描述符,为什么不直接获取到了task->state = state设置呢?

那是因为如果在SMP(多核)系统中,需要保护,防止其他处理器做重新排序(进程运行队列);

 

进程上下文

这是一个很重要的概念。当一个程序执行系统调用或触发了某个异常时,它就陷入了内核空间,这时,我们称内核“代码进程执行”并处理进程上下文中。在此上下文中current宏是有效地。除非有更高级的进程抢占了当前进程的执行,否则在内核退出的时候,程序恢复在用户空间继续执行。

 

系统调用和异常处理程序是对内核明确定义的接口。进程只有通过这些接口才能陷入内核。

 

LINUX进程之间存在一个明显的继承关系。所有进程都是PID为1的init进程的后代。内核在系统启动的最后阶段启动init进程,该进程读取系统的初始化脚本并执行相关程序,最终完成系统的启动全过程。

 

每个进程都有一个父进程,每个进程可以拥有一个或多个子进程。拥有同一个父进程的所有进程为兄弟。进程间的关系存放在进程描述符中。每个task_struct都包含一个父进程指针,还包含一个children的子进程链表。

struct task_struct *task = current->parent;   /*  获取当前进程的父进程 */

struct list_head *list;

 

 

可以通过以下方式依次访问子进程:

struct task_struct *task;

struct list_head *list;

 

list_for_each(list, &current->children)

{

    task = list_entry(list, struct task_struct, sibling)   /*sibling也是进程描述符的成员。内核是这样解释的:linkage in my parent's children list,父进程的子进程链表,也就是当前进程的兄弟进程链表*/

}

 

 init进程的进程描述符是作为init_task静态分配的。(其实就是一个静态变量吧)

下面代码可得到所有进程之间的关系

struct task_struct *task;

 

for (task = current; task != &init_task; task = task->parent)

{

    ;

 

上面可以看到,可以从系统任何一个进程出发,找到任意指定的其他进程。但是我们不需要这么麻烦,只要简单的遍历系统中的所有进程,因为任务队列本来就是一个双向循环链表。如下:

对于给定的进程,获取链表下一个进程:

list_entry(task->tasks.next, struct task_struct, tasks) 

  (解释一下这个什么意思,回想一下前面的进程描述符结构,结构里是不是有一个成员list_head tasks 。 list_entry是一个宏,由它可以由其成员tasks的指针task->tasks.next得到struct task_struct的地址

    不知道有没有说清楚、。。)

 

同理,获取前一个进程:

list_entry(task->tasks.prev, struct task_struct, tasks)

 

同样,for_each_process(task)宏提供了一个依次访问这个任务的能力:

struct task_struct *task;

 

for_each_process(task)

{

}

 

但是,在一个拥有大量进程的系统中,通过这样的方法来遍历所有进程是非常耗时的,所以没有充足理由别这么干

 

 

 3 进程创建

 

 写时拷贝(copy-on-write)

一开始我们介绍了进程的创建,传统的fork()系统调用直接把所有资源复制给新创建的进程。这种实现过于简单且效率低下。linux的fork()使用写时拷贝(copy-on-write)页实现

 

 写时拷贝是一种可以推迟甚至免除拷贝数据的技术。内核并不复制整个进程地址空间,而是让父进程和子进程共享同一个拷贝,只有在需要写入的时候,数据才会被复制,从而使各个进程拥有各自的拷贝。

 fork()实际开销就是复制父进程的页表以及子进程创建唯一的进程描述符。

 

fork()

Linux通过clone()系统调用实现fork()。这个调用通过一系列的参数标志来指明父、子进程需要共享的资源。fork()、vfork()、 __clone()库函数都是根据各自需要的标志去调用clone()。

然后由clone()去调用do_fork()。

do_fork完成了创建中的大部分工作,它定义在kernel/fork.c,该函数调用copy_process函数,然后让进程开始运行。

copy_process()主要工作:

(1)调用dup_task_struct()为新进程创建一个内核栈、thread_info和task_struct,这些值与当前进程的值相同,此时父子进程描述符完成相同;

(2)检查新创建子进程是否超过进程数目限制;

(3)子进程描述符很多成员清0或者设为初始值,区别父进程;

(4)子进程的状态设置为TASK_UNINTERRUPTIBLE以保证它不会投入运行;

(5)调用copy_flags()以更新task_struct的flags成员。表明进程是否拥有超级用户权限的PF_SUPERPRIV标志清0。表明进程还没调用exec()函数的PF_FORKNOEXEC标志被设置;

(6)调用get_pid()为新进程获取一个有效PID;

(7)根据传递给clone()的参数标志,拷贝资源;

(8)让父子进程平分剩余的时间片

(9)返回一个指向子进程的指针

 

再回到do_fork()函数,如果copy_process()函数成功返回,新创建的子进程被唤醒并让其投入运行。内核有意选择子进程首先执行。因为一般子进程都会马上调用exec()函数,这样就可以避免写时拷贝的额外开销,

 

vfork()

vfork是fork还未实现写时拷贝页表现的一个优化,后来fork实现了就彻底没用了,这里也不介绍了;

 

 

 

 

 

原创粉丝点击