一、实验目的
学习Linux内核的系统调用,理解、掌握Linux系统调用的实现框架、用户界面、参数传递、进入/返回过程。阅读Linux内核源代码,通过添加一个简单的系统调用实验,进一步理解Linux操作系统处理系统调用的统一流程。
二、实验内容
在现有的系统中添加一个不用传递参数的系统调用。这个系统调用的功能是实现遍历进程。实验主要内容:
l 添加系统调用的名字
l 利用标准C库进行包装
l 添加系统调用号
l 在系统调用表中添加相应表项
l sys_mysyscall的实现
l 编写用户态测试程序
三、主要仪器设备(必填)
Linux环境:utuntu10.10,linux内核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的实现。
【4】sys_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。如下图
【3】sys_mysyscall的实现,我们把一小段程序添加在kernel/sys.c里面,如下图
其中task是进程结构指针,task->comm是进程名,task->pid是进程id,task->state是进程状态,task->parent->pid是进程的父进程id。
【4】重新编译内核。成功后,重启。此时,在启动项中有2.6.36和2.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.h,unistd_32.h,unistd_64.h,其实unistd.h中的内容只有几句代码,用来判断要使用unistd_32.h还是unistd_64.h。所以,平时我们在编写程序时引入头文件unistd.h,其实是引入了unistd_32.h(前提是你的机器是32位的,64位的同理)。
3、在编写test.c时,通过搜索互联网、查看c语言的相关书籍以及和同学的探讨,深入理解和运用了相关的流文件函数,包括fopen,fgetc,fgets,fseek等。对c的流文件操作是这次实验收获最大的,尤其是文件指针的定位。