现在的位置: 首页 > 综合 > 正文

linux 进程

2013年03月03日 ⁄ 综合 ⁄ 共 33161字 ⁄ 字号 评论关闭
文章目录
 

进程

 

目 录

  1. 进程

    1. 信号
    2. sched.c
    3. 进程信号队列
    4. SMP
    5. 内核线程页目录的借用
    6. 代码分析
    7. 线程
    8. 进程描述符
    9. init进程从内核态切换到用户态
    10. SET_LINKS
    11. REMOVE_LINKS
    12. get_wchan()
    13. sigframe的结构
    14. rt_sigframe结构
    15. 信号队列的结构
    16. 内核线程简介
    17. 进程切换简介
    18. 同步机制

进程

一  进程调度

    进程的状态([include/linux.h]):

TASK_RUNNING, it means that it is in the "Ready List"
TASK_INTERRUPTIBLE, task waiting for a signal or a resource (sleeping)
TASK_UNINTERRUPTIBLE, task waiting for a resource (sleeping), it is in same "Wait Queue"
TASK_ZOMBIE, task child without father
TASK_STOPPED, task being debugged

       ______________     CPU Available     ______________
      |              |  ---------------->  |              |
      | TASK_RUNNING |                     | Real Running |
      |______________|  <----------------  |______________|
                           CPU Busy
            |   /|/
Waiting for |    | Resource
Resource   |    | Available
           /|/   |
    ______________________
   |                      |
   | TASK_INTERRUPTIBLE / |
   | TASK-UNINTERRUPTIBLE |
   |______________________|

                     Main Multitasking Flow

    从系统内核的角度看来,一个进程仅仅是进程控制表(process table)中的一项。进程控制表中的每一项都是一个task_struct 结构,而task_struct 结构本身是在include/linux/sched.h中定义的。在task_struct结构中存储各种低级和高级的信息,包括从一些硬件设备的寄存器拷贝到进程的工作目录的链接点。

    进程控制表既是一个数组,又是一个双向链表,同时又是一个树。其物理实现是一个包括多个指针的静态数组。此数组的长度保存在include/linux/tasks.h 定义的常量NR_TASKS中,其缺省值为128,数组中的结构则保存在系统预留的内存页中。链表是由next_task 和prev_task两个指针实现的,而树的实现则比较复杂。

    系统启动后,内核通常作为某一个进程的代表。一个指向task_struct的全局指针变量current用来记录正在运行的进程。变量current只能由kernel/sched.c中的进程调度改变。当系统需要查看所有的进程时,则调用for_each_task,这将比系统搜索数组的速度要快得多。

二、用户进程和内核线程

    某一个进程只能运行在用户方式(user mode)或内核方式(kernel mode)下。用户程序运行在用户方式下,而系统调用运行在内核方式下。在这两种方式下所用的堆栈不一样:用户方式下用的是一般的堆栈,而内核方式下用的是固定大小的堆栈(一般为一个内存页的大小)

    尽管linux是一个宏内核系统,内核线程依然存在,以便并行地处理一些内核的“家务室”。这些任务不占用USER memory(用户空间),而仅仅使用KERNEL memory。和其他内核模块一样,它们也在高级权限(i386系统中的RING 0)下工作作。内核线程是被kernel_thread [arch/i386/kernel/process]创建的,它又通过调用著名的clone系统调用[arch/i386/kernel/process.c] (类似fork系统调用的所有功能都是由它最终实现):

int kernel_thread(int (*fn)(void *), void * arg, unsigned long flags)
{
        long retval, d0;

        __asm__ __volatile__(
                "movl %%esp,%%esi/n/t"
                "int $0x80/n/t"         /* Linux/i386 system call */
                "cmpl %%esp,%%esi/n/t"  /* child or parent? */
                "je 1f/n/t"             /* parent - jump */
                /* Load the argument into eax, and push it.  That way, it does
                 * not matter whether the called function is compiled with
                 * -mregparm or not.  */
                "movl %4,%%eax/n/t"
                "pushl %%eax/n/t"
                "call *%5/n/t"          /* call fn */
                "movl %3,%0/n/t"        /* exit */
                "int $0x80/n"
                "1:/t"
                :"=&a" (retval), "=&S" (d0)
                :"0" (__NR_clone), "i" (__NR_exit),
                 "r" (arg), "r" (fn),
                 "b" (flags | CLONE_VM)
                : "memory");
        return retval;
}

    一旦调用,我们就有了一个新的任务(Task) (一般PID都很小, 例如2,3,等) 等待一个响应很慢的资源,例如swap或者usb事件,以便同步。下面是一些最常用的内核线程(你可以用ps x命令):

PID      COMMAND
1        init
2        keventd
3        kswapd
4        kreclaimd
5        bdflush
6        kupdated
7        kacpid
67        khubd

     init内核线程也是启动以后最初的进程。 它会调用其它用户模式的任务,(/etc/inittab)例如控制台守护进程(daemons), tty守护进程以及网络守护进程(rc脚本)。

下面是一个典型的内核线程kswapd [mm/vmscan.c].
kswapd是被clone()建立的 [arch/i386/kernel/process.c]''

|do_initcalls
   |kswapd_init
      |kernel_thread
         |syscall fork (in assembler)

·do_initcalls [init/main.c]
·kswapd_init [mm/vmscan.c]
·kernel_thread [arch/i386/kernel/process.c]

三 进程创建,运行和消失

    Linux系统使用系统调用fork( )来创建一个进程,使用exit( )来结束进程。fork( )和exit( )的源程序保存在kernel/fork.c and kernel/exit.c中。fork( )的主要任务是初始化要创建进程的数据结构,其主要的步骤有:

1)申请一个空闲的页面来保存task_struct。
2)查找一个空的进程槽(find_empty_process( ))。
3)为kernel_stack_page申请另一个空闲的内存页作为堆栈。
4)将父进程的LDT表拷贝给子进程。
5)复制父进程的内存映射信息。
6)管理文件描述符和链接点。

|sys_fork
   |do_fork
      |alloc_task_struct
         |__get_free_pages
       |p->state = TASK_UNINTERRUPTIBLE
       |copy_flags
       |p->pid = get_pid
       |copy_files
       |copy_fs
       |copy_sighand
       |copy_mm // should manage CopyOnWrite (I part)
          |allocate_mm
          |mm_init
             |pgd_alloc -> get_pgd_fast
                |get_pgd_slow
          |dup_mmap
             |copy_page_range
                |ptep_set_wrprotect
                   |clear_bit // set page to read-only
          |copy_segments // For LDT
       |copy_thread
          |childregs->eax = 0
          |p->thread.esp = childregs // child fork returns 0
          |p->thread.eip = ret_from_fork // child starts from fork exit
       |retval = p->pid // parent fork returns child pid
       |SET_LINKS // insertion of task into the list pointers
       |nr_threads++ // Global variable
       |wake_up_process(p) // Now we can wake up just created child
       |return retval

·sys_fork [arch/i386/kernel/process.c]
·do_fork [kernel/fork.c]
·alloc_task_struct [include/asm/processor.c]
·__get_free_pages [mm/page_alloc.c]
·get_pid [kernel/fork.c]
·copy_files
·copy_fs
·copy_sighand
·copy_mm
·allocate_mm
·mm_init
·pgd_alloc -> get_pgd_fast [include/asm/pgalloc.h]
·get_pgd_slow
·dup_mmap [kernel/fork.c]
·copy_page_range [mm/memory.c]
·ptep_set_wrprotect [include/asm/pgtable.h]
·clear_bit [include/asm/bitops.h]
·copy_segments [arch/i386/kernel/process.c]
·copy_thread
·SET_LINKS [include/linux/sched.h]
·wake_up_process [kernel/sched.c]

    撤消一个进程可能稍微复杂些,因为撤消子进程必须通知父进程。另外,使用kill( )也可以结束一个进程。sys_kill( )、sys_wait( )和sys_exit( )都保存在文件exit.c中。

    使用fork ( )创建一个进程后,程序的两个拷贝都在运行。通常一个拷贝使用exec ( )调用另一个拷贝。系统调用exec ( )负责定位可执行文件的二进制代码,并负责装入和运行。Linux系统中的exec ( )通过使用linux_binfmt结构支持多种二进制格式。每种二进制格式都代表可执行代码和链接库。linux _binfmt结构种包含两个指针,一个指向装入可执行代码的函数,另一个指向装入链接库的函数。

    Unix系统提供给程序员6种调用exec( ) 的方法。其中的5种是作为库函数实现,而sys_execve( )是由系统内核实现的。它执行一个十分简单的任务:装入可执行文件的文件头,并试图执行它。如果文件的头两个字节是#! ,那么它就调用在文件第一行中所指定的解释器,否则,它将逐个尝试注册的二进制格式。

[目录]


信号

struct semaphore {
        atomic_t count; 进程抓取semaphore时减1
        int sleepers; 抓取semaphore失败时增1
        wait_queue_head_t wait; semaphore的等待队列
};
        down(&sem) 编绎成:

        movl $sem,% ecx        通过寄存器ecx向__down函数传递sem指针
        decl sem
        js 2f 如果为负值,表示semaphore已被占用,执行__down_failed过程
1:
由于出现semaphore竞争的可能性比较小,将分支代码转移到.text.lock段,以缩短正常的指令路径.
.section .text.lock,"ax"
2:        call __down_failed
        jmp 1b
.previous
        ...

        up(&sem) 编绎成:

        movl $sem,% ecx
        incl sem
        jle 2f 如果小于或等于0,表示该semaphore有进程在等待,就去调用__up_wakeup
1:
.section .text.lock,"ax"
2:        call __up_wakeup
        jmp 1b
.previous
        ...
__down_failed:
        pushl % eax
        pushl % edx
        pushl % ecx ; eax,edx,ecx是3个可用于函数参数的寄存器
        call __down
        popl % ecx
        popl % edx
        popl % eax
        ret
__up_wakeup:
        pushl % eax
        pushl % edx
        pushl % ecx
        call __up
        popl % ecx
        popl % edx
        popl % eax
        ret
; semaphore.c
void __down(struct semaphore * sem)
{
        struct task_struct *tsk = current;
        DECLARE_WAITQUEUE(wait, tsk);
        tsk->state = TASK_UNINTERRUPTIBLE;
        add_wait_queue_exclusive(&sem->wait, &wait);
        // 将当前进程加入到该semaphore的等待队列中

        spin_lock_irq(&semaphore_lock);
        sem->sleepers++;
        for (;;) {
                int sleepers = sem->sleepers;

                /*
                * Add "everybody else" into it. They aren't
                * playing, because we own the spinlock.
                */

                // atomic_add_negative(int i,atomic_t *v)将i + v->counter相加,
                // 结果为负返回1,否则返回0
                if (!atomic_add_negative(sleepers - 1, &sem->count)) {
                // 如果(sleepers - 1 + sem->count.counter)非负,则说明
                // semaphore已经被释放,可以返回
                        sem->sleepers = 0;
                        break;
                }
                sem->sleepers = 1;        /* us - see -1 above */

                spin_unlock_irq(&semaphore_lock);
                // 当semaphore被up()唤醒时,schedule()返回
                schedule();
                // 虽然已线程被up恢复,但为防止碰巧又有一个线程获得了semaphore,
                // 因此将它们放在循环体中
                tsk->state = TASK_UNINTERRUPTIBLE;
                spin_lock_irq(&semaphore_lock);
        }
        spin_unlock_irq(&semaphore_lock);
        // 该进程获得了semaphore,将它从等待队列中删除
        remove_wait_queue(&sem->wait, &wait);
        tsk->state = TASK_RUNNING;
        // 为什么这里要调用wake_up,是因为调用它没有副作用从而防止潜在的死锁吗?
        wake_up(&sem->wait);
}
void __up(struct semaphore *sem)
{
扩展为
__wake_up_common(&sem->wait,TASK_UNINTERRUPTIBLE|TASK_INTERRUPTIBLE,1,0);
唤醒队列中第1个进程,即将第1个进程放入运行队列
        wake_up(&sem->wait);
}
; sched.c
static inline void __wake_up_common (wait_queue_head_t *q, unsigned int
mode,
                                     int nr_exclusive, const int sync)
{
        struct list_head *tmp, *head;
        struct task_struct *p;
        unsigned long flags;

        if (!q)
                goto out;

        wq_write_lock_irqsave(&q->lock, flags);

#if WAITQUEUE_DEBUG
        CHECK_MAGIC_WQHEAD(q);
#endif

        head = &q->task_list;
#if WAITQUEUE_DEBUG
        if (!head->next || !head->prev)
                WQ_BUG();
#endif
        tmp = head->next;
        while (tmp != head) {
                unsigned int state;
                wait_queue_t *curr = list_entry(tmp, wait_queue_t,
task_list);
                tmp = tmp->next;

#if WAITQUEUE_DEBUG
                CHECK_MAGIC(curr->__magic);
#endif
                p = curr->task;
                state = p->state;
                if (state & mode) {
#if WAITQUEUE_DEBUG
                        curr->__waker = (long)__builtin_return_address(0);
#endif
                        if (sync)
                                wake_up_process_synchronous(p);
                        else
                                wake_up_process(p);
                        if ((curr->flags & WQ_FLAG_EXCLUSIVE) && !--nr_exclusive)
                                break;
                }
        }
        wq_write_unlock_irqrestore(&q->lock, flags);
out:
        return;
}
; sched.c
inline void wake_up_process(struct task_struct * p)
{
        unsigned long flags;

        /*
        * We want the common case fall through straight, thus the goto.
        */
        spin_lock_irqsave(&runqueue_lock, flags);
        p->state = TASK_RUNNING;
        if (task_on_runqueue(p))
                goto out;
        add_to_runqueue(p);
        reschedule_idle(p);
out:
        spin_unlock_irqrestore(&runqueue_lock, flags);
}
; sched.c
static inline void wake_up_process_synchronous(struct task_struct * p)
{
        unsigned long flags;

        /*
        * We want the common case fall through straight, thus the goto.
        */
        spin_lock_irqsave(&runqueue_lock, flags);
        p->state = TASK_RUNNING;
        if (task_on_runqueue(p))
                goto out;
        add_to_runqueue(p);
out:
        spin_unlock_irqrestore(&runqueue_lock, flags);
}
; sched.h
static inline int task_on_runqueue(struct task_struct *p)
{
        return (p->run_list.next != NULL);
}
; sched.c
static inline void add_to_runqueue(struct task_struct * p)
{
        list_add(&p->run_list, &runqueue_head);
        nr_running++;
}
static LIST_HEAD(runqueue_head);

; fork.c
void add_wait_queue_exclusive(wait_queue_head_t *q, wait_queue_t *
wait)
{
        unsigned long flags;

        wq_write_lock_irqsave(&q->lock, flags);
        wait->flags = WQ_FLAG_EXCLUSIVE;
        __add_wait_queue_tail(q, wait);
        wq_write_unlock_irqrestore(&q->lock, flags);
}
; wait.h
static inline void __add_wait_queue_tail(wait_queue_head_t *head,
                                                wait_queue_t *new)
{
#if WAITQUEUE_DEBUG
        if (!head || !new)
                WQ_BUG();
        CHECK_MAGIC_WQHEAD(head);
        CHECK_MAGIC(new->__magic);
        if (!head->task_list.next || !head->task_list.prev)
                WQ_BUG();
#endif
        list_add_tail(&new->task_list, &head->task_list);
}
正执行调度的函数是schedule(void),它选择一个最合适的进程执行,并且真正进行上下文切换,
使得选中的进程得以执行。而reschedule_idle(struct task_struct *p)的作用是为进程选择
一个合适的CPU来执行,如果它选中了某个CPU,则将该CPU上当前运行进程的need_resched标志
置为1,然后向它发出一个重新调度的处理机间中断,使得选中的CPU能够在中断处理返回时执行
schedule函数,真正调度进程p在CPU上执行。在schedule()和reschedule_idle()中调用了goodness()
函数。goodness()函数用来衡量一个处于可运行状态的进程值得运行的程度。此外,在schedule()
函数中还调用了schedule_tail()函数;在reschedule_idle()函数中还调用了reschedule_idle_slow()。

 

[目录]


sched.c

|schedule
   |do_softirq // manages post-IRQ work
   |for each task
      |calculate counter
   |prepare_to__switch // does anything
   |switch_mm // change Memory context (change CR3 value)
   |switch_to (assembler)
      |SAVE ESP
      |RESTORE future_ESP
      |SAVE EIP
      |push future_EIP *** push parameter as we did a call
         |jmp __switch_to (it does some TSS work)
         |__switch_to()
          ..
         |ret *** ret from call using future_EIP in place of call address
      new_task

/*
* 'sched.c' is the main kernel file. It contains scheduling primitives
* (sleep_on, wakeup, schedule etc) as well as a number of simple system
* call functions (type getpid(), which just extracts a field from
* current-task
*/
#include
#include
#include
#include
#include
#include
#include
#define LATCH (1193180/HZ)
extern void mem_use(void);
extern int timer_interrupt(void);
extern int system_call(void);
union task_union {
struct task_struct task;
char stack[PAGE_SIZE];
};
static union task_union init_task = {INIT_TASK,};
long volatile jiffies=0;
long startup_time=0;
struct task_struct *current = &(init_task.task), *last_task_used_math =
NULL;
struct task_struct * task[NR_TASKS] = {&(init_task.task), };
long user_stack [ PAGE_SIZE>>2 ] ;
struct {
long * a;
short b;
} stack_start = { & user_stack [PAGE_SIZE>>2] , 0x10 };
/*
* 'math_state_restore()' saves the current math information in the
* old math state array, and gets the new ones from the current task
*/
void math_state_restore() @@协处理器状态保存
{
if (last_task_used_math)
__asm__("fnsave %0"::"m" (last_task_used_math->tss.i387));
if (current->used_math)
__asm__("frstor %0"::"m" (current->tss.i387));
else {
__asm__("fninit"::);
current->used_math=1;
}
last_task_used_math=current;
}
/*
* 'schedule()' is the scheduler function. This is GOOD CODE! There
* probably won't be any reason to change this, as it should work well
* in all circumstances (ie gives IO-bound processes good response etc).
* The one thing you might take a look at is the signal-handler code
here.
*
* NOTE!! Task 0 is the 'idle' task, which gets called when no other
* tasks can run. It can not be killed, and it cannot sleep. The 'state'
* information in task[0] is never used.
*/
void schedule(void)
{
int i,next,c;
struct task_struct ** p;
/* check alarm, wake up any interruptible tasks that have got a signal
*/
for(p = &LAST_TASK ; p > &FIRST_TASK ; --p)
if (*p) {
if ((*p)->alarm && (*p)->alarm < jiffies) {
@@??
(*p)->signal |= (1<<(SIGALRM-1));@@14-1
(*p)->alarm = 0;
}
if ((*p)->signal && (*p)->state==TASK_INTERRUPTIBLE)
(*p)->state=TASK_RUNNING;
}
@@ task 1 如何变为TASK_RUNNING??signal 如何得到,alarm如何变非0且 /* this is the
scheduler proper: */
@@操作系统最重要的函数,调度算法
@@这个循环要找到一个可运行的任务才能退出,会死在这吗?即如没有一个可运行
while (1) {
c = -1;
next = 0;
i = NR_TASKS;
p = &task[NR_TASKS];
while (--i) {
if (!*--p)
continue;
if ((*p)->state == TASK_RUNNING && (*p)->counter > c)
c = (*p)->counter, next = i;
}
if (c) break; @@记数大于零
for(p = &LAST_TASK ; p > &FIRST_TASK ; --p)
if (*p)
(*p)->counter = ((*p)->counter >> 1) +
(*p)->priority;
}
switch_to(next);
}
int sys_pause(void)
{
current->state = TASK_INTERRUPTIBLE; @@任务可中断
schedule();
return 0;
}
void sleep_on(struct task_struct **p)
{
struct task_struct *tmp;
if (!p)
return;
if (current == &(init_task.task))
panic("task[0] trying to sleep");
tmp = *p;
*p = current;
current->state = TASK_UNINTERRUPTIBLE;
schedule();
if (tmp) @@激活p,什么时候回来?唤醒上次睡眠的进程
tmp->state=0;
}
void interruptible_sleep_on(struct task_struct **p)
{
struct task_struct *tmp;
if (!p)
return;
if (current == &(init_task.task))
panic("task[0] trying to sleep");
tmp=*p;
*p=current;
repeat: current->state = TASK_INTERRUPTIBLE;
schedule();
if (*p && *p != current) {
(**p).state=0;
goto repeat;
}
@@好象下不来
*p=NULL;
if (tmp)
tmp->state=0;
}
void wake_up(struct task_struct **p)
{
if (p && *p) {
(**p).state=0; @@唤醒该进程running
*p=NULL; @@睡眠栈为0
}
}
void do_timer(long cpl) @@定时调度
{
if (cpl)
current->utime++; @@用户态时间加一
else
current->stime++; @@系统态时间加一
if ((--current->counter)>0) return; @@当前记数减一
current->counter=0;
if (!cpl) return;
schedule();
}
int sys_alarm(long seconds)
{
current->alarm = (seconds>0)?(jiffies+HZ*seconds):0;
return seconds;
}
int sys_getpid(void)
{
return current->pid;
}
int sys_getppid(void)
{
return current->father;
}
int sys_getuid(void)
{
return current->uid;
}
int sys_geteuid(void)
{
return current->euid;
}
int sys_getgid(void)
{
return current->gid;
}
int sys_getegid(void)
{
return current->egid;
}
int sys_nice(long increment)
{
if (current->priority-increment>0)
current->priority -= increment;
return 0;
}
int sys_signal(long signal,long addr,long restorer)
{
long i;
switch (signal) {
case SIGHUP: case SIGINT: case SIGQUIT: case SIGILL:
case SIGTRAP: case SIGABRT: case SIGFPE: case SIGUSR1:
case SIGSEGV: case SIGUSR2: case SIGPIPE: case SIGALRM:
case SIGCHLD:
i=(long) current->sig_fn[signal-1];
current->sig_fn[signal-1] = (fn_ptr) addr;
current->sig_restorer = (fn_ptr) restorer;
return i;
default: return -1;
}
}
void sched_init(void)
{
int i;
struct desc_struct * p;
set_tss_desc(gdt+FIRST_TSS_ENTRY,&(init_task.task.tss));@@init task tss
set_ldt_desc(gdt+FIRST_LDT_ENTRY,&(init_task.task.ldt));@@init ldt
p = gdt+2+FIRST_TSS_ENTRY;
for(i=1;i task[i] = NULL;
p->a=p->b=0;
p++;
p->a=p->b=0;
p++;
}
ltr(0); @@调入task 0的tss
lldt(0); @@调入task 0的ldt
outb_p(0x36,0x43); /* binary, mode 3, LSB/MSB, ch 0 */
outb_p(LATCH & 0xff , 0x40); /* LSB */
outb(LATCH >> 8 , 0x40); /* MSB */
set_intr_gate(0x20,&timer_interrupt); @@irq 0 时钟中断
outb(inb_p(0x21)&~0x01,0x21);
set_system_gate(0x80,&system_call);
}

 

[目录]


进程信号队列

    每个进程具有一个sigpending结构所描述的信号队列,它有3个成员,head指向第一个sigqueue成员,tail指向最末的sigqueue成员的next指针,signal描述了此队列中的信号集.

static int
send_signal(int sig, struct siginfo *info, struct sigpending *signals);
将信号sig和对应的消息结构info添加到信号队列signal中.
static int
collect_signal(int sig, struct sigpending *list, siginfo_t *info);
返回信号sig在队列list中的信息info.

struct task_struct {
        ...
        struct sigpending pending;
        ...
};
struct sigpending {
       struct sigqueue *head, **tail;
       sigset_t signal;
};
struct sigqueue {
       struct sigqueue *next;
       siginfo_t info;
       };
// kernel/signal.c
static int
send_signal(int sig, struct siginfo *info, struct sigpending *signals)
{
     struct sigqueue * q = NULL;
     /* Real-time signals must be queued if sent by sigqueue, or
       some other real-time mechanism.  It is implementation
      defined whether kill() does so.  We attempt to do so, on
      the principle of least surprise, but since kill is not
     allowed to fail with EAGAIN when low on memory we just
     make sure at least one signal gets delivered and don't
     pass on the info struct.  */

    if (atomic_read(&nr_queued_signals) < max_queued_signals) {
       q = kmem_cache_alloc(sigqueue_cachep, GFP_ATOMIC);
    }
    // nr_queued_signals和max_queued_signals用来限制全局sigqueue成员的数目
    if (q) {
        atomic_inc(&nr_queued_signals);
        q->next = NULL;
        *signals->tail = q;
        signals->tail = &q->next; tail总是指向最末的信号成员的next指针                switch ((unsign
ed long) info)
         {
        case 0:
          // info参数如果为0,表示信号来源于当前用户进程                                  q->info.si_signo =
sig;
            q->info.si_errno = 0;
            q->info.si_code = SI_USER;
          q->info.si_pid = current->pid;
            q->info.si_uid = current->uid;
          break;
        case 1:
          // info参数如果为1,表示信号来源于内核本身                                  q->info.si_signo = sig;
          q->info.si_errno = 0;
          q->info.si_code = SI_KERNEL;
           q->info.si_pid = 0;
          q->info.si_uid = 0;
           break;
        default:
           // 否则从info指针中拷贝信号
           copy_siginfo(&q->info, info);
          break;
        }
      }
      else if (sig >= SIGRTMIN && info && (unsigned long)info != 1                   && info->
si_code != SI_USER)
      {
        ; 如果该信号是内核发出的实时信号,就返回错误码
        /*
         * Queue overflow, abort.  We may abort if the signal was rt
         * and sent by user using something other than kill().
       */
        return -EAGAIN;
      }
      sigaddset(&signals->signal, sig); 将sig号标记在队列的信号集上
      return 0;
}
static int
collect_signal(int sig, struct sigpending *list, siginfo_t *info)
{
      if (sigismember(&list->signal, sig)) {
        /* Collect the siginfo appropriate to this signal.  */                struct sigqueue *q, **
pp;
         pp = &list->head; pp指向第一个信号成员的next指针
        while ((q = *pp) != NULL) {
                if (q->info.si_signo == sig)                                            goto found_it;
                pp = &q->next;
         }
        /* Ok, it wasn't in the queue.  We must have
           been out of queue space.  So zero out the
          info.
         */
        sigdelset(&list->signal, sig);
        info->si_signo = sig;
        info->si_errno = 0;
        info->si_code = 0;
        info->si_pid = 0;
        info->si_uid = 0;
        return 1;
   found_it:
   // 将找到信号成员从信号队列中删除
        if ((*pp = q->next) == NULL)
        list->tail = pp;
  /* Copy the sigqueue information and free the queue entry */
       copy_siginfo(info, &q->info);
       kmem_cache_free(sigqueue_cachep,q);
       atomic_dec(&nr_queued_signals);
  /* Non-RT signals can exist multiple times.. */
       if (sig >= SIGRTMIN) {
                while ((q = *pp) != NULL) {
                  if (q->info.si_signo == sig)                                             goto found_another;
                    pp = &q->next;
                }
       }
       sigdelset(&list->signal, sig);
    found_another:
        return 1;
    }
    return 0;
}

 

 

 

[目录]


SMP

    多处理机系统正在变得越来越普通。尽管大多数用户空间代码仍将完美地运行,而且有些情况下不需要增加额外的代码就能利用SMP特性的优势,但是内核空间代码必须编写成具备“SMP意识”且是“SMP安全的”。以下几段文字解释如何去做。

问题

    当有多个CPU时,同样的代码可能同时在两个或多个CPU上执行。这在如下所示用于初始化某个图像设备的例程中可能会出问题。
        void init_hardware(void)
        {
            outb(0x1, hardware_base + 0x30);
            outb(0x2, hardware_base + 0x30);
            outb(0x3, hardware_base + 0x30);
            outb(0x4, hardware_base + 0x30);
        }
    假设该硬件依赖于寄存器0x30按顺序依次被设为0、1、2、3来初始化,那么要是有另一个CPU来参乎的话,事情就会搞糟。想象有两个CPU的情形,它们都在执行这个例程,不过2号CPU进入得稍慢点:
        CPU 1                           CPU 2

        0x30 = 1
        0x30 = 2                        0x30 = 1
        0x30 = 3                        0x30 = 2
        0x30 = 4                        0x30 = 3
                                        0x30 = 4
    这会发生什么情况呢?从我们设想的硬件设备看来,它在寄存器0x30上收到的字节按顺序为:1、2、1、3、2、4、3、4。
    啊!原本好好的事第二个CPU一来就搞得一团糟了也。所幸的是,我们有防止这类事情发生的办法。

自旋锁小历史

    2.0.x版本的Linux内核通过给整个内核引入一个全局变量来防止多于一个CPU会造成的问题。这意味着任何时刻只有一个CPU能够执行来自内核空间的代码。这样尽管能工作,但是当系统开始以多于2个的CPU出现时,扩展性能就不怎么好。
    2.1.x版本的内核系列加入了粒度更细的SMP支持。这意味着不再依赖于以前作为全局变量出现的“大锁”,而是每个没有SMP意识的例程现在都需要各自的自旋锁。文件asm/spinlock.h中定义了若干类型的自旋锁。
    有了局部化的自旋锁后,不止一个CPU同时执行内核空间代码就变得可能了。

简单的自旋锁

    理解自旋锁的最简单方法是把它作为一个变量看待,该变量把一个例程或者标记为“我当前在另一个CPU上运行,请稍等一会”,或者标记为“我当前不在运行”。如果1号CPU首先进入该例程,它就获取该自旋锁。当2号CPU试图进入同一个例程时,该自旋锁告诉它自己已为1号CPU所持有,需等到1号CPU释放自己后才能进入。
        spinlock_t my_spinlock = SPIN_LOCK_UNLOCKED;
        unsigned long flags;

        spin_lock (&my_spinlock);
        ...
        critical section
        ...
        spin_unlock (&my_spinlock);

中断

    设想我们的硬件的驱动程序还有一个中断处理程序。该处理程序需要修改某些由我们的驱动程序定义的全局变量。这会造成混乱。我们如何解决呢?
    保护某个数据结构,使它免遭中断之修改的最初方法是全局地禁止中断。在已知只有自己的中断才会修改自己的驱动程序变量时,这么做效率很低。所幸的是,我们现在有更好的办法了。我们只是在使用共享变量期间禁止中断,此后重新使能。
    实现这种办法的函数有三个:
        disable_irq()
        enable_irq()
        disable_irq_nosync()
    这三个函数都取一个中断号作为参数。注意,禁止一个中断的时间太长会导致难以追踪程序缺陷,丢失数据,甚至更坏。
    disable_irq函数的非同步版本允许所指定的IRQ处理程序继续运行,前提是它已经在运行,普通的disable_irq则所指定的IRQ处理程序不在如何CPU上运行。
    如果需要在中断处理程序中修改自旋锁,那就不能使用普通的spin_lock()和spin_unlock(),而应该保存中断状态。这可通过给这两个函数添加_irqsave后缀很容易地做到:
        spinlock_t my_spinlock = SPIN_LOCK_UNLOCKED;
        unsigned long flags;

        spin_lock_irqsave(&my_spinlock, flags);
        ...
        critical section
        ...
        spin_unlock_irqrestore (&my_spinlock, flags);

 

[目录]


内核线程页目录的借用

    创建内核线程的时候,由于内核线程没有用户空间,而所有进程的内核页目录都是一样的((某些情况下可能有不同步的情况出现,主要是为了减轻同步所有进程内核页目录的开销,而只是在各个进程要访问内核空间,如果有不同步的情况,然后才进行同步处理),所以创建的内核线程的内核页目录总是借用进程0的内核页目录。

>>> kernel_thread以标志CLONE_VM调用clone系统调用
/*
* Create a kernel thread
*/
int kernel_thread(int (*fn)(void *), void * arg, unsigned long flags)
{
long retval, d0;

__asm__ __volatile__(
  "movl %%esp,%%esi/n/t"
  "int $0x80/n/t"  /* Linux/i386 system call */
  "cmpl %%esp,%%esi/n/t" /* child or parent? */
  /* Load the argument into eax, and push it.  That way, it does
   * not matter whether the called function is compiled with
   * -mregparm or not.  */
  "movl %4,%%eax/n/t"
  "pushl %%eax/n/t"
  "call *%5/n/t"  /* call fn */
  "movl %3,%0/n/t" /* exit */
  "int $0x80/n"
  "1:/t"
  :"=&a" (retval), "=&S" (d0)
  :"0" (__NR_clone), "i" (__NR_exit),
   "r" (arg), "r" (fn),
   "b" (flags | CLONE_VM)
  : "memory");
return retval;
}

>>> sys_clone->do_fork->copy_mm:
static int copy_mm(unsigned long clone_flags, struct task_struct * tsk)
{
struct mm_struct * mm, *oldmm;
int retval;

。。。。。。。。

tsk->mm = NULL;
tsk->active_mm = NULL;

/*
  * Are we cloning a kernel thread?
  *
  * We need to steal a active VM for that..
  */
>>> 如果是内核线程的子线程(mm=NULL),则直接退出,此时内核线程mm和active_mm均为为NULL
oldmm = current->mm;
if (!oldmm)
  return 0;

>>> 内核线程,只是增加当前进程的虚拟空间的引用计数
if (clone_flags & CLONE_VM) {
  atomic_inc(&oldmm->mm_users);
  mm = oldmm;
  goto good_mm;
}

。。。。。。。。。。

good_mm:
>>> 内核线程的mm和active_mm指向当前进程的mm_struct结构
tsk->mm = mm;
tsk->active_mm = mm;
return 0;

。。。。。。。
}

然后内核线程一般调用daemonize来释放对用户空间的引用:
>>> daemonize->exit_mm->_exit_mm:
/*
* Turn us into a lazy TLB process if we
* aren't already..
*/
static inline void __exit_mm(struct task_struct * tsk)
{
struct mm_struct * mm = tsk->mm;

mm_release();
if (mm) {
  atomic_inc(&mm->mm_count);
  if (mm != tsk->active_mm) BUG();
  /* more a memory barrier than a real lock */
  task_lock(tsk);
>>> 释放用户虚拟空间的数据结构
  tsk->mm = NULL;
  task_unlock(tsk);
  enter_lazy_tlb(mm, current, smp_processor_id());

>>> 递减mm的引用计数并是否为0,是则释放mm所代表的映射
  mmput(mm);
}
}

asmlinkage void schedule(void)
{
。。。。。。。。。
if (!current->active_mm) BUG();

。。。。。。。。。

prepare_to_switch();
{
  struct mm_struct *mm = next->mm;
  struct mm_struct *oldmm = prev->active_mm;
>>> mm = NULL,选中的为内核线程
  if (!mm) {
>>> 对内核线程,active_mm = NULL,否则一定是出错了
   if (next->active_mm) BUG();
>>> 选中的内核线程active_mm借用老进程的active_mm
   next->active_mm = oldmm;
   atomic_inc(&oldmm->mm_count);
   enter_lazy_tlb(oldmm, next, this_cpu);
  } else {
>>> mm != NULL 选中的为用户进程,active_mm必须与mm相等,否则一定是出错了
   if (next->active_mm != mm) BUG();
   switch_mm(oldmm, mm, next, this_cpu);
  }

>>> prev = NULL ,切换出去的是内核线程
  if (!prev->mm) {
>>> 设置其 active_mm = NULL 。
   prev->active_mm = NULL;
   mmdrop(oldmm);
  }
}

}

对内核线程的虚拟空间总结一下:
1、创建的时候:
父进程是用户进程,则mm和active_mm均共享父进程的,然后内核线程一般调用daemonize适头舖m
父进程是内核线程,则mm和active_mm均为NULL
总之,内核线程的mm = NULL;进程调度的时候以此为依据判断是用户进程还是内核线程。

2、进程调度的时候
如果切换进来的是内核线程,则置active_mm为切换出去的进程的active_mm;
如果切换出去的是内核线程,则置active_mm为NULL。

 

 

 

[目录]


代码分析

    LINUX系统是分时多用户系统, 它有多进程系统的特点,CPU按时间片分配给各个用户使用, 而在实质上应该说CPU按时间片分配给各个进程使用, 每个进程都有自己的运行环境以使得在CPU做进程切换时保存该进程已计算了一半的状态。

进程的切换包括三个层次:

    ·用户数据的保存: 包括正文段(TEXT), 数据段(DATA,BSS), 栈段(STACK), 共享内存段(SHARED MEMORY)的保存。
    ·寄存器数据的保存: 包括PC(program counter,指向下一条要执行的指令的地址),   PSW(processor status word,处理机状态字), SP(stack pointer,栈指针), PCBP(pointer of process control block,进程控制块指针), FP(frame pointer,指向栈中一个函数的local 变量的首地址), AP(augument pointer,指向栈中函数调用的实参位置), ISP(interrupt stack pointer,中断栈指针), 以及其他的通用寄存器等。
    ·系统层次的保存: 包括proc,u,虚拟存储空间管理表格,中断处理栈。以便于该进程再一次得到CPU时间片时能正常运行下去。

    多进程系统的一些突出的特点:
并行化
   一件复杂的事件是可以分解成若干个简单事件来解决的, 这在程序员的大脑中早就形成了这种概念, 首先将问题分解成一个个小问题, 将小问题再细分, 最后在一个合适的规模上做成一个函数。 在软件工程中也是这么说的。如果我们以图的方式来思考, 一些小问题的计算是可以互不干扰的, 可以同时处理, 而在关键点则需要统一在一个地方来处理, 这样程序的运行就是并行的, 至少从人的时间观念上来说是这样的。 而每个小问题的计算又是较简单的。
简单有序
   这样的程序对程序员来说不亚于管理一班人, 程序员为每个进程设计好相应的功能, 并通过一定的通讯机制将它们有机地结合在一起, 对每个进程的设计是简单的, 只在总控部分小心应付(其实也是蛮简单的), 就可完成整个程序的施工。
互不干扰
   这个特点是操作系统的特点, 各个进程是独立的, 不会串位。
事务化
   比如在一个数据电话查询系统中, 将程序设计成一个进程只处理一次查询即可, 即完成一个事务。当电话查询开始时, 产生这样一个进程对付这次查询; 另一个电话进来时, 主控程序又产生一个这样的进程对付, 每个进程完成查询任务后消失. 这样的编程多简单, 只要做一次查询的程序就可以了。

   Linux是一个多进程的操作系统,进程是分离的任务,拥有各自的权利和责任。如果一个进程崩溃,它不应该让系统的另一个进程崩溃。每一个独立的进程运行在自己的虚拟地址空间,除了通过安全的核心管理的机制之外无法影响其他的进程。
   在一个进程的生命周期中,进程会使用许多系统资源。比如利用系统的CPU执行它的指令,用系统的物理内存来存储它和它的数据。它会打开和使用文件系统中的文件,会直接或者间接使用系统的物理设备。如果一个进程独占了系统的大部分物理内存和CPU,对于其他进程就是不公平的。所以Linux必须跟踪进程本身和它使用的系统资源以便公平地管理系统中的进程。
   系统最宝贵的资源就是CPU。通常系统只有一个CPU。Linux作为一个多进程的操作系统,它的目标就是让进程在系统的CPU上运行,充分利用CPU。如果进程数多于CPU(一般情况都是这样),其他的进程就必须等到CPU被释放才能运行。多进程的思想就是:一个进程一直运行,直到它必须等待,通常是等待一些系统资源,等拥有了资源,它才可以继续运行。在一个单进程的系统中,比如DOS,CPU被简单地设为空闲,这样等待资源的时间就会被浪费。而在一个多进程的系统中,同一时刻许多进程在内存中,当一个进程必须等待时,操作系统将CPU从这个进程切换到另一个更需要的进程。
   我们组分析的是Linux进程的状态转换以及标志位的作用,它没有具体对应某个系统调用,而是分布在各个系统调用中。所以我们详细而广泛地分析了大量的原码,对进程状态转换的原因、方式和结果进行了分析,大致总结了整个Linux系统对进程状态管理的实现机制。

   Linux中,每个进程用一个task_struct的数据结构来表示,用来管理系统中的进程。Task向量表是指向系统中每一个task_struct数据结构的指针的数组。这意味着系统中的最大进程数受到Task向量表的限制,缺省是512。这个表让Linux可以查到系统中的所有的进程。操作系统初始化后,建立了第一个task_struct数据结构INIT_TASK。当新的进程创建时,从系统内存中分配一个新的task_struct,并增加到Task向量表中。为了更容易查找,用current指针指向当前运行的进程。

   task_struct结构中有关于进程调度的两个重要的数据项:
   struct task_struct {
       ………….
       volatile  long  state; /* -1 unrunnable , 0 runnable , >0 stopped */
       unsigned  long  flags; /* per process flags, defined below */
       ………….
     };
   每个在Task向量表中登记的进程都有相应的进程状态和进程标志,是进行进程调度的重要依据。进程在执行了相应的进程调度操作后,会由于某些原因改变自身的状态和标志,也就是改变state和flags这两个数据项。进程的状态不同、标志位不同对应了进程可以执行不同操作。在Linux2.2.8版本的sched.h中定义了六种状态,十三种标志。
//进程状态
#define TASK_RUNNING                0
#define TASK_INTERRUPTIBLE        1
#define TASK_UNINTERRUPTIBLE        2
#define TASK_ZOMBIE                4
#define TASK_STOPPED                8
#define TASK_SWAPPING                16

它们的含义分别是:

TASK_RUNNING:正在运行的进程(是系统的当前进程)或准备运行的进程(在Running队列中,等待被安排到系统的CPU)。处于该状态的进程实际参与了进程调度。
TASK_INTERRUPTIBLE:处于等待队列中的进程,待资源有效时唤醒,也可由其它进程被信号中断、唤醒后进入就绪状态。
TASK_UNINTERRUPTIBLE:处于等待队列中的进程,直接等待硬件条件,待资源有效时唤醒,不可由其它进程通过信号中断、唤醒。
TASK_ZOMBIE:终止的进程,是进程结束运行前的一个过度状态(僵死状态)。虽然此时已经释放了内存、文件等资源,但是在Task向量表中仍有一个task_struct数据结构项。它不进行任何调度或状态转换,等待父进程将它彻底释放。
TASK_STOPPED:进程被暂停,通过其它进程的信号才能唤醒。正在调试的进程可以在该停止状态。
TASK_SWAPPING:进程页面被兑换出内存的进程。这个状态基本上没有用到,只有在sched.c的count_active_tasks()函数中判断处于该种状态的进程也属于active的进程,但没有对该状态的赋值。

//进程标志位:
#define PF_ALIGNWARN        0x00000001
#define PF_STARTING        0x00000002
#define PF_EXITING        0x00000004
#define PF_PTRACED        0x00000010
#define PF_TRACESYS        0x00000020
#define PF_FORKNOEXEC        0x00000040
#define PF_SUPERPRIV        0x00000100
#define PF_DUMPCORE        0x00000200
#define PF_SIGNALED        0x00000400
#define PF_MEMALLOC        0x00000800
#define PF_VFORK            0x00001000
#define PF_USEDFPU        0x00100000
#define PF_DTRACE        0x00200000

其中PF_STARTING没有用到。
PF_MEMEALLOC和PF_VFORK这两个标志位是新版本中才有的。

各个标志位的代表着不同含义,对应着不同调用:

PF_ALIGNWARN    标志打印“对齐”警告信息,只有在486机器上实现
PF_STARTING      进程正被创建
PF_EXITING        标志进程开始关闭。
在do_exit()时置位。
    current->flags |= PF_EXITING
用于判断是否有效进程。
在nlmclnt_proc()(在fs/lockd/clntproc.c),如果current_flag为PF_EXITING,则进程由于正在退出清除所有的锁,将执行异步RPC 调用。
PF_PTRACED      进程被跟踪标志,
在do_fork()时清位。
    p->flags &= ~PF_PTRACED
当ptrace(0)被调用时置位,在进程释放前要清掉。
    current->flags |= PF_PTRACED
在sys_trace()中判断
如果request为PTRACE_TRACEME,如是则将current_flag置为PF_PTRACED;
如果request为PTRACE_ATTACH,则将child_flag置为PF_PTRACED,给child发一个SIGSTOP信号;
如果request为PTRACE_DETACH ,则将child清除PF_PTRACED。
在syscall_trace()中判断current_flag如果为PF_TRACED和PF_TRACESYS,则current强行退出时的出错代码置为SIGTRAP并将状态置为STOPPED。
PF_TRACESYS       正在跟踪系统调用。
do_fork()时清位,在进程释放前要清掉。
在sys_trace()中判断request如果为PTRACE_SYSCALL,则将child->flags 置为 PF_TRACESYS;如为PTRACE_SYSCALL,则将child->flags 清除 PF_TRACESYS;然后唤醒child。如果request为PTRACE_SINGLESTEP(即单步跟踪),则将child_flag清除PF_TRACESYS,唤醒child。
PF_FORKNOEXEC    进程刚创建,但还没执行。
在do_fork()时置位。
    p->flags |=  PF_FORKNOEXEC
在调入格式文件时清位。
    p->flags &= ~ PF_FORKNOEXEC
PF_SUPERPRIV       超级用户特权标志。
    如果是超级用户进程则置位,用户特权设为超级用户,如是超级用户,在统计时置统计标志(accounting flag)为ASU。
PF_DUMPCORE       标志进程是否清空core文件。
Core文件由gdb进行管理,给用户提供有用信息,例如查看浮点寄存器的内容比较困难,事实上我们可以从内核文件里的用户结构中得到
  Core文件格式如下图:
UPAGE
DATA
STACK
              Core 文件结构
       UPAGE是包含用户结构的一个页面,告诉gdb文件中现有内容所有寄存器也在           UPAGE中,通常只有一页。DATA存放数据区。STACK堆栈区
   最小Core文件长度为三页(12288字节)
在task_struct中定义一个dumpable变量,当dumpable==1时表示进程可以清空core文件(即将core文件放入回收站),等于0时表示该进程不能清空core文件(即core文件以放在回收站中,不可再放到回收站中),此变量初值为1。
例如在调用do_aout_core_dump()时判断current->dumpable是否等于1(即判断该进程是否能将core文件放入回收站),如果等于1则将该变量置为0,在当前目录下建立一个core dump image ,在清空用户结构前,由gdb算出数据段和堆栈段的位置和使用的虚地址,用户数据区和堆栈区在清空前将相应内容写入core dump,将PF_DUMPCORE置位,清空数据区和堆栈区。
只有在aout_core_dump()内调用do_aout_core_dump(),而没有地方调用aout_core_dump()。对其它文件格式也是类似。
9、 PF_SIGNALED       标志进程被信号杀出。
在do_signal()中判断信号,如果current收到信号为SIGHUP, SIGINT, SIGIOT, SIGKILL, SIGPIPE, SIGTERM, SIGALRM, SIGSTKFLT, SIGURG, SIGXCPU, SIGXFSZ, SIGVTALRM, SIGPROF, SIGIO, SIGPOLL, SIGLOST, SIGPWR,则执行lock_kernel(),将信号加入current的信号队列,将current->flag置为PF_SIGNALED,然后执行do_exit()
PF_USEDFPU        标志该进程使用FPU,此标志只在SMP时使用。
在task_struct中有一变量used_math,进程是否使用FPU。
在CPU从prev切换到next时,如果prev使用FPU则prev的flag清除PF_USEDFPU。
    prev->flags&=~PF_USEDFPU
在flush_thread()(arch/i386/kernel/process.c)、restore_i387_hard()、save_i387_hard()(arch/i386/kernel/signal.c)中,如果是SMP方式,且使用FPU则stts(),否则清除PF_USEDFPU。
    current->flags &= ~PF_USEDFPU
在sys_trace()中如果request为PTRACE_SETFPREGS,则将child的used_math置为1,将child_flag清除PF_USEDFPU。
    child->flags &= ~PF_USEDFPU
在SMP方式下进行跟踪时,判断是否使用FPU。
在跟踪时出现数学错误时清位。
    current->flags &= ~PF_USEDFPU
PF_DTRACE          进程延期跟踪标志,只在m68k下使用。
跟踪一个trapping指令时置位。
    current->flags |= PF_DTRACE
PF_ONSIGSTK        标志进程是否工作在信号栈,只在m68k方式下使用。
liunx 2.1.19版本中使用此标志位,而2.2.8版本中不使用。
在处理信号建立frame时如果sigaction标志为ONSTACK,则将current->flag置为PF_ONSIGSTK。
PF_MEMALLOC      进程分配内存标志。
linux 2.2.8版本中使用此标志位。
在kpiod()和kwpad()中置位。
    tsk->flags |= PF_MEMALLOC
PF_VFORK           linux 2.2.8版本中使用此标志位。
在copy_flags(unsigned long clone_flags, struct task_struct *p),如果clone_flags为CLONE_VFORK,则将p的flags置为PF_VFORK。
在mm_release()中将current ->flags清除PF_VFORK。
    tsk->flags &= ~PF_VFORK
    具体的分析由我组的另外同学进行。

Linux的各进程之间的状态转换的系统调用
我将参与Linux的各进程之间的状态转换的系统调用总结成一张流程图:

 

进程的创建:TASK_RUNNING

第一个进程在系统启动时创建,当系统启动的时候它运行在核心态,这时,只有一个进程:初始化进程。象所有其他进程一样,初始进程有一组用堆栈、寄存器等等表示的机器状态。当系统中的其他进程创建和运行的时候这些信息存在初始进程的task_struct数据结构中。在系统初始化结束的时候,初始进程启动一个核心进程(叫做init)然后执行空闲循环,什么也不做。当没有什么可以做的时候,调度程序会运行这个空闲的进程。这个空闲进程的task_struct是唯一一个不是动态分配而是在核心连接的时候静态定义的,为了不至于混淆,叫做init_task。
   系统调用sys_fork 和sys_clone都调用函数do_fork()(在kernel/fork.中定义)。
   进程由do_fork()函数创建,先申请空间,申请核心堆栈;然后在Task向量表中找到空闲位置;在进行正式初始化以前,将新创建的进程的状态都置为TASK_UNINTERRUPTIBLE,以免初始化过程被打断;开始初始化工作,如初始化进程时钟、信号、时间等数据;继承父进程的资源,如文件、信号量、内存等;完成进程初始化后,由父进程调用wake_up_process()函数将其唤醒,状态变为TASK_RUNNING,挂到就绪队列run queue,返回子进程的pid。

//   C:/SRCLNX/KERNEL/FORK.C
int do_fork(unsigned long clone_flags, unsigned long usp, struct pt_regs *regs)
{

        为新进程申请PCB空间;
        if (申请不到)
                返回错误,退出;
        为新进程申请核心堆栈;
        if (核心堆栈申请不到)
                返回错误,退出;
        为新进程在Task向量表中找到空闲位置;
/*复制父进程current PCB中的信息,继承current的资源*/;
    p = current;
        在进行正式初始化以前,将新创建的进程的状态都置为TASK_UNINTERRUPTIBLE,以免初始化过程被打断,并置一些标志位.
/*为防止信号、定时中断误唤醒未创建完毕的进                                       程,将子进程的状态设成不可中断的*/
        p->state = TASK_UNINTERRUPTIBLE;
/*跟踪状态和超级用户特权是没有继承性的,因为在root用户为普通用户创建进程时,出于安全考虑这个普通用户的进程不允许拥有超级用户特权。*/
        p->flags &= ~(PF_PTRACED|PF_TRACESYS|PF_SUPERPRIV);
/*将进程标志设成初建,在进程第一次获得CPU时,内核将根据此标志进行一定操作*/
        p->flags |= PF_FORKNOEXEC;
   开始Task_struct的初始化工作,如初始化进程时钟、信号、时间等数据;
   继承父进程所有资源:
                拷贝父进程当前打开的文件;
                拷贝父进程在VFS的位置;
                拷贝父进程的信号量;
                拷贝父进程运行的内存;
                拷贝父进程的线程;
   初始化工作结束,父进程将其将其唤醒,挂入running队列中,返回子进程的pid;

}

进程的调度(schedule()):

   处于TASK_RUNNING状态的进程移到run queue,会由schedule()按CPU调度算法在合适的时候选中,分配给CPU。
   新创建的进程都是处于TASK_RUNNING状态,而且被挂到run queue的队首。进程调度采用变形的轮转法(round robin)。当时间片到时(10ms的整数倍),由时钟中断引起新一轮调度,把当前进程挂到run queue队尾。
   所有的进程部分运行与用户态,部分运行于系统态。底层的硬件如何支持这些状态各不相同但是通常有一个安全机制从用户态转入系统态并转回来。用户态比系统态的权限低了很多。每一次进程执行一个系统调用,它都从用户态切换到系统态并继续执行。这时让核心执行这个进程。Linux中,进程不是互相争夺成为当前运行的进程,它们无法停止正在运行的其它进程然后执行自身。每一个进程在它必须等待一些系统事件的时候会放弃CPU。例如,一个进程可能不得不等待从一个文件中读取一个字符。这个等待发生在系统态的系统调用中。进程使用了库函数打开并读文件,库函数又执行系统调用从打开的文件中读入字节。这时,等候的进程会被挂起,另一个更加值得的进程将会被选择执行。进程经常调用系统调用,所以经常需要等待。即使进程执行到需要等待也有可能会用去不均衡的CPU事件,所以Linux使用抢先式的调度。用这种方案,每一个进程允许运行少量一段时间,200毫秒,当这个时间过去,选择另一个进程运行,原来的进程等待一段时间直到它又重新运行。这个时间段叫做时间片。
   需要调度程序选择系统中所有可以运行的进程中最值得的进程。一个可以运行的进程是一个只等待CPU的进程。Linux使用合理而简单的基于优先级的调度算法在系统当前的进程中进行选择。当它选择了准备运行的新进程

抱歉!评论已关闭.