今 年二月上旬,Yahoo、eBay、CNN.com、Amazon、Buy.com和E*Trade等著名商业网站连续遭到黑客攻击,造成了数以十亿美元 的损失,向世人再一次敲响了网络并不安全的警钟。防火墙作为一种网络或系统之间强制实行访问控制的机制,是确保网络安全的重要手段。目前社会上各种商业产 品的防火墙非常多,功能也大都很强。我们暂且不管这些防火墙产品的价格如何,由于它们在开发设计过程中注重的是产品的通用性、兼容性,考虑更多的是市场和 利润,因此对于某些特殊的应用就不一定很合适。如果用户能根据自己的实际需要,将防火墙设计的一般理论和方法与自己系统的具体实践相结合,设计一些小而 精、精而强的防火墙程序,则往往可以发挥出比花大价钱买来的通用型防火墙更好的作用。
由于篇幅所限,本文不可能对防火墙的一般理论和结构进行深入的讨论,因此仅以Linux系统为例,具体说明防火墙程序的设计方法。
一、 从程序设计角度看Linux网络
编写防火墙程序并不一定要求对Linux网络内核有多么深刻的理解,只是需要明白在网络内核中有这样一种机制,那就是内核可以自动调用用户编写的防火墙程序,并根据这个防火墙程序返回的结果来决定对网络收发数据报的处理策略。这一点可以从图1中看出。
二、 怎样将自己编写的防火墙程序登记到内核中
我们已经知道内核在网络层中自动调用用户编写的防火墙程序。但有一个前提条件就是用户必须正确地将自己编写的防火墙程序登记到内核中。关于Linux内核驱动程序的编写方法,可参见本刊第四期中《Linux设备驱动程序设计实例》一文。
内核中提供了防火墙的登记和卸载函数,分别是register_firewall和unregister_firewall,参见firewall.c。
1、 register_firewall
函数原型如下:
int register_firewall(int pf,struct firewall_ops *fw)
返回值:0代表成功,小于0表示不成功。
参数:
* 协议标志pf,主要的取值及其代表的协议如下:
2代表Ipv4协议,4代表IPX协议,10代表Ipv6协议等。
* 参数结构fw定义如下:
struct firewall_ops{
struct firewall_ops *next;
int (*fw_forward)(struct firewall_ops *this, int pf,
struct device *dev, void *phdr, void *arg, struct sk_buff **pskb);
int (*fw_input)(struct firewall_ops *this, int pf,
struct device *dev, void *phdr, void *arg, struct sk_buff **pskb);
int (*fw_output)(struct firewall_ops *this, int pf,
struct device *dev, void *phdr, void *arg, struct sk_buff **pskb);
int fw_pf;
int fw_priority;
};
结构中next的域将由内核来修改,使其指向下一个防火墙模块。
fw_pf域为协议标志,含义同上。
fw_priority指定优先级,一般应大于0。
fw_input、fw_output、fw_forward是用户编写的防火墙函数模块,在接收到网络报和发送网络报时内核将调用这些模块,后面将详细讨论。
2、 unregister_firewall
unregister_firewall的原型说明与调用方法同register_firewall。
三、 防火墙函数模块的设计
1、 防火墙函数模块的返回值
返回值是至关重要的,内核将根据它来决定对网络数据报采取的处理策略。主要返回值及意义如下:
0和1 通知内核忽略该网络报。
-1 通知内核忽略该网络报,并发送不可达到的网络控制报(ICMP报文)。
2 通知内核认可该网络报。
2、 各模块函数的入口参数
* 参数this
指向register_firewall中的fw参数结构。
* 参数pf
含义同register_firewall中的pf参数。
* 参数dev
dev是指向数据结构device的指针。在Linux系统中,每一个网络设备都是用device数据结构来描述的。在系统引导期间,网络设备驱动程序向 Linux登记设备信息,如设备名、设备的I/O基地址、设备中断号、网卡的48位硬件地址等,device数据结构中包括这些设备信息以及设备服务函数 的地址。关于device结构的详细信息可参见netdevice.h头文件。
* 参数phdr
该参数指向链路层数据报报头首址。
* 参数arg
利用这个参数可以向内核传递信息,如重定向时的端口号。
* 参数pskb
此参数是指向sk_buff结构指针的指针。在Linux中,所有网络数据的发送和接收都用sk_buff数据结构表示。在sk_buff数据结构中包含 有对应设备结构的device地址、传输层、网络层、链路层协议头地址等。关于sk_buff的定义可参见skbuff.h头文件。
3、防火墙程序示例
下面给出一个简单防火墙程序。在这里假设读者对以太协议、IP协议、TCP协议等常用协议有一定的了解。用命令行"gcc -Wall -O2 -c MyFirewall.c"进行编译,再用insmod命令加载程序后,系统将只响应外部网络用TCP协议的80端口所进行的访问。要让系统恢复原有功 能,则可用rmmod命令卸载该程序,源代码见网站http://www.pccomputing.com.cn上的同名文章。/
// MyFirewall.c 2000年3月7日编写
#ifndef __KERNEL__
# define __KERNEL__ //按内核模块编译
#endif
#ifndef MODULE
# define MODULE //按设备驱动程序模块编译
#endif
#include //最基本的内核模块头文件
#include
#include //最基本的内核模块头文件
#include
#include
#include
#include
#include
#include
#include
#include
#define SOL_ICMP 1
#define PERMIT_PORT 80 //只允许访问TCP的80端口
int zzl_input(struct firewall_ops *this,int pf,struct device *dev,
void *phdr,void *arg,struct sk_buff **pskb)
{//每当收到一个网络报时,此函数将被内核调用
struct tcphdr *tcph; //TCP的头指针
struct iphdr *iph; //IP头指针
struct sk_buff *skb=*pskb;
if (skb->protocol==htons(ETH_P_ARP)){
printk("/nPermit a ARP Packet");
return FW_ACCEPT;//允许地址解析协议报
}
if(skb->protocol==htons(ETH_P_RARP)){
printk("/nPermit a RARP Packet");
return FW_ACCEPT;//允许反向地址解析协议报
}
if(skb->protocol==htons(ETH_P_IP))
{
iph=skb->nh.iph;
if (iph->protocol==SOL_ICMP)
{
printk("/nPermit a ICMP Packet");
return FW_ACCEPT;//允许网络控制报
}
if(iph->protocol==SOL_TCP){
tcph=skb->h.th;
if(tcph->dest==PERMIT_PORT){
printk("/nPermit a valid access");
return FW_ACCEPT;//允许对TCP端口80的访问
}
}
}
return FW_REJECT;//禁止对本计算机的所有其它访问
}
int zzl_output(struct firewall_ops *this,int pf,struct device *dev,
void *phdr,void *arg,struct sk_buff **pskb)
{//程序编写方法同zzl_input函数模块
printk("/nzzl_output is called ");
return FW_SKIP;
}
int zzl_foreward(struct firewall_ops *this,int pf,struct device *dev,
void *phdr,void *arg,struct sk_buff **pskb)
{//程序编写方法同zzl_input函数模块
printk("/nzzl_foreward is called ");
return FW_SKIP;
}
struct firewall_ops zzl_ops=
{
NULL,
zzl_foreward,
zzl_input,
zzl_output,
PF_INET,
01
};
int init_module(void)
{
if(register_firewall(PF_INET,&zzl_ops)!=0)
{
printk("/nunable register firewall");
return -1;
}
printk("/nzzl_ops=%p",&zzl_ops);
return 0;
}
void cleanup_module(void)
{
printk("unload/n");
unregister_firewall(PF_INET,&zzl_ops);
}
新一篇: 一位项目经理经验总结 | 旧一篇: 解析Linux中的VFS文件系统机制Linux系统中,设备驱动程序是操作系统内核的重要组成部分,在与硬件设备之间建立了标准的抽象接口。通过这个接 口,用户可以像处理普通文件一样,对硬件设备进行打开(open)、关闭(close)、读写(read/write)等操作。通过分析和设计设备驱动程 序,可以深入理解Linux系统和进行系统开发。本文通过一个简单的例子来说明设备驱动程序的设计。
1、程序清单
#ifndef __KERNEL__
# define __KERNEL__ //按内核模块编译
#endif
#ifndef MODULE
# define MODULE //设备驱动程序模块编译
#endif
#define DEVICE_NAME "MyDev"
#define OPENSPK 1
#define CLOSESPK 2
//必要的头文件
#include //同kernel.h,最基本的内核模块头文件
#include //同module.h,最基本的内核模块头文件
#include //这里包含了进行正确性检查的宏
#include //文件系统所必需的头文件
#include //这里包含了内核空间与用户空间进行数据交换时的
函数宏
#include //I/O访问
int my_major=0; //主设备号
static int Device_Open=0;
static char Message[]="This is from device driver";
char *Message_Ptr;
int my_open(struct inode *inode, struct file *file)
{//每当应用程序用open打开设备时,此函数被调用
printk ("ndevice_open(%p,%p)n", inode, file);
if (Device_Open)
return -EBUSY; //同时只能由一个应用程序打开
Device_Open++;
MOD_INC_USE_COUNT; //设备打开期间禁止卸载
return 0;
}
static void my_release(struct inode *inode, struct file *file)
{//每当应用程序用close关闭设备时,此函数被调用
printk ("ndevice_release(%p,%p)n", inode, file);
Device_Open --;
MOD_DEC_USE_COUNT; //引用计数减1
}
ssize_t my_read (struct file *f,char *buf,int size,loff_t off)
{//每当应用程序用read访问设备时,此函数被调用
int bytes_read=0;
#ifdef DEBUG
printk("nmy_read is called. User buffer is %p,size is dn",buf,size);
#endif
if (verify_area(VERIFY_WRITE,buf,size)==-EFAULT)
return -EFAULT;
Message_Ptr=Message;
while(size && *Message_Ptr)
{
if(put_user(*(Message_Ptr++),buf++)) //写数据到用户空间
return -EINVAL;
size --;
bytes_read++;
}
return bytes_read;
}
ssize_t my_write (struct file *f,const char *buf, int size,loff_t off)
{//每当应用程序用write访问设备时,此函数被调用
int i;
unsigned char uc;
#ifdef DEBUG
printk("nmy_write is called. User buffer is %p,size is dn",buf,size);
#endif
if (verify_area(VERIFY_WRITE,buf,size)==-EFAULT)
return -EFAULT;
printk("nData below is from user program:n");
for (i=0;i if(!get_user(uc,buf++)) //从用户空间读数据
printk("%02x ",uc);
return size;
}
int my_ioctl(struct inode *inod,struct file *f,unsigned int arg1,
unsigned int arg2)
{//每当应用程序用ioctl访问设备时,此函数被调用
#ifdef DEBUG
printk("nmy_ioctl is called. Parameter is %p,size is %dn",arg1);
#endif
switch (arg1)
{
case OPENSPK:
printk("nNow,open PC's speaker.n");
outb(inb(0x61)|3,0x61); //打开计算机的扬声器
break;
case CLOSESPK:
printk("nNow,close PC's speaker.");
outb(inb(0x61)&0xfc,0x61);//关闭计算机的扬声器
break;
}
}
struct file_operations my_fops = {
NULL, /* lseek */
my_read,
my_write,
NULL,
NULL,
my_ioctl,
NULL,
my_open,
my_release,
/* nothing more, fill with NULLs */
};
int init_module(void)
{//每当装配设备驱动程序时,系统自动调用此函数
int result;
result = register_chrdev(my_major,DEVICE_NAME,&my_fops);
if (result < 0) return result;
if (my_major == 0)
my_major = result;
printk("nRegister Ok. major-number=%dn",result);
return 0;
}
void cleanup_module(void)
{//每当卸载设备驱动程序时,系统自动调用此函数
printk("nunloadn");
unregister_chrdev(my_major, DEVICE_NAME);
}
2、设备驱动程序设计
Linux设备分为字符设备、块设备和网络设备。字符设备是不需要缓冲而直接
读写的设备,如串口、键盘、鼠标等,本例就是字符设备驱动程序;块设备的访问
通常需要缓冲来支持,以数据块为单位来读写,如磁盘设备等;网络设备是通过套
接字来访问的特殊设备。
1) 设备驱动程序和内核与应用程序的接口
无论哪种类型的设备,Linux都是通过在内核中维护特殊的设备控制块来与设
备驱动程序接口的。在字符设备和块设备的控制块中,有一个重要的数据结构
file_operations,该结构中包含了驱动程序提供给应用程序访问硬件设备的各种
方法,其定义如下(参见fs.h):
struct file_operations {
loff_t (*llseek) (struct file *, loff_t, int);
//响应应用程序中lseek调用的函数指针
ssize_t (*read) (struct file *, char *, size_t, loff_t *);
//响应应用程序中read调用的函数指针
ssize_t (*write) (struct file *, const char *, size_t, loff_t *);
//响应应用程序中write调用的函数指针
int (*readdir) (struct file *, void *, filldir_t);
//响应应用程序中readdir调用的函数指针
unsigned int (*poll) (struct file *, struct poll_table_struct *);
//响应应用程序中select调用的函数指针
int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);
//响应应用程序中ioctl调用的函数指针
int (*mmap) (struct file *, struct vm_area_struct *);
//响应应用程序中mmap调用的函数指针
int (*open) (struct inode *, struct file *);
//响应应用程序中open调用的函数指针
int (*flush) (struct file *);
int (*release) (struct inode *, struct file *);
//响应应用程序中close调用的函数指针
int (*fsync) (struct file *, struct dentry *);
int (*fasync) (int, struct file *, int);
int (*check_media_change) (kdev_t dev);
int (*revalidate) (kdev_t dev);
int (*lock) (struct file *, int, struct file_lock *);
};
多数情况下,只需为上面结构中的少数方法编写服务函数,其他均设为NULL即可。
每一个可装配的设备驱动程序都必须有init_module和cleanup_module两个函数,装载和卸载设备时内核自动调用 这两个函数。在init_module中,除了可以对硬件设备进行检查和初始化外,还必须调用register_* 函数将设备登记到系统中。本例中是通过register_chrdev来登记的,如果是块设备或网络设备则应该用
register_blkdev和register_netdev来登记。Register_chrdev 的主要功能是将设
备名和结构file_operations登记到系统的设备控制块中。
2) 与应用程序的数据交换
由于设备驱动程序工作在内核存储空间,不能简单地用"="、"memcpy"等方法与应用程序交换数据。在头文件uaccess.h中定义了方法put_user(x, ptr)和
get_user(x, ptr),用于内核空间与用户空间的数据交换。值x的类型根据指针
ptr的类型确定,请参见源代码中的my_read与my_write函数。
3) 与硬件设备的接口
Linux中为设备驱动程序访问I/O端口、硬件中断和DMA提供了简便方法,相应的头文件分别为io.h、irq.h、 dma.h。由于篇辐限制,本例中只涉及到I/O端口访问。Linux提供的I/O端口访问方法主要有:inb()、inw()、outb()、outw ()、inb_p()、inw_p()、outb_p()、outw_p()等。要注意的是,设备驱动程序在使用端口前,应该先用 check_region()检查该端口的占用情况,如果指定的端口可用,则再用request_region()向系统登记。说明 check_region()、request_region()的头文件是ioport.h。
4) 内存分配
设备驱动程序作为内核的一部分,不能使用虚拟内存,必须利用内核提供的
kmalloc()与kfree()来申请和释放内核存储空间。Kmalloc()带两个参数,第一个
要申请的是内存数量,在早期的版本中,这个数量必须是2的整数幂,如128、256
。关于kmalloc()与kfree()的用法,可参考内核源程序中的malloc.h与slab.c程序
。
3、程序的编译和访问
本例在Linux 2.2.x.x中可以成功编译和运行。先用下面的命令行进行编译:
gcc -Wall -O2 -c MyDev.c该命令行中参数-Wall告诉编译程序显示警告信息;参数-O2是关于代码优化的设置,注意内核模块必须优化;参数-c规定只进行编译和汇 编,不进行连接。正确编译后,形成MyDev.o文件。可以输入命insmod MyDev.o来装载此程序。如果装配成功,则显示信息:Register Ok.major-number=xx。利用命令lsmod可以看到该模块被装配到系统中。为了访问该模块,应该用命令mknode来创建设备文件。下面 的应用程序可以对创建的设备文件进行访问。
#include >
#include
#include
#define DEVICE_NAME MyDev
#define OPENSPK 1
#define CLOSESPK 2
char buf[128];
int main(){
int f=open(DEVICE_NAME,O_RDRW);
if (f==-1) return 1;
printf("nHit enter key to read device...");
read(f,buf,128); printf(buf);
printf("nHit enter key to write device ...");
write(f,"test",4);
printf("nHit enter key to open PC's speaker...");
ioctl(f,OPENSPK);
printf("nHit enter key to close PC's speaker...");
ioctl(f,CLOSESPK);
close(f);
}
由于篇幅所限,本文不可能对防火墙的一般理论和结构进行深入的讨论,因此仅以Linux系统为例,具体说明防火墙程序的设计方法。
一、 从程序设计角度看Linux网络
编写防火墙程序并不一定要求对Linux网络内核有多么深刻的理解,只是需要明白在网络内核中有这样一种机制,那就是内核可以自动调用用户编写的防火墙程序,并根据这个防火墙程序返回的结果来决定对网络收发数据报的处理策略。这一点可以从图1中看出。
二、 怎样将自己编写的防火墙程序登记到内核中
我们已经知道内核在网络层中自动调用用户编写的防火墙程序。但有一个前提条件就是用户必须正确地将自己编写的防火墙程序登记到内核中。关于Linux内核驱动程序的编写方法,可参见本刊第四期中《Linux设备驱动程序设计实例》一文。
内核中提供了防火墙的登记和卸载函数,分别是register_firewall和unregister_firewall,参见firewall.c。
1、 register_firewall
函数原型如下:
int register_firewall(int pf,struct firewall_ops *fw)
返回值:0代表成功,小于0表示不成功。
参数:
* 协议标志pf,主要的取值及其代表的协议如下:
2代表Ipv4协议,4代表IPX协议,10代表Ipv6协议等。
* 参数结构fw定义如下:
struct firewall_ops{
struct firewall_ops *next;
int (*fw_forward)(struct firewall_ops *this, int pf,
struct device *dev, void *phdr, void *arg, struct sk_buff **pskb);
int (*fw_input)(struct firewall_ops *this, int pf,
struct device *dev, void *phdr, void *arg, struct sk_buff **pskb);
int (*fw_output)(struct firewall_ops *this, int pf,
struct device *dev, void *phdr, void *arg, struct sk_buff **pskb);
int fw_pf;
int fw_priority;
};
结构中next的域将由内核来修改,使其指向下一个防火墙模块。
fw_pf域为协议标志,含义同上。
fw_priority指定优先级,一般应大于0。
fw_input、fw_output、fw_forward是用户编写的防火墙函数模块,在接收到网络报和发送网络报时内核将调用这些模块,后面将详细讨论。
2、 unregister_firewall
unregister_firewall的原型说明与调用方法同register_firewall。
三、 防火墙函数模块的设计
1、 防火墙函数模块的返回值
返回值是至关重要的,内核将根据它来决定对网络数据报采取的处理策略。主要返回值及意义如下:
0和1 通知内核忽略该网络报。
-1 通知内核忽略该网络报,并发送不可达到的网络控制报(ICMP报文)。
2 通知内核认可该网络报。
2、 各模块函数的入口参数
* 参数this
指向register_firewall中的fw参数结构。
* 参数pf
含义同register_firewall中的pf参数。
* 参数dev
dev是指向数据结构device的指针。在Linux系统中,每一个网络设备都是用device数据结构来描述的。在系统引导期间,网络设备驱动程序向 Linux登记设备信息,如设备名、设备的I/O基地址、设备中断号、网卡的48位硬件地址等,device数据结构中包括这些设备信息以及设备服务函数 的地址。关于device结构的详细信息可参见netdevice.h头文件。
* 参数phdr
该参数指向链路层数据报报头首址。
* 参数arg
利用这个参数可以向内核传递信息,如重定向时的端口号。
* 参数pskb
此参数是指向sk_buff结构指针的指针。在Linux中,所有网络数据的发送和接收都用sk_buff数据结构表示。在sk_buff数据结构中包含 有对应设备结构的device地址、传输层、网络层、链路层协议头地址等。关于sk_buff的定义可参见skbuff.h头文件。
3、防火墙程序示例
下面给出一个简单防火墙程序。在这里假设读者对以太协议、IP协议、TCP协议等常用协议有一定的了解。用命令行"gcc -Wall -O2 -c MyFirewall.c"进行编译,再用insmod命令加载程序后,系统将只响应外部网络用TCP协议的80端口所进行的访问。要让系统恢复原有功 能,则可用rmmod命令卸载该程序,源代码见网站http://www.pccomputing.com.cn上的同名文章。/
// MyFirewall.c 2000年3月7日编写
#ifndef __KERNEL__
# define __KERNEL__ //按内核模块编译
#endif
#ifndef MODULE
# define MODULE //按设备驱动程序模块编译
#endif
#include //最基本的内核模块头文件
#include
#include //最基本的内核模块头文件
#include
#include
#include
#include
#include
#include
#include
#include
#define SOL_ICMP 1
#define PERMIT_PORT 80 //只允许访问TCP的80端口
int zzl_input(struct firewall_ops *this,int pf,struct device *dev,
void *phdr,void *arg,struct sk_buff **pskb)
{//每当收到一个网络报时,此函数将被内核调用
struct tcphdr *tcph; //TCP的头指针
struct iphdr *iph; //IP头指针
struct sk_buff *skb=*pskb;
if (skb->protocol==htons(ETH_P_ARP)){
printk("/nPermit a ARP Packet");
return FW_ACCEPT;//允许地址解析协议报
}
if(skb->protocol==htons(ETH_P_RARP)){
printk("/nPermit a RARP Packet");
return FW_ACCEPT;//允许反向地址解析协议报
}
if(skb->protocol==htons(ETH_P_IP))
{
iph=skb->nh.iph;
if (iph->protocol==SOL_ICMP)
{
printk("/nPermit a ICMP Packet");
return FW_ACCEPT;//允许网络控制报
}
if(iph->protocol==SOL_TCP){
tcph=skb->h.th;
if(tcph->dest==PERMIT_PORT){
printk("/nPermit a valid access");
return FW_ACCEPT;//允许对TCP端口80的访问
}
}
}
return FW_REJECT;//禁止对本计算机的所有其它访问
}
int zzl_output(struct firewall_ops *this,int pf,struct device *dev,
void *phdr,void *arg,struct sk_buff **pskb)
{//程序编写方法同zzl_input函数模块
printk("/nzzl_output is called ");
return FW_SKIP;
}
int zzl_foreward(struct firewall_ops *this,int pf,struct device *dev,
void *phdr,void *arg,struct sk_buff **pskb)
{//程序编写方法同zzl_input函数模块
printk("/nzzl_foreward is called ");
return FW_SKIP;
}
struct firewall_ops zzl_ops=
{
NULL,
zzl_foreward,
zzl_input,
zzl_output,
PF_INET,
01
};
int init_module(void)
{
if(register_firewall(PF_INET,&zzl_ops)!=0)
{
printk("/nunable register firewall");
return -1;
}
printk("/nzzl_ops=%p",&zzl_ops);
return 0;
}
void cleanup_module(void)
{
printk("unload/n");
unregister_firewall(PF_INET,&zzl_ops);
}
新一篇: 一位项目经理经验总结 | 旧一篇: 解析Linux中的VFS文件系统机制Linux系统中,设备驱动程序是操作系统内核的重要组成部分,在与硬件设备之间建立了标准的抽象接口。通过这个接 口,用户可以像处理普通文件一样,对硬件设备进行打开(open)、关闭(close)、读写(read/write)等操作。通过分析和设计设备驱动程 序,可以深入理解Linux系统和进行系统开发。本文通过一个简单的例子来说明设备驱动程序的设计。
1、程序清单
#ifndef __KERNEL__
# define __KERNEL__ //按内核模块编译
#endif
#ifndef MODULE
# define MODULE //设备驱动程序模块编译
#endif
#define DEVICE_NAME "MyDev"
#define OPENSPK 1
#define CLOSESPK 2
//必要的头文件
#include //同kernel.h,最基本的内核模块头文件
#include //同module.h,最基本的内核模块头文件
#include //这里包含了进行正确性检查的宏
#include //文件系统所必需的头文件
#include //这里包含了内核空间与用户空间进行数据交换时的
函数宏
#include //I/O访问
int my_major=0; //主设备号
static int Device_Open=0;
static char Message[]="This is from device driver";
char *Message_Ptr;
int my_open(struct inode *inode, struct file *file)
{//每当应用程序用open打开设备时,此函数被调用
printk ("ndevice_open(%p,%p)n", inode, file);
if (Device_Open)
return -EBUSY; //同时只能由一个应用程序打开
Device_Open++;
MOD_INC_USE_COUNT; //设备打开期间禁止卸载
return 0;
}
static void my_release(struct inode *inode, struct file *file)
{//每当应用程序用close关闭设备时,此函数被调用
printk ("ndevice_release(%p,%p)n", inode, file);
Device_Open --;
MOD_DEC_USE_COUNT; //引用计数减1
}
ssize_t my_read (struct file *f,char *buf,int size,loff_t off)
{//每当应用程序用read访问设备时,此函数被调用
int bytes_read=0;
#ifdef DEBUG
printk("nmy_read is called. User buffer is %p,size is dn",buf,size);
#endif
if (verify_area(VERIFY_WRITE,buf,size)==-EFAULT)
return -EFAULT;
Message_Ptr=Message;
while(size && *Message_Ptr)
{
if(put_user(*(Message_Ptr++),buf++)) //写数据到用户空间
return -EINVAL;
size --;
bytes_read++;
}
return bytes_read;
}
ssize_t my_write (struct file *f,const char *buf, int size,loff_t off)
{//每当应用程序用write访问设备时,此函数被调用
int i;
unsigned char uc;
#ifdef DEBUG
printk("nmy_write is called. User buffer is %p,size is dn",buf,size);
#endif
if (verify_area(VERIFY_WRITE,buf,size)==-EFAULT)
return -EFAULT;
printk("nData below is from user program:n");
for (i=0;i if(!get_user(uc,buf++)) //从用户空间读数据
printk("%02x ",uc);
return size;
}
int my_ioctl(struct inode *inod,struct file *f,unsigned int arg1,
unsigned int arg2)
{//每当应用程序用ioctl访问设备时,此函数被调用
#ifdef DEBUG
printk("nmy_ioctl is called. Parameter is %p,size is %dn",arg1);
#endif
switch (arg1)
{
case OPENSPK:
printk("nNow,open PC's speaker.n");
outb(inb(0x61)|3,0x61); //打开计算机的扬声器
break;
case CLOSESPK:
printk("nNow,close PC's speaker.");
outb(inb(0x61)&0xfc,0x61);//关闭计算机的扬声器
break;
}
}
struct file_operations my_fops = {
NULL, /* lseek */
my_read,
my_write,
NULL,
NULL,
my_ioctl,
NULL,
my_open,
my_release,
/* nothing more, fill with NULLs */
};
int init_module(void)
{//每当装配设备驱动程序时,系统自动调用此函数
int result;
result = register_chrdev(my_major,DEVICE_NAME,&my_fops);
if (result < 0) return result;
if (my_major == 0)
my_major = result;
printk("nRegister Ok. major-number=%dn",result);
return 0;
}
void cleanup_module(void)
{//每当卸载设备驱动程序时,系统自动调用此函数
printk("nunloadn");
unregister_chrdev(my_major, DEVICE_NAME);
}
2、设备驱动程序设计
Linux设备分为字符设备、块设备和网络设备。字符设备是不需要缓冲而直接
读写的设备,如串口、键盘、鼠标等,本例就是字符设备驱动程序;块设备的访问
通常需要缓冲来支持,以数据块为单位来读写,如磁盘设备等;网络设备是通过套
接字来访问的特殊设备。
1) 设备驱动程序和内核与应用程序的接口
无论哪种类型的设备,Linux都是通过在内核中维护特殊的设备控制块来与设
备驱动程序接口的。在字符设备和块设备的控制块中,有一个重要的数据结构
file_operations,该结构中包含了驱动程序提供给应用程序访问硬件设备的各种
方法,其定义如下(参见fs.h):
struct file_operations {
loff_t (*llseek) (struct file *, loff_t, int);
//响应应用程序中lseek调用的函数指针
ssize_t (*read) (struct file *, char *, size_t, loff_t *);
//响应应用程序中read调用的函数指针
ssize_t (*write) (struct file *, const char *, size_t, loff_t *);
//响应应用程序中write调用的函数指针
int (*readdir) (struct file *, void *, filldir_t);
//响应应用程序中readdir调用的函数指针
unsigned int (*poll) (struct file *, struct poll_table_struct *);
//响应应用程序中select调用的函数指针
int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);
//响应应用程序中ioctl调用的函数指针
int (*mmap) (struct file *, struct vm_area_struct *);
//响应应用程序中mmap调用的函数指针
int (*open) (struct inode *, struct file *);
//响应应用程序中open调用的函数指针
int (*flush) (struct file *);
int (*release) (struct inode *, struct file *);
//响应应用程序中close调用的函数指针
int (*fsync) (struct file *, struct dentry *);
int (*fasync) (int, struct file *, int);
int (*check_media_change) (kdev_t dev);
int (*revalidate) (kdev_t dev);
int (*lock) (struct file *, int, struct file_lock *);
};
多数情况下,只需为上面结构中的少数方法编写服务函数,其他均设为NULL即可。
每一个可装配的设备驱动程序都必须有init_module和cleanup_module两个函数,装载和卸载设备时内核自动调用 这两个函数。在init_module中,除了可以对硬件设备进行检查和初始化外,还必须调用register_* 函数将设备登记到系统中。本例中是通过register_chrdev来登记的,如果是块设备或网络设备则应该用
register_blkdev和register_netdev来登记。Register_chrdev 的主要功能是将设
备名和结构file_operations登记到系统的设备控制块中。
2) 与应用程序的数据交换
由于设备驱动程序工作在内核存储空间,不能简单地用"="、"memcpy"等方法与应用程序交换数据。在头文件uaccess.h中定义了方法put_user(x, ptr)和
get_user(x, ptr),用于内核空间与用户空间的数据交换。值x的类型根据指针
ptr的类型确定,请参见源代码中的my_read与my_write函数。
3) 与硬件设备的接口
Linux中为设备驱动程序访问I/O端口、硬件中断和DMA提供了简便方法,相应的头文件分别为io.h、irq.h、 dma.h。由于篇辐限制,本例中只涉及到I/O端口访问。Linux提供的I/O端口访问方法主要有:inb()、inw()、outb()、outw ()、inb_p()、inw_p()、outb_p()、outw_p()等。要注意的是,设备驱动程序在使用端口前,应该先用 check_region()检查该端口的占用情况,如果指定的端口可用,则再用request_region()向系统登记。说明 check_region()、request_region()的头文件是ioport.h。
4) 内存分配
设备驱动程序作为内核的一部分,不能使用虚拟内存,必须利用内核提供的
kmalloc()与kfree()来申请和释放内核存储空间。Kmalloc()带两个参数,第一个
要申请的是内存数量,在早期的版本中,这个数量必须是2的整数幂,如128、256
。关于kmalloc()与kfree()的用法,可参考内核源程序中的malloc.h与slab.c程序
。
3、程序的编译和访问
本例在Linux 2.2.x.x中可以成功编译和运行。先用下面的命令行进行编译:
gcc -Wall -O2 -c MyDev.c该命令行中参数-Wall告诉编译程序显示警告信息;参数-O2是关于代码优化的设置,注意内核模块必须优化;参数-c规定只进行编译和汇 编,不进行连接。正确编译后,形成MyDev.o文件。可以输入命insmod MyDev.o来装载此程序。如果装配成功,则显示信息:Register Ok.major-number=xx。利用命令lsmod可以看到该模块被装配到系统中。为了访问该模块,应该用命令mknode来创建设备文件。下面 的应用程序可以对创建的设备文件进行访问。
#include >
#include
#include
#define DEVICE_NAME MyDev
#define OPENSPK 1
#define CLOSESPK 2
char buf[128];
int main(){
int f=open(DEVICE_NAME,O_RDRW);
if (f==-1) return 1;
printf("nHit enter key to read device...");
read(f,buf,128); printf(buf);
printf("nHit enter key to write device ...");
write(f,"test",4);
printf("nHit enter key to open PC's speaker...");
ioctl(f,OPENSPK);
printf("nHit enter key to close PC's speaker...");
ioctl(f,CLOSESPK);
close(f);
}