Linux进程与线程及其他知识点拾遗

    0x00进程与线程的区别

    进程和线程都是由父进程创建出来了,区别在于是否共享页表和页目录表。

    进程是通过系统调用fork来创建的。使用全新的页表和页目录表,实行copy_on_write策略。

asmlinkage int sys_fork(struct pt_regs regs)  
{  
    return do_fork(SIGCHLD, regs.esp, &regs, 0);  
}  
    线程在C++层是通过pthread_create函数创建的,最后会调用到clone,

    int clone(int (*fn)(void *arg), void *child_stack, int flags, void *arg),传入的参数是 

    int clone_flags = (CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGNAL
    | CLONE_SETTLS | CLONE_PARENT_SETTID
    | CLONE_CHILD_CLEARTID | CLONE_SYSVSEM
    | 0);
asmlinkage int sys_clone(struct pt_regs regs)  
{  
    unsigned long clone_flags;  
    unsigned long newsp;  
  
    clone_flags = regs.ebx;//就是用户态的flags  
    newsp = regs.ecx;//就是用户态的child_stack  
    if (!newsp)  
        newsp = regs.esp;  
    return do_fork(clone_flags, newsp, &regs, 0);  
}  
    线程是共享父进程的页表和页目录表,线程是有独立的堆栈的,全局变量是共享的。

    

    还有一种通过vfork出来的线程,并没有独立的堆栈。所以子线程在执行时,父进程必须等待;子线程执行完毕,通知父进程继续执行。

asmlinkage int sys_vfork(struct pt_regs regs)  
{  
    return do_fork(CLONE_VFORK | CLONE_VM | SIGCHLD, regs.esp, &regs, 0);//主要区别是有两个标志位CLONE_VFORK,CLONE_VM  
}  


    0x01进程调度

    进程调度分为主动调度和被动调度。

    1、以这个例子来说下主动调度。

 #include <stdio.h>  
  
int main()  
{  
    int child;  
    char *args[] = {"/bin/echo", "Hello", "World!", NULL};  
      
    if(!(child = fork()))  
    {  
        /* child */  
        execve("/bin/echo", args, NULL});  
        printf("I am back, something is wrong!\n");  
    }     
    else  
    {  
        /* father */  
        wait4(child, NULL, 0, NULL);  
    }  
}  
    1)、父进程fork出子进程

    2)、父进程执行wait4,并调用schedule切换到子进程

    3)、子进程开始执行execve

    4)、子进程执行完/bin/echo之后,会调动do_exit,部分销毁子进程,并调用schedule切换到正在sys_wait4等待的父进程

    5)、父进程彻底销毁子进程


    这里我们看到了两处主动调度。


    2、什么时候发生被动调度呢?

    当唤醒某个进程,或者时钟中断发现当前进程的时间片已经耗尽,会将当前进程task_struck的need_resched置为1,当中断、异常或者系统调用返回到用户空间之前会调用schedule执行调度。


    0x02进程切换是如何完成的呢

    首先说明下,linux调度的基本单元的线程和进程。

    1、切换到子进程

    我们知道fork出来的子进程,堆栈如下,pt_regs保存了返回的值,如eax已经被设置为0,fork返回0用来标记子进程:


    进程切换的关键代码是

#define switch_to(prev,next,last) do {                  \  
    asm volatile("pushl %%esi\n\t"                  \ //把esi存入现在进程prev的堆栈  
             "pushl %%edi\n\t"                  \ //把edi存入现在进程prev的堆栈  
             "pushl %%ebp\n\t"                  \ //把ebp存入现在进程prev的堆栈  
             "movl %%esp,%0\n\t"    /* save ESP */      \ //现在进程prev的esp保存在prev->thread.esp  
             "movl %3,%%esp\n\t"    /* restore ESP */   \ //将要切换的进程next->thread.esp保存在esp中,堆栈已经切换了   
             "movl $1f,%1\n\t"      /* save EIP */      \ //现在进程prev的eip(也就是"1:\t"地址)保存在prev->thread.eip  
             "pushl %4\n\t"     /* restore EIP */   \ //将要切换的进程next->thread.eip保存在eip中  
             "jmp __switch_to\n"                \ //且不说__switch_to中干了些什么,当CPU执行到那里的ret指令时,由于是通过jmp指令转过去的,最后进入堆栈的next->thread.eip就变成了返回地址  
             "1:\t"                     \ //如果切换的不是子进程,next->thread.eip实际上就是上一次保存在prev->thread.eip,也就是这一行语句  
             "popl %%ebp\n\t"                   \ //由于堆栈已经切换过来,pop出的都是上面存入进程prev堆栈的内容  
             "popl %%edi\n\t"                   \  
             "popl %%esi\n\t"                   \  
             :"=m" (prev->thread.esp),"=m" (prev->thread.eip),    \  
              "=b" (last)                   \  
             :"m" (next->thread.esp),"m" (next->thread.eip),  \  
              "a" (prev), "d" (next),               \  
              "b" (prev));                  \  
} while (0)
    由于我们在fork中,设置了一些值,这里就用了:

int copy_thread(int nr, unsigned long clone_flags, unsigned long esp,  
    unsigned long unused,  
    struct task_struct * p, struct pt_regs * regs)  
{  
    struct pt_regs * childregs;  
  
    childregs = ((struct pt_regs *) (THREAD_SIZE + (unsigned long) p)) - 1;//指向了子进程系统空间堆栈中的pt_regs结构  
    struct_cpy(childregs, regs);//把当前进程系统空间堆栈中的pt_regs结构复制过去  
    childregs->eax = 0;//子进程系统空间堆栈中的pt_regs结构eax置成0  
    childregs->esp = esp;//子进程系统空间堆栈中的pt_regs结构esp置成这里的参数esp,在fork中,则来自调用do_fork()前夕的regs.esp,所以实际上并没有改变  
  
    p->thread.esp = (unsigned long) childregs;//子进程系统空间堆栈中pt_regs结构的起始地址  
    p->thread.esp0 = (unsigned long) (childregs+1);//指向子进程的系统空间堆栈的顶端  
  
    p->thread.eip = (unsigned long) ret_from_fork;  
  
    savesegment(fs,p->thread.fs);  
    savesegment(gs,p->thread.gs);  
  
    unlazy_fpu(current);  
    struct_cpy(&p->thread.i387, current->thread.i387);  
  
    return 0;  
}  

    所以执行完swith_to代码后,程序开始执行ret_from_fork,pop堆栈,返回到用户态,由于我们已经设置堆栈中eax为0,所以子进程fork返回到用户态的值为0。


    2、切换到其他进程

    进程切换关键代码swith_to,被终止的进程下一次回来继续执行的eip就是popl %%ebp\n\t的地址,此时堆栈已经切回到当前进程的堆栈。

    和切换到子进程不同的是返回eip不同,子进程eip指向ret_from_fork;其他进程eip指向popl %%ebp\n\t的地址。

    堆栈也是不同的,子进程的堆栈中只有regs;而其他进程的堆栈除了regs,还有一些函数的调用中使用的栈,不过返回到用户空间的时候,进程栈都是清空的。


    3、将新进程页面目录的起始物理地址装入到控制寄存器CR3中

static inline void switch_mm(struct mm_struct *prev, struct mm_struct *next, struct task_struct *tsk, unsigned cpu)  
{  
    if (prev != next) {  
        /* stop flush ipis for the previous mm */  
        clear_bit(cpu, &prev->cpu_vm_mask);  
        /* 
         * Re-load LDT if necessary 
         */  
        if (prev->context.segments != next->context.segments)  
            load_LDT(next);  
#ifdef CONFIG_SMP  
        cpu_tlbstate[cpu].state = TLBSTATE_OK;  
        cpu_tlbstate[cpu].active_mm = next;  
#endif  
        set_bit(cpu, &next->cpu_vm_mask);  
        /* Re-load page tables */  
        asm volatile("movl %0,%%cr3": :"r" (__pa(next->pgd)));//我们只关心这一句,将新进程页面目录的起始物理地址装入到控制寄存器CR3中  
    }  
#ifdef CONFIG_SMP  
    else {  
        cpu_tlbstate[cpu].state = TLBSTATE_OK;  
        if(cpu_tlbstate[cpu].active_mm != next)  
            BUG();  
        if(!test_and_set_bit(cpu, &next->cpu_vm_mask)) {  
            /* We were in lazy tlb mode and leave_mm disabled  
             * tlb flush IPI delivery. We must flush our tlb. 
             */  
            local_flush_tlb();  
        }  
    }  
#endif  
}  


    4、更新进程栈首地址

    Linux没有为每一个进程都准备一个tss段,而是每一个cpu使用一个tss段,tr寄存器保存该段。进程切换时,只更新唯一tss段中的esp0字段到新进程的内核栈。

    在进程切换关键代码swith_to中会调用__swith_to:

void __switch_to(struct task_struct *prev_p, struct task_struct *next_p)  
{  
    struct thread_struct *prev = &prev_p->thread,  
                 *next = &next_p->thread;  
    struct tss_struct *tss = init_tss + smp_processor_id();  
  
    unlazy_fpu(prev_p);  
  
    /* 
     * Reload esp0, LDT and the page table pointer: 
     */  
    tss->esp0 = next->esp0;//将TSS中的内核空间(0级)堆栈指针换成next->esp0,指向子进程的系统空间堆栈的顶端  
  
    /* 
     * Save away %fs and %gs. No need to save %es and %ds, as 
     * those are always kernel segments while inside the kernel. 
     */  
    asm volatile("movl %%fs,%0":"=m" (*(int *)&prev->fs));  
    asm volatile("movl %%gs,%0":"=m" (*(int *)&prev->gs));  
  
    /* 
     * Restore %fs and %gs. 
     */  
    loadsegment(fs, next->fs);  
    loadsegment(gs, next->gs);  
  
    /* 
     * Now maybe reload the debug registers 
     */  
    if (next->debugreg[7]){  
        loaddebug(next, 0);  
        loaddebug(next, 1);  
        loaddebug(next, 2);  
        loaddebug(next, 3);  
        /* no 4 and 5 */  
        loaddebug(next, 6);  
        loaddebug(next, 7);  
    }  
  
    if (prev->ioperm || next->ioperm) {  
        if (next->ioperm) {  
            /* 
             * 4 cachelines copy ... not good, but not that 
             * bad either. Anyone got something better? 
             * This only affects processes which use ioperm(). 
             * [Putting the TSSs into 4k-tlb mapped regions 
             * and playing VM tricks to switch the IO bitmap 
             * is not really acceptable.] 
             */  
            memcpy(tss->io_bitmap, next->io_bitmap,  
                 IO_BITMAP_SIZE*sizeof(unsigned long));  
            tss->bitmap = IO_BITMAP_OFFSET;  
        } else  
            /* 
             * a bitmap offset pointing outside of the TSS limit 
             * causes a nicely controllable SIGSEGV if a process 
             * tries to use a port IO instruction. The first 
             * sys_ioperm() call sets up the bitmap properly. 
             */  
            tss->bitmap = INVALID_IO_BITMAP_OFFSET;  
    }  
} 


    0x04中断、异常、系统调用

    这三者切换到内核态的堆栈就是tss中esp0

    他们在返回用户空间时都会检查是是否有下半部需要处理,是否需要调度,是否有信号需要处理。

    中断上半部一般都是在关中断下进行,执行一些时间短和硬件相关的操作;中断下半部一般在开中断下进程,一般执行一些耗时长的任务。

    相同中断线的中断不可以打断同类中断,但是不同中断线的中断可以相互打断。

    tasklet是特殊的软中断,与一般的软中断不同,某一段tasklet代码在某个时刻只能在一个CPU上运行,而不像一般的软中断服务函数(即softirq_action结构中的action函数指针)那样——在同一时刻可以被多个CPU并发地执行。


    0x05进程的页目录表、页表

    0号进程->1号内核进程->1号用户进程(init进程)->getty进程->shell进程,具体可参考http://blog.csdn.net/gongxifacai_believe/article/details/53771464


     以android linux内核为例,0号进程不仅创建了init进程,还创建了2号内核进程,2号内核进程进一步创建了很多内核线程,每个内核线程都有独立的堆栈,并共享内核数据。

     内核线程和父进程共享页目录表和页表,但不能映射到用户空间的页面。

     每个用户态进程或者线程都是4G的虚拟地址空间,0~3G为用户态映射的区域,3G~4G为内核态映射的区域(对应物理地址0~1G)。

     所以每个用户态进程都有独立的页目录表和页表,线程则共享父进程的页目录表和页表;

     在进程或者线程切换时cr3指向新的页目录表。


     0x06进程的睡眠等待和唤醒

     Linux中处于等待状态的进程分为两种:可中断的等待状态(TASK_INTERRUPTIBLE)和不可中断的等待状态(TASK_UNINTERRUPTIBLE))。处于可中断等待态的进程可以被信号唤醒,如果收到信号,该进程就从等待状态进入可运行状态,并且加入到运行队列中,等待被调度;而处于不可中断等待态的进程是因为硬件环境不能满足而等待,例如等待特定的系统资源,它任何情况下都不能被打断,只能用特定的方式来唤醒它,例如唤醒函数wake_up()等。参考http://blog.csdn.net/jansonzhe/article/details/47341383

    TASK_INTERRUPTIBLE:

    1、进程状态被设置为TASK_INTERRUPTIBLE,并通过wait_event将当前进程加入得到一个全局的等待队列中。

    2、仅进程状态被设置为TASK_INTERRUPTIBLE

    TASK_UNINTERRUPTIBLE:

    1、进程状态被设置为TASK_UNINTERRUPTIBLE,并通过wait_event将当前进程加入得到一个全局的等待队列中。


    唤醒:

    1、适用于TASK_INTERRUPTIBLE和TASK_UNINTERRUPTIBLE

    当等待队列的条件满足时,通过wake_up函数,将全局等待队列中的目标进程状态设置为TASK_RUNNING,将进程need_resched设置为1,在系统调用返回时会调用schedule来完成一次调度。

    那么wait_event和wait_event_interruptible之间有什么区别呢?

#define __wait_event_interruptible(wq, condition, ret)      \
do {                                                        \
    DEFINE_WAIT(__wait);                                    \
    for (;;) {                                              \
        prepare_to_wait(&wq, &__wait, TASK_INTERRUPTIBLE);  \
        if (condition)                                      \
            break;                                          \
        if (!signal_pending(current)) {                     \
            schedule();                                     \
            continue;                                       \
        }                                                   \
        ret = -ERESTARTSYS;                                 \
        break;                                              \
    }                                                       \
    finish_wait(&wq, &__wait);                              \
} while (0)
#define wait_event(wq, condition)                   
do {                                    
    if (condition) //判断条件是否满足,如果满足则退出等待         
        break;                          
    __wait_event(wq, condition);//如果不满足,则进入__wait_event宏
} while (0)

#define __wait_event(wq, condition)                     
do {    
DEFINE_WAIT(__wait);
/*定义并且初始化等待队列项,后面我们会将这个等待队列项加入我们的等待队列当中,同时在初始化的过程中,会定义func函数的调用函数autoremove_wake_function函数,该函数会调用default_wake_function函数。*/                    

    for (;;) {                          
        prepare_to_wait(&wq, &__wait, TASK_UNINTERRUPTIBLE);    
/*调用prepare_to_wait函数,将等待项加入等待队列当中,并将进程状态置为不可中断TASK_UNINTERRUPTIBLE;*/
        if (condition)  //继续判断条件是否满足                    
            break;                      
        schedule(); //如果不满足,则交出CPU的控制权,使当前进程进入休眠状态                      
    }
    /**如果condition满足,即没有进入休眠状态,跳出了上面的for循环,便会将该等待队列进程设置为可运行状态,并从其所在的等待队列头中删除    */                          
    finish_wait(&wq, &__wait);              
} while (0)                             

    wait_event_interruptible不一定要condition满足,如果是有信号同样可以退出wait_event_interruptible。  


   wake_up只唤醒等待队列中一个进程。

#define wake_up(x)          __wake_up(x, TASK_NORMAL, 1, NULL)  
#define wake_up_nr(x, nr)       __wake_up(x, TASK_NORMAL, nr, NULL)  
#define wake_up_all(x)          __wake_up(x, TASK_NORMAL, 0, NULL)  
#define wake_up_locked(x)       __wake_up_locked((x), TASK_NORMAL)  
  
#define wake_up_interruptible(x)    __wake_up(x, TASK_INTERRUPTIBLE, 1, NULL)  
#define wake_up_interruptible_nr(x, nr) __wake_up(x, TASK_INTERRUPTIBLE, nr, NULL)  
#define wake_up_interruptible_all(x)    __wake_up(x, TASK_INTERRUPTIBLE, 0, NULL)  
#define wake_up_interruptible_sync(x)   __wake_up_sync((x), TASK_INTERRUPTIBLE, 1)  

  



    2、只使用与TASK_INTERRUPTIBLE

    在向目标进程发送信号时,会检查目标进程状态如果是TASK_INTERRUPTIBLE,会设置为TASK_RUNNING,代码如下:

if ((t->state & TASK_INTERRUPTIBLE) && signal_pending(t))  
        wake_up_process(t);//设置进程状态为TASK_RUNNING


    0x07copy_from_user和copy_to_user

    是在内核态直接使用汇编,从用户态拷贝数据到内核,和从内核拷贝数据到用户态。

    原理:内核态有权限可以访问任意虚拟内存地址。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值