前言
转载请注明出处http://www.cnblogs.com/dvd0423/p/4183443.html
内核让人最爽的地方就是它给你站在山上看风景的感觉,一切尽收眼底。就像0号博文说的,不管它有没有用,知其所以然总是好的。
这个系列的内容围绕Linux内核展开,涉及的主要是我做KVM的过程中遇到的部分,网络、调度、KVM等等。虽然是底层的东西但是搞应用的人看一看也没有坏处。我们都知道内核太庞大,要想全了解几乎不可能,所以我们只能根据自己的需要去针对性的学习。而如此庞大的项目却能被组织的有条不紊,层次分明,让一个初学者面对一个庞然大物不至于无从下手,只能感慨人外有人,天外有天啊。其实内核发展到现在,已经有很多深度各异的书籍和文档资料,也有越来越多的人加入到社区中去,你遇到的问题别人都遇到过,所以现在内核已经不是那么难学了。不得不说我们能快快速入手内核,主要还是因为我们站在了巨人的头上,算了还是站到肩膀上吧。
我的这个系列文章介绍内核的同时会介绍《unix高级环境编程》的知识,并结合着我所了解的高层应用去认识内核,这样不管对底层还是对高层都是一种认识的加深。这一篇文章主要介绍系统调用、模块编程和钩子函数。在这里为什么我不从编译安装开始呢?因为我在学校兼职搞集群运维,我现在最讨厌的就是搭建集群环境了(这一部分请自行百度)。
1 系统调用
内核是一间毛胚房,有了系统调用后变成了精装修,而应用程序就是让房子有立体感的家具。所有的应用程序都必须要经过系统调用。虽然有点夸张但听到这句话就知道为什么我先讲系统调用了,它是用户访问内核的入口。高层应用能够创建进程,网络通信,内存操作,读取文件和各种shell命令等都是它的直接功劳。而我们看到的c++/java/pathon等等五花八门的语言库,都是封装了系统调用而已,本质都是一样的。如果我们想和内核打交道又不想深入内核源码,那了解下系统调用大有裨益。比如在java里面
String cmds="java -version"; Process p = Runtime.getRuntime().exec(cmds);
这两个语句会创建进程执行cmds命令,而在Posix C中用fork()/exec()创建新进程。但他们深入到linux内核中都是调用do_fork()。了解虚拟机的朋友都知道有个Hypercall的接口函数,实现原理和Syscall类似,这里不再延伸,将来会讲到的。(这里感谢实验室的一个外号叫“大海”的同学)
对于我们的用户程序使用strace命令可以追踪系统调用。命令格式为:
# strace –o log.txt ./hello
下面我们以open系统调用为例说明其原理。open在内核中函数原型如下:
1 SYSCALL_DEFINE3(open, const char __user *, filename, int, flags, umode_t, mode) 2 { 3 if (force_o_largefile()) 4 flags |= O_LARGEFILE; 5 6 return do_sys_open(AT_FDCWD, filename, flags, mode); 7 }
而在用户空间系统调用函数原型为:
long open(const char *filename, int flags, int mode);
当然也可以用另一种方式调用即syscall()函数,详细使用方法用man命令查找。上面函数等价于:
1 /* 2 #define __NR_restart_syscall 0 3 #define __NR_exit 1 4 #define __NR_fork 2 5 #define __NR_read 3 6 #define __NR_write 4 7 #define __NR_open 5 8 ... 9 */ 10 syscall(num,const char *, filename, int, flags, int, mode); //num是调用号,后面是参数,这里是5
syscall
下面就让我们实现自己的系统调用以此加深对系统调用这个工具的认识。首先要实现自己的系统调用首先要在系统调用表/arch/sh/include/uapi/asm/unistd_64.h中添加调用号,并将总调用数加1。
#define __NR_firstsyscall 380 //添加的部分 #define NR_syscalls 381 //本来为380
其次要在系统调用表syscall_table.s中添加相应的表项。
ENTRY(sys_call_table) .long sys_restart_syscall /* 0 - old "setup()" system call, * used for restarting */ ... .long sys_kcmp .long sys_finit_module /*添加自己的*/ .long sys_firstsyscall /*380*/
第三实现系统调用的具体程序
SYSCALL_DEFINE3(firstsyscall, int, value){ printk("fuckDW"); return value; }
最后在用户空间实现系统调用:
#include <linux/unistd.h> #include <syscall.h> #include <sys/types.h> #include <stdio.h> int main(int argc, char** argv) { printf("%ld\n",syscall(380, 423)); return 0; }
然后最麻烦的事情就是重新编译内核了。
2 模块编程
由于精力有限,我没去了解模块化在linux中的实现原理,我用它主要来提取内核源码中的一些数据结构。这个在后面讲到网络和调度的时候会体现它的作用。要在自己模块中使用内核的参数,首先用EXPORT_SYMBOL(init_net)宏声明让init_net变量可以调用。
下面函数实现了显示所有网络设备的功能,当然我们还可以随便的提取并改变内核中的任意数据结构:
1 /* 2 *init_net是全局变量,模块内可以调用。这里用hello world函数就可以代替get_devs()函数,看不懂不要紧,这不是重点。 3 *printf对应内河中的printk 4 */ 5 #include <linux/init.h> 6 #include <linux/module.h> 7 #include <linux/kernel.h> 8 #include <linux/netdevice.h> 9 #include <net/net_namespace.h> 10 #include <linux/netdevice.h> 11 #include <linux/list.h> 12 13 MODULE_LICENSE("GPL"); //许可声明,要加进来 14 15 16 static int get_devs(void) 17 { 18 struct net_device *a_dev = dev_get_by_name(&init_net, "eth0"); //得到 19 struct list_head *p; 20 struct net_device *temp_dev; 21 int i = 0; 22 23 list_for_each(p, &(a_dev->dev_list)){ 24 temp_dev = list_entry(p, struct net_device, dev_list); 25 26 printk("%d\t%s\n", (++i), temp_dev->name); 27 } 28 dev_put(a_dev); 29 return 0; 30 } 31 //加载模块运行的函数 32 static int __init mode4_init(void) 33 { 34 printk("Module 4 Init!\n"); 35 get_devs(); 36 return 0; 37 } 38 39 static void __exit mode4_exit(void) 40 { 41 printk("Module 4 Exit!\n"); 42 43 } 44 45 module_init(mode4_init);//注册模块 46 module_exit(mode4_exit);
mode4.c
写完代码就编写Makefile文件如下:
1 obj-m += mode4.o 2 PWD:=$(shell pwd) 3 KDIR:=/usr/src/kernels/$(shell uname -r)/ 4 5 all: 6 $(MAKE) -C $(KDIR) M=$(PWD) modules 7 8 clean: 9 $(MAKE) -C $(KDIR) M=$(PWD) clean
Makefile
在命令行里输入如下命令:
# make # insmod mode4.ko //加载模块 # dmesg //显示如下信息,保存在目录/var/log/dmesg中
# rmmod mode4.ko //卸载模块,显示Module 4 Exit! # make clean //清除编译文件
我们还可以给模块传递命令行参数,用module_param()或module_param_array()宏实现。具体使用自己看源码吧。
用模块编程还可以给内核加入一些你想要的功能。模块编程也就是自定义操作系统的开始。
3 钩子函数
这个不好实现,原理大概是用户拦截内核消息,修改后返回内核,内核根据用户的设置选择不同的运行方式。以后在网络模块会提到。很多命令就是用钩子函数实现对操作系统的修改。
今天就写到这里。说实话写到这里又感觉不想写了,主要原因是当我懂了一件事的时候,我再写每一句话都觉得是多余的,都是很显然很简单的废话。但是我想还是有一些同学不懂的,希望我写的能帮到别人。毕竟你苦苦思考了很久,结果别人一句话就帮你解决了问题,那种心情经历过的人都懂。
by 糖球