2017-2018-1 20179215《Linux内核原理与分析》第六周作业

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

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

①更新menu代码到最新版

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

 int Getuid(int argc,char *argv[])
{
    int uid;
    uid=getuid();
    printf("uid=%d\n",uid);
    return 0;
}

  int GetuidAsm()
{
    int uid;
    uid=getuid();
    asm volatile(
    "mov $0,%%ebx\n\t"
    "mov $0x18,%%eax\n\t"
    "int $0x80\n\t"
    "mov %%eax,%0\n\t"
    :"=m"(uid)
);
    printf("uid=%d\n",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];
}
posted @ 2017-11-04 22:33  20179215袁琳  阅读(233)  评论(0编辑  收藏  举报