zoukankan      html  css  js  c++  java
  • 2017-2018-1 20179215《Linux内核原理与分析》第六周作业

    一、实验部分:使用gdb跟踪分析一个系统调用内核函数(上周选择的那一个系统调用)。

    【第一部分】 根据要求完成第一部分,步骤如下:

    ①更新menu代码到最新版

    ②在原有代码中加入C函数、汇编函数

     int Getuid(int argc,char *argv[])
    {
        int uid;
        uid=getuid();
        printf("uid=%d
    ",uid);
        return 0;
    }
    
      int GetuidAsm()
    {
        int uid;
        uid=getuid();
        asm volatile(
        "mov $0,%%ebx
    	"
        "mov $0x18,%%eax
    	"
        "int $0x80
    	"
        "mov %%eax,%0
    	"
        :"=m"(uid)
    );
        printf("uid=%d
    ",uid);
        return 0;
    }
    

    ③在main函数中加入getuid以及getuid-asm的makeconfig

    MenuConfig("getuid","Show System User",Getuid);
    MenuConfig("getuid_asm","Show System User(asm)",GetuidAsm);
    

    ④make rootfs

    ⑤可以看到qemu中增加了我们先前添加的命令

    ⑥分别执行新增的命令

    【第二部分】gdb跟踪分析一个系统调用内核函数

    ①进入gdb调试

    ②设置断点,继续执行,得到结果:

    ③查看我所选用的系统调用的函数:

    ④设置断点在sys_getuid16处,发现执行命令getuid时并没有停下:

    ⑤反而在执行getuid_asm时停下了:

    ⑥直接结束若干次单步执行,然后继续往下单步执行,发现出现了进程调度函数,返回了进程调度中的一个当前进程任务的值。


    ⑦设置断点于system_ call处。发现可停,而继续执行时,刚才停下的getuid_asm也返回了值。


    【第三部分】system_call到iret过程

    系统调用在内核代码中的工作机制和初始化

    1.系统调用机制的初始化

    trap_init();
    #ifdef CONFIG_X86_32
    set_ system_trap_ gate(SYSCALL_VECTOR,&system_call);//两个参数分别代表:系统调用的中断向量;汇编代码的入口,一旦执行时int 0x80,系统就跳转至此执行
    set_bit(SYSCALL_VECTOR,used-vectors);
    #endif
    

    2.理解system_call代码

     # system call handler stub
    ENTRY(system_call)
    RING0_INT_FRAME         # can't unwind into user space anyway
    ASM_CLAC
    pushl_cfi %eax          # save orig_eax
    SAVE_ALL                // 保存系统寄存器信息,即保存现场
    GET_THREAD_INFO(%ebp)   // 获取thread_info结构的信息
         # system call tracing in operation / emulation
    testl $_TIF_WORK_SYSCALL_ENTRY,TI_flags(%ebp)  // 测试是否有系统跟踪
    jnz syscall_trace_entry    // 如果有系统跟踪,先执行,然后再回来
    cmpl $(NR_syscalls), %eax  // 比较eax中的系统调用号和最大syscall,超过则无效
    jae syscall_badsys  // 无效的系统调用 直接返回
    
    syscall_call:
    call *sys_call_table(,%eax,4) // 调用实际的系统调用程序
    
    syscall_after_call:
    movl %eax,PT_EAX(%esp)      // 将系统调用的返回值eax存储在栈中
    
    syscall_exit:
    LOCKDEP_SYS_EXIT
    DISABLE_INTERRUPTS(CLBR_ANY)    # make sure we don't miss an interrupt
                    # setting need_resched or sigpending
                    # between sampling and the iret
    TRACE_IRQS_OFF
    movl TI_flags(%ebp), %ecx
    testl $_TIF_ALLWORK_MASK, %ecx  //检测是否所有工作已完成
    jne syscall_exit_work           //工作已经完成,则去进行系统调用推出工作
    
    restore_all:
    TRACE_IRQS_IRET         // iret 从系统调用返回
    

    System_ Call中的关键部分:syscall_ exit_ work

    syscall_exit_work:
    testl $_TIF_WORK_SYSCALL_EXIT, %ecx //测试syscall的工作完成
    jz work_pending
    TRACE_IRQS_ON  //切换中断请求响应追踪可用
    ENABLE_INTERRUPTS(CLBR_ANY) # could let syscall_trace_leave() call
                    //schedule() instead
    movl %esp, %eax
    call syscall_trace_leave //停止追踪系统调用
    jmp resume_userspace //返回用户空间,只需要检查need_resched
    
    END(syscall_exit_work)
    

    该过程为系统调用完成后如何退出调用的过程,其中比较重要的是work_pending,详见如下:

    work_pending:
    testb $_TIF_NEED_RESCHED, %cl  // 判断是否需要调度
    jz work_notifysig   // 不需要则跳转到work_notifysig
    
    work_resched:
    call schedule   // 调度进程
    LOCKDEP_SYS_EXIT
    DISABLE_INTERRUPTS(CLBR_ANY)    # make sure we don't miss an interrupt
                    # setting need_resched or sigpending
                    # between sampling and the iret
    TRACE_IRQS_OFF
    movl TI_flags(%ebp), %ecx
    andl $_TIF_WORK_MASK, %ecx  // 是否所有工作都已经做完
    jz restore_all              // 是则退出
    testb $_TIF_NEED_RESCHED, %cl // 测试是否需要调度
    jnz work_resched            // 重新执行调度代码
    
    work_notifysig:             // 处理未决信号集
    #ifdef CONFIG_VM86
    testl $X86_EFLAGS_VM, PT_EFLAGS(%esp) // 判断是否在虚拟8086模式下
    movl %esp, %eax
    jne work_notifysig_v86      // 返回到内核空间
    1:
    #else
    movl %esp, %eax
    #endif
    
    TRACE_IRQS_ON  // 启动跟踪中断请求响应
    ENABLE_INTERRUPTS(CLBR_NONE)
    movb PT_CS(%esp), %bl
    andb $SEGMENT_RPL_MASK, %bl
    cmpb $USER_RPL, %bl
    jb resume_kernel        // 恢复内核空间
    xorl %edx, %edx
    call do_notify_resume  // 将信号投递到进程
    jmp resume_userspace  // 恢复用户空间
    
    #ifdef CONFIG_VM86
    ALIGN
    
    work_notifysig_v86:
    pushl_cfi %ecx          # save ti_flags for do_notify_resume
    call save_v86_state     // 保存VM86模式下的CPU信息
    popl_cfi %ecx
    movl %eax, %esp
    jmp 1b
    #endif
    END(work_pending)
    

    System_Call的基本处理流程为:

     首先保存中断上下文(SAVE_ALL,也就是CPU状态,包括各个寄存器),判断请求的系统调用是否有效,然后call *sys_call_table(,%eax,4)通过系统查询系统调用查到相应的系统调用程序地址,执行相应的系统调用,系统调用完后,返回系统调用的返回值,关闭中断响应,检测系统调用的所有工作是否已经完成,如果完成则进行syscall_ exit_ work(完成系统调用退出工作),最后restore_all(恢复中断请求响应),返回用户态

     总结:具体的系统调用与系统调用号绑定,然后都记载在一个系统调用表内,每次使用系统调用时都是通过这样的绑定关系,由系统调用号去找系统调用表然后查找到所对应的系统调用的位置。同理,中断处理过程也是一样的,它也是经由中断向量号作为索引去查表,然后执行相应的具体的中断处理程序去处理中断。简而言之就是“两个号&两张表”。

     整体的流程图:

    存在的疑问:
    ###看到在执行work_ pending()后才又重新开启总中断,那么在work_pending函数中判断的是否有被阻塞的信号指什么时候进来的信号,为什么不等返回用户之后再来处理这些信号?

    二、读书笔记

    1、同步

     所谓同步,其实防止在临界区中形成竞争条件。如果临界区里是原子操作(即整个操作完成前不会被打断),那么自然就不会出竞争条件。但在实际应用中,临界区中的代码往往不会那么简单,所以为了保持同步,引入了锁机制。

    2、互斥量与信号量

     互斥量如其名,同一时间只能被一个线程占有,实现线程间对某种数据结构的互斥访问。试图对一个已经加锁的互斥量加锁,会导致线程阻塞。允许多个线程对同一个互斥量加锁。当对互斥量解锁时,阻塞在该互斥量上的线程会被唤醒,它们竞争对该互斥量加锁,加锁成功的线程将停止阻塞,剩余的加锁失败于是继续阻塞。注意到,谁将竞争成功是无法预料的,这一点就类似于弱信号量。(强信号量把阻塞在信号量上的进程按时间排队,先进先出)

     互斥量区别于信号量的地方在于,互斥量只有两种状态,锁定和非锁定。它不像信号量那样可以赋值,甚至可以是负值。共性方面,我所体会到的就一句话,都是用来实现互斥的。

    1、生产者消费者问题

    该问题要满足:

    (1). 当缓冲区已满时,生产者不会继续向其中添加数据

    (2). 当缓冲区为空时,消费者不会从中移走数据

    (3). 要避免忙等待,睡眠和唤醒操作(原语)

    #define N 100                   /*缓冲区个数*/
    typedef int semaphore;          /*信号量是一种特殊的整数类型*/
    semaphore mutex = 1;            /*互斥信号量:控制对临界区的访问*/
    semaphore empty= N;             /*空缓冲区的个数*/
    semaphore full = 0;             /*满缓冲区个数*/
    void producer(void)
    {
        int item;
        while(TRUE)
    {
           item = produce_item()
        P(&empty);
        P(&mutex);
           insert_item(item);
        V(&mutex);
        V(&full);
    
       }
    
    }
    
    void consumer(void)
    {
       int item;
       while(TRUE)
    {
        P(&full);
        P(&mutex);
            item = remove_item()
        P(&mutex);
        V(&empty);
            consume_item(item);
       }
    
    }
    

    2、读者写者问题

     有读者和写者两组并发进程,共享一个文件,当两个或以上的读进程同时访问共享数据时不会产生副作用,但若某个写进程和其他进程(读进程或写进程)同时访问共享数据时则可能导致数据不一致的错误。因此要求:①允许多个读者可以同时对文件执行读操作;②只允许一个写者往文件中写信息;③任一写者在完成写操作之前不允许其他读者或写者工作;④写者执行写操作前,应让已有的读者和写者全部退出。

    (1)读者优先

     读进程是优先的,也就是说,当存在读进程时,写操作将被延迟,并且只要有一个读进程活跃,随后而来的读进程都将被允许访问文件。在这种方式下,会导致写进程可能长时间等待,导致存在写进程“饿死”的情况。

    int count=0; //用于记录当前的读者数量
     semaphore mutex=1; //用于保护更新count变量时的互斥
     semaphore rw=1; //用于保证读者和写者互斥地访问文件
    
     writer () { //写者进程
       while (1){
         P(rw); // 互斥访问共享文件
             Writing; //写入
         V(rw) ; //释放共享文件
         }
     }
     reader () { // 读者进程
       while(1){
         P (mutex) ; //互斥访问count变量
             if (count==0) //当第一个读进程读共享文件时
         P(rw); //阻止写进程写
             count++; //读者计数器加1
         V (mutex) ; //释放互斥变量count
             reading; //读取
         P (mutex) ; //互斥访问count变量
             count--; //读者计数器减1
             if (count==0) //当最后一个读进程读完共享文件
         V(rw) ; //允许写进程写
         V (mutex) ; //释放互斥变量 count
         }
    }
    

    (2)写者优先

     即当有读进程正在读共享文件时,有写进程请求访问,这时应禁止后续读进程的请求,等待到已在共享文件的读进程执行完毕则立即让写进程执行,只有在无写进程执行的情况下才允许读进程再次运行。为此,增加一个信号量并且在上面的程序中 writer()和reader()函数中各增加一对PV操作,就可以得到写进程优先的解决程序。

    int count = 0; //用于记录当前的读者数量
     semaphore mutex = 1; //用于保护更新count变量时的互斥
     semaphore rw=1; //用于保证读者和写者互斥地访问文件
     semaphore w=1; //用于实现“写优先”
    
     writer(){
        while(1){
          P(w); //在无写进程请求时进入
          P(rw); //互斥访问共享文件
              writing; //写入
          V(rw); // 释放共享文件
          V(w) ; //恢复对共享支件的访问
         }
     }
     reader () { //读者进程
        while (1){
          P (w) ; // 在无写进程请求时进入
          P (mutex); // 互斥访问count变量
             if (count==0) //当第一个读进程读共享文件时
          P(rw); //阻止写进程写
             count++; //读者计数器加1
          V (mutex) ; //释放互斥变量count
          V(w); //恢复对共享文件的访问
             reading; //读取
          P (mutex) ; //互斥访问count变量
             count--; //读者计数器减1
             if (count==0) //当最后一个读进程读完共享文件
          V(rw); //允许写进程写
          V (mutex); //释放互斥变量count
        }
     }  
    

    3、死锁

     死锁就是所有线程都在相互等待释放资源,导致谁也无法继续执行下去。比如哲学家进餐问题,当每个人都同时拿起左边的筷子,那么同时每个人都在等待有人放下获取另一支筷子,这时就构成了死锁。可见竞争资源以及推进进程顺序不当会引发死锁。下面一些简单的规则可以帮助我们避免死锁:

    1. 如果有多个锁的话,尽量确保每个线程都是按相同的顺序加锁,按加锁相反的顺序解锁。
    (即加锁a->b->c,解锁c->b->a)

    2. 防止发生饥饿。即设置一个超时时间,防止一直等待下去。

    3. 不要重复请求同一个锁。

    4. 设计应力求简单。加锁的方案越复杂就越容易出现死锁。

     对于解决哲学家进餐问题有两种解决办法:

    (1)同时只允许一位哲学家就餐
    semaphore fork[5]={1,1,1,1,1};
    semaphore mutex = 1;
    void philosopher(int i){
       while(TRUE){
          think();
          P(mutex)
          P(fork[i]);
          P(fork[(i+1)%N);
          V(mutex)
          eat();
          V(fork[i]);
          V(fork[(i+1)%N];
       }
    
    (2)对哲学家顺序编号,要求奇数号哲学家先抓左边的叉子,然后再抓他右边的叉子,而偶数号哲学家刚好相反。
    semaphore fork[5]={1,1,1,1,1};
    void philosopher(int i){
       while(TRUE){
      think();
      if(i%2==1){
         P(fork[i]);
         P(fork[(i+1)%N);
      }else{
         P(fork[(i+1)%N]);
         P(fork[i]);
      }
      eat();
      V(fork[i]);
      V(fork[(i+1)%N];
    }
  • 相关阅读:
    C#面向对象
    C#语句
    C#语言数据类型
    Jupyter Notebook(iPython)
    BeautifulSoup模块
    requests模块
    爬虫基本原理
    版本控制系统
    支付宝支付
    django内置组件——ContentTypes
  • 原文地址:https://www.cnblogs.com/yl-930/p/7784904.html
Copyright © 2011-2022 走看看