一、实验目的

学习Linux内核的系统调用,理解、掌握Linux系统调用的实现框架、用户界面、参数传递、进入/返回过程。阅读Linux内核源代码,通过添加一个简单的系统调用实验,进一步理解Linux操作系统处理系统调用的统一流程。

 

二、实验内容

在现有的系统中添加一个不用传递参数的系统调用。这个系统调用的功能是实现遍历进程。实验主要内容:

l  添加系统调用的名字

l  利用标准C库进行包装

l  添加系统调用号

l  在系统调用表中添加相应表项

l  sys_mysyscall的实现

l  编写用户态测试程序

 

三、主要仪器设备(必填)

Linux环境:utuntu10.10linux内核2.6.36

待编译内核:linux2.6.36

 

四、操作方法和实验步骤

1】下载并部署内核源代码

       此步已经在实验2中完成。

 

2】添加系统调用号

       系统调用号在文件unistd.h里面定义。这个文件在ubuntu10.10下位于/usr/include/asm/unistd_32.h。现在我们在unistd.h中添加我们的系统调用号:__NR_mysyscall,如下所示:

    231 #define __NR_mysyscall                                223      /*添加或修改为mysyscall */

/* 注意:不同版本的内核系统调用号不一样,您可以根据内核版本不同对系统调用号进行修改*/

       添加系统调用号之后,系统才能根据这个号,作为索引,去找syscall­_table中的相应表项。

 

3】在系统调用表中添加或修改相应表项

       我们知道,系统调用处理程序(system_call)会根据eax中的索引到系统调用表(sys_call_table)中寻找相应的表项。所以,我们必须在那里添加我们自己的一个值。

       2.6.36的内核下,只需要修改arch/x86/kernel/syscall_table_32.S。注意,修改该文件首先要切换到root权限,此外使用gedit打开该文件时注意它的扩展名是大写的S

    ……

233         .long sys_mysyscall      /*在对应的位置修改或添加*/

234         .long sys_gettid

235         .long sys_readahead                       /* 225 */

……

 

       到现在为止,系统已经能够正确地找到并且调用sys_mysyscall。剩下的就只有一件事情,那就是sys_mysyscall的实现。

 

4sys_mysyscall的实现

       我们把一小段程序添加在kernel/sys.c里面。在这里,我们并没有在kernel目录下另外添加自己的一个文件,这样做的目的是为了简单,而且不用修改makefile,省去不必要的麻烦。

       mysyscall系统调用实现遍历系统中的所有的进程,并打印每个进程的进程名字,进程标识符,进程的状态和父进程的标识符。

       进程名字、pid、进程状态、父进程的指针在task-struct结构的字段中。在内核中使用printk函数打印有关变量的值。遍历进程可以使用next_task宏,init_task进程为0号进程。

  asmlinkage int sys_mysyscall(void)

{

              //在此处加入遍历进程的代码;

              return 0;

}

 

5】重新编译内核

       一定要重新编译内核。内核编译完成后,重新启动编译后的新内核。

 

6】编写用户态程序

       要测试新添加的系统调用,需要编写一个用户态测试程序(test.c)调用mysyscall系统调用。mysyscall系统调用中printk函数输出的信息在/var/log/message文件中。也可以在shell下用dmesg命令查看。

用户态测试程序可以用如下方法实现

圆角矩形标注: 系统调用号根据实验具体数字而定#include <linux/unistd.h>

# include <sys/syscall.h>

#define __NR_ mysyscall 223

int main()

{

syscall(__NR_mysyscall);    /*syscall(223)  */

//在此加入在屏幕输出每个进程相关信息的代码;

}

l  gcc编译源程序

# gcc –o test test.c

l  运行程序

# ./test

l  shell命令查看遍历进程输出的信息

#dmesg

 

五、实验结果和分析

【1】   ubuntu10.10下位于/usr/include/asm/unistd_32.h。现在我们在unistd.h中添加我们的系统调用号:__NR_mysyscall,如下图

 

231 #define __NR_mysyscall                     223   

 

 

 

2】在系统调用表中添加相应表项,即修改arch/x86/kernel/syscall_table_32.S。如下图

 

3sys_mysyscall的实现,我们把一小段程序添加在kernel/sys.c里面,如下图



其中task是进程结构指针,task->comm是进程名,task->pid是进程idtask->state是进程状态,task->parent->pid是进程的父进程id

 

4】重新编译内核。成功后,重启。此时,在启动项中有2.6.362.6.36old两个选项,其中新的内核是2.6.36。选择它并进入系统。至此,我们已经成功添加了一个自己的系统调用。

 

5】编写用户态程序test.c,代码如下

#include <stdio.h>

#include <stdlib.h>

#include <sys/types.h>

#include <sys/stat.h>

#include <fcntl.h>

#include <time.h>

#include <string.h>

#define __NR_mysyscall 223

int main()

{

syscall(223); //系统调用

time_t mytime;

char temp[40]; //缓冲区

char m_time[16]; //存放所需要的特定格式的当前时间

time(&mytime); //得到当前时间

strcpy(temp,ctime(&mytime));//把某种格式的当前时间的内容存入缓冲区

int i=0;

//对当前时间格式化使之与messages文件中时间格式对应

while(i<15)

{

m_time[i]=temp[i+4]; //从第4个字符开始复制

i++;

}

FILE *fp;

char ch2[16];

char mm;

fp=fopen("/var/log/messages","r"); //以流的方式打开文件

int flag=0;

while(!feof(fp))

{

mm=fgetc(fp);

if(mm=='\n' && flag==0)

{

fgets(ch2,16,fp); //得到某行的前16个字符,即时间

if(strncmp(ch2,m_time,15)==0) //判断是否与当前时间相同

{//如果messages中时间为当前时间则输出

fseek(fp,-15,SEEK_CUR);

flag=1;

}

}

if(flag==1 && mm!=EOF)

printf("%c",mm);

}

fclose(fp);

return 0;

}

    详细的注释见代码

 

    程序运行后得到的截图如下

 



       在终端输入dmesg后得到的截图如下


 

       使用gedit查看/var/log/message文件,截图如下

 


六、讨论、心得

1、编译过一次内核后,由于.o文件都在存在,所以第二次编译时间非常快,本次实验,编译只用了10分钟左右。

2、添加一个系统调用类似于MFC中添加一个自定义的消息,首先要注册这个消息,以便系统知道有这么个消息,然后用户在程序中才能使用它。

4、在2.6.36中,有unistd.hunistd_32.hunistd_64.h,其实unistd.h中的内容只有几句代码,用来判断要使用unistd_32.h还是unistd_64.h。所以,平时我们在编写程序时引入头文件unistd.h,其实是引入了unistd_32.h(前提是你的机器是32位的,64位的同理)。

3、在编写test.c时,通过搜索互联网、查看c语言的相关书籍以及和同学的探讨,深入理解和运用了相关的流文件函数,包括fopenfgetcfgetsfseek等。对c的流文件操作是这次实验收获最大的,尤其是文件指针的定位。