实验
给MenuOS增加time和time-asm命令的方法:
- 更新menu代码到最新版
- 再main()函数中增加MenuConfig
- 增加对应的Time函数和TimeAsm函数(这里的函数要换成我们自己编写的使用系统调用的函数,比如mkdir和mkdirAsm)
- make rootfs (帮我们自动编译自动生成根文件系统,自动帮我们启动起来menuos)
接下来我要使用gdb跟踪分析一个系统调用内核函数(mkdir)
这次我实验所用的系统调用仍然是是mkdir
首先,我们需要把上周做的两个实验加入到我们的MenuOS中,变成MenuOS中的两个命令。如图:
方法是要将函数加入到test.c中(因为实验楼中的clone无法使用),加入命令为:
int Mkdir()
{
int flag;
flag = mkdir("/home/shiyanlou/testdir");
if (flag == -1)
printf("mkdir failed!
");
else
printf("make dirctory success!
");
return 0;
}
int MkdirAsm()
{
int flag;
char *dir = "/home/shiyanlou/testdir2";
asm volatile(
"movl $0x27,%%eax
"
"movl %1,%%ebx
"
"int $0x80
"
"movl %%eax,%0
"
:"=m"(flag)
:"c"(dir)
);
if(flag==0)
printf("mkdir success!
");
else
printf("mkdir failed!
");
return 0;
}
在main函数中加入:
MenuConfig("mkdir","Make A Directory",Mkdir);
MenuConfig("mkdir-asm","Make A Directory(asm)",MkdirAsm);
make rootfs就可以自动生成,输入help可以看到mkdir,如下结果:
下面开始用gdb调试:
cd LinuxKernel
qemu -kernel linux-3.18.6/arch/x86/boot/bzImage -initrd rootfs.img -s -S
水平分割
gdb
file linux-3.18.6/vmlinux //加载内核
target remote:1234 //链接到menu os里
b start_kernel //在start_kernel处设置断点
c //继续执行,到start_kernel 停下来
list //查看startkernel这段代码
b sys_mkdir //在我们要分析的这个系统调用处设置断点
c //继续执行
c
c
启动menuos,输入mkdir,看gdb调试结果
如果我们换用视频里的sys_time设置断点进行调试,之前的方法与mkdir类似,然后可以继续跟踪调试如下:
b sys_time
c
c
c
list
s //单步执行
s //单步执行
finish //函数执行完
s
s
n
n
这边代码就不大好调试了,如果我们再设置一个断点,当我们用int 0x80时,cpu自动跳转到system_call这个函数,下面我们直接设置断点system_call,是否能停下来呢?
b system_call
c
发现time返回了。在menuos中输入time—asm,停在system_time的位置,system_call并不能停下。老师说这里的system_call他不是一个正常的函数,它是一段特殊的汇编代码,可能gdb对他不支持,我们只能调试系统调用函数和对应的内核函数,调试内核函数的处理过程,但不能跟踪entry_32.s这个汇编代码,在system_call处并没有停下来。system_call在entry_32.s中,可以找到ENTRY(system_call),gdb可以发现一个函数原型,实际上不是函数,是汇编代码的起点,gdb现在还不能跟踪它。
关于从system_call到iret之间的过程
画了一个大致的流程图,如下:
首先保护现场(SAVE_ALL:保存需要用到的寄存器数据);退出恢复现场(RESTORE_ALL:退出中断程序,恢复保存寄存器的数据)。
下面为SAVE_ALL的实现:
.macro SAVE_ALL
cld
PUSH_GS
pushl_cfi %fs
/*CFI_REL_OFFSET fs, 0;*/
pushl_cfi %es
/*CFI_REL_OFFSET es, 0;*/
pushl_cfi %ds
/*CFI_REL_OFFSET ds, 0;*/
pushl_cfi %eax
CFI_REL_OFFSET eax, 0
pushl_cfi %ebp
CFI_REL_OFFSET ebp, 0
pushl_cfi %edi
CFI_REL_OFFSET edi, 0
pushl_cfi %esi
CFI_REL_OFFSET esi, 0
pushl_cfi %edx
CFI_REL_OFFSET edx, 0
pushl_cfi %ecx
CFI_REL_OFFSET ecx, 0
pushl_cfi %ebx
CFI_REL_OFFSET ebx, 0
movl $(__USER_DS), %edx
movl %edx, %ds
movl %edx, %es
movl $(__KERNEL_PERCPU), %edx
movl %edx, %fs
SET_KERNEL_GS %edx
.endm
在这段代码中,保存了相关寄存器的值。 他们依次是:ES,DS,EAX,EBP,EDI,ESI,EDX,ECX,EBX等等。从这里寄存器的顺序可以知道压栈的最后压入的是ebx,这里压入的栈是内核栈。
遇到的问题
- 在使用实验楼clone的时候出现问题,后将test.c文件更改进行解决
- 实验楼一直很卡,试验了好多次最后在MenuOs上输入的mkdir命令上还是卡住了
书上的内容整理
- 内核同步介绍:留意保护共享资源,防止共享资源并发访问,发生各个线程之间相互覆盖共享数据的情况,造成访问数据不一致;临界区和竞争条件,临界区:访问和操作共享数据的代码段。 竞争条件:两个执行线程处于同一个临界区中;内核各个部分都会调用两个函数,一个函数将新请求添加到队列尾部,另一个函数从队列头删除请求,然后处理它。
- 锁的使用是自愿的、非强制的,它完全属于一种编程者自选的编程手段;内核中造成并发的原因:(1)中断:任何时刻异步发生,打断当前执行的代码(2)软中断和tasklet:任何时刻唤醒或调度软中断、tasklet(3)内核抢占(preempt)(4)睡眠及与用户空间的同步:唤醒调度程序,调度新进程执行(5)对称多处理器(SMP);编程需注意的问题: (1)数据是否全局?除了当前线程,其他线程是否可以访问? (2)数据是否在进程/中断上下文中共享?是否在两个不同中断中共享?(3)进程在访问数据时可否被抢占?被调度的新进程是否会访问同一数据? (4)当前进程是否会睡眠(阻塞)在某些资源上?共享数据处于何种状态? (5)怎样防止数据失控?;加锁:按顺序加锁。使用嵌套锁必须以相同顺序获取锁;防止发生饥饿; 不要重复请求同一个锁; 设计力求简单;建议以获取锁相反的顺序来释放锁。
- 原子操作:原子操作执行过程不被打断,原子操作接口分为整数操作接口和单独位操作接口。
- 自旋锁:自旋锁最多只能被一个可执行线程持有。若一个线程试图获得一个被征用的自旋锁,线程会一直忙循环、选择、等待锁可用。缺点:由于自旋锁在等待时自旋(浪费处理器时间),因此自旋锁不应长时间持有。优点:线程不用睡眠,不用进行上下文切换。自旋锁可以使用在中断处理程序中。对于软中断,无论是否同种类型,如果数据被软中断共享,那么它必须得到锁的保护。读写自旋锁:读写不同锁,多人和可并发持有读者锁,写锁只能被一个任务持有。
- 信号量特点: Linux中的信号量是一种睡眠锁; 信号量适用于锁会被长期占有的情况;
由于信号量会睡眠,所以只能用于进程上下文中,中断上下文不支持调度;可以在持有信号量时睡眠; 不可以在持有信号量时使用自旋锁; 信号量允许任意数目的锁持有者,自旋锁一个时刻只允许一个任务持有;在声明时课指定信号量拥有的持有者数量。 - 禁止抢占:内核代码使用自旋锁作为非抢占区域的标记。preempt_disable() 增加抢占计数值,从而禁止内核抢占;preempt_enable() 减少抢占计数值,当减为0时检查和执行被挂起的任务; preempt_count() 返回抢占计数。