2019-2020-1 20199309《Linux内核原理与分析》第五周作业

一、系统调用实验:

asm” 表示后面的代码为内嵌汇编,“asm”是“asm”的别名。

volatile” 表示编译器不要优化代码,后面的指令保留原样,“volatile”是它的别名。

一个简单的汇编模板:
代码:

int a=10,b;
asm("movl %1, %%eax;
     movl %%eax, %0;"
    :"=r"(b)          /*输出部*/
    :"r"(a)           /*输入部*/
    :"%eax"           /*毁坏部*/
   );

表示C语言里的“b=a;”。

里边r表示使用任意寄存器,%0、%1表示使用两个寄存器,一般只能%0~%9共十个操作数,按输入输出部变量出现顺序进行映射。

寄存器用两个百分号,是因为使用了%0%1这些数字使百分号有了特殊意义,所以在操作数出现的寄存器必须用双百分表示。

毁坏部里边的%eax表示eax寄存器在汇编代码块执行过程中会被改写,在执行前要保护好,这是提交给编译器决定的。

系统调用的两种实现方法:

本实验我选择的是系统调用号为 38 的 sys_rename(),功能实现设计为将hello.c文件重新命名 newhello.c

路径: /usr/src/linux-headers-4.15.0-64/include/linux

(1)库函数API实现:

运行结果如下:

(2)嵌入汇编代码实现:

执行:

gcc -o rename_asm rename.c -m32

二、实验分析:

(1)库函数API实现:
在 /include/linux/syscalls.h 中找到 sys_rename 函数原型:

asmlinkage long sys_rename(const char __user *oldname,const char __user *newname);

可以看出需要传递两个参数,一个是旧文件名,一个是新文件名。由于是指针类型,所以此时的文件名也代表该文件,也就是说,先找到叫 oldname 的文件,然后把其名改为 newname。rename() 是 glibc 对sys_rename 的封装,用户在用户态调用 rename(),将 oldname和newname 参数传入,系统会产生中断陷入内核态执行sys_rename。当重命名成功时,函数返回0。

(2)嵌入汇编代码实现:

asm volatile("movl %2,%%ecx\n\t"         //newname存入ecx
             "movl %1,%%ebx\n\t"         //oldname存入ebx
             "movl $0x26,%%eax\n\t"      //系统调用号存入eax
             "int $0x80"                 //执行系统调用
             :"=a"(ret)                                     
             :"b"(oldname),"c"(newname)                     
                        );

把系统调用号38(16进制是0x26)存入 eax,将 oldname 存入 ebx,将 newname 存入 ecx,通过执行 int $0x80 来执行系统调用,使应用程序陷入内核态,system_call 根据传入的系统调用号在系统调用列表中查找对应的内核函数,根据 ebx 和 ecx 中保存的参数调用内核函数 sys_rename,执行完后将执行结果存放到 eax 中,最后返回 eax 中的值。

三、小结

如果用户态要涉及内核态的操作,就需要通过系统调用来实现,可以屏蔽平台相关操作,降低了软件开发难度,增强了系统安全性,使程序具有更好的移植性。系统调用过程为:应用程序在用户态调用 API 函数,系统调用号和参数保存到 eax,ebx 等寄存器中,通过 0x80 中断向量触发中断陷入内核态,中断服务程序根据系统调用号调用并执行对应的内核函数,执行完毕后将结果存放的 eax 中并返回给程序,程序返回用户态。

处理器的速度跟外围硬件设备的速度往往不在一个数量级,所以提供中断机制,让硬件需要的时候再向内核发出信号,使得处理器和外部设备可以协同工作。硬件发出的中断是为了通知内核有新的东西等待处理,内核响应中断时会执行中断处理程序,运行于中断上下文。

  中断处理可切为上半部和下半部。重要、紧迫而又与硬件相关的工作必须放在上半部,下半部的实现主要是通过软中断、tasklet、工作队列来实现。上半部的功能是响应中断。上半部完全屏蔽中断,如果它没有执行完,其他中断就不能及时地处理,只能等到这个中断处理程序执行完毕以后。下半部是可中断的,如果其它设备产生了中断,这个下半部可以暂时中断,等到那个设备的上半部运行完了,再运行自己的下半部。request_irq() 注册中断处理程序,free_irq() 释放中断处理程序。上半部将下半部排到它们所负责的设备中断的处理队列中去,不再做其他处理,而下半部几乎完成了中断处理程序所有的事情。

  软中断一般是“可延迟函数”的总称,可延迟函数上可以执行四种操作:初始化、激活、执行、屏蔽。软中断产生后并不是马上执行,必须要等待内核的调度才能执行。软中断不能被自己打断,只能被中断打断(上半部)。软中断可以并发运行在多个CPU上(即使同一类型的也可以)。软中断必须设计为可重入的函数(允许多个CPU同时操作),因此也需要使用自旋锁来保护其数据结构。

  tasklet建立在软中断上。某一段tasklet代码在某个时刻只能在一个CPU上运行,不同的tasklet代码在同一时刻可以在多个CPU上并发地执行。用DECLARE_TASKLET(name,func, data)和DECLARE_TASKLET_DISABLED(name,func, data)静态地创建一个tasklet_struct结构。当该tasklet被调度以后,给定的函数func(处理程序)会被执行,它的参数由data给出,此外还可动态地创建tasklet。tasklet不能睡眠,但可以响应中断。

  软中断不能睡眠、不能阻塞。由于中断上下文出于内核态,没有进程切换,所以如果软中断一旦睡眠或者阻塞,将无法退出这种状态,导致内核会整个僵死,因此要使用工作队列。工作队列可以在进程上下文中执行,工作队列允许被重新调度甚至是睡眠。推后执行的任务叫做工作(work),数据结构为work_struct,这些工作以队列结构组织成工作队列(workqueue),数据结构为workqueue_struct,而工作线程就是负责执行工作队列中的工作。当一个工作者线程被唤醒时,它会执行它的链表上的所有工作。工作被执行完毕,它就将相应的work_struct对象从链表上移去。当链表上不再有对象的时候,它就会继续休眠。

猜你喜欢

转载自www.cnblogs.com/fungi/p/11691401.html