1、 Linux串口上网程序的整个内核部分是以LKM(Loadable Kernel Module)形式实现的。LKM加载的时候完成伪网络设备、发送字符设备、接收字符设备的初始化和注册。注册的目的是让操作系统可以识别用户进程所要操作的设备,并完成在其上的操作(比如read,write等系统调用)。Linux加载模块,实际上就是模块链表的插入;删除模块象是模块链表成员的删除。 初始化内核模块入口函数init_module()中包括对字符设备的初始化入口 函数eddev_module_init()和伪网络设备初始化入口函数ednet_module_init()。 在内核需要卸载的时候,必须进行资源释放
2、以及设备注销, cleanup_module()完成这个任务。函数cleanup_module()中用eddev_module_cleanup()来释放字符设备占用的资源(比如分配的缓存区等);有ednet_module_cleanup()来释放伪网络设备占用的资源。本文的内核部分模块程序编译后就是ed_device.o,加载后使用lsmod命令查看,模块名就是ed_device。模块ed_device的加载和注销函数。图 2 当我们需要加载模块的时候,我们只需要使用insmod命令,如果需要卸载模块,我们使用rmmod命令。比如加载ed_device模块,并且配置伪网络接口IP地址为192.
3、168.5.1 rootlocalhost testinsmod ed_device.o, rootlocalhost testifconfig ed0 192.168.5.1 up 这时可以在/proc/net/dev 文件中看到有ed0伪网络设备了。如果需要卸载ed_device模块,应先停止其网络数据发送和接收工作,然后卸载模块: rootlocalhost testifconfig ed0 down rootlocalhost testrmmod ed_device如果我们设置另一台Linux box的伪网接口地址是192.168.5.2那么,我们可以用串口线直接连接两台终端并使用网络
4、应用程序了,在两台终端上运行server守护程序,然后执行telnet: rootlocalhost test# telnet 192.168.5.2 Trying 192.168.5.2. Connected to 192.168.5.2 (192.168.5.2). Escape character is '&. Red Hat Linux release 9 (Shrike) Kernel 2.4.20-8 on an i686 login: 编写字符设备驱动程序用户空间的进程主要通过两种方式和内核空间模块打交道,一种是使用proc文件系统,另一种是使用字符设备。本文所描述的两个
5、字符设备sending device 和receiving device事实上是内核空间和用户空间交换数据的缓存区,编写字符设备驱动实际上就是编写用户空间读写字符设备所需要的内核设备操作函数。 在头文件中,我们定义ED_REC_DEVICE为receiving device,名字是ed_rec;定义ED_TX_DEVICE为sending device,名字是ed_tx。 #define MAJOR_NUM_REC 200#define MAJOR_NUM_TX 201#define IOCTL_SET_BUSY _IOWR(MAJOR_NUM_TX,1,int) 200和201分别代表rec
6、eiving device 和 sending device的主设备号。在内核空间,驱动程序是根据主、次设备号识别设备的,而不是设备名;本文的字符设备的次设备号都是0,主设备号是用户定义的且不能和系统已有的设备的主设备有冲突。IOCTL_SET_BUSY _IOWR(MAJOR_NUM_TX,1,int)是ioctl的操作函数定义(从用户空间发送命令到内核空间),主要作用是使得每次在同一时间,同一字符设备上,只可进行一次操作。我们可以使用mknod来建立这两个字符设备 rootlocalhost#mknod c 200 0 /dev/ed_recrootlocalhost#mknod c 20
7、1 0 /dev/ed_tx 设备建立后,编译好的模块就可以动态加载了:rootlocalhost#insmod ed_device.o 为了方便对设备编程,我们还需要一个字符设备管理的数据结构: struct ed_device int magic; char name8; int busy; unsigned char *buffer; #ifdef LINUX_24wait_queue_head_t rwait;#endif int mtu; spinlock_t lock; int data_len; int buffer_size; struct file *file; ssize_
8、t (*kernel_write)(const char *buffer,size_t length,int buffer_size); 这个数据结构是用来保存字符设备的一些基本状态信息。ssize_t (*kernel_write)(const char *buffer,size_t length,int buffer_size) 是一个指向函数的指针,它的作用是为伪网络驱动程序提供写字符设备数据的系统调用接口。magic字段主要是标志设备类型号的,这里没有别的特殊意义;busy字段用来说明字符设备是否是处于忙状态,buffer指向内核缓存区,用来存放读写数据;mtu保存当前可发送的网络数据
9、包最大传输单位,以字节为单位;lock的类型是自旋锁类型spinlock_t,它实际以一个整数域作为锁,在同一时刻对同一字符设备,只能有一个操作,所以使用内核锁机制保护防止数据污染;data_len是当前缓存区内保存的数据实际大小,以字节为单位;file是指向设备文件结构struct file的一个指针,其作用主要是定位设备的私有数据 file-> private_data。定义字符设备struct ed_device ed2,其中edED_REC_DEVICE就是receving device,edED_TX_DEVICE就是sending device。如果sending devic
10、e ED_TX_DEVICE没有数据,用户空间的read调用将被阻塞,并把进程信息放于rwait队列中。当有数据的时候,kernel_write()中的wake_up_interruptible()将唤醒等待进程。kernel_write()函数定义如下: ssize_t kernel_write(const char *buffer,size_t length,int buffer_size) if(length & buffer_size ) length = buffer_size; memset(edED_TX_DEVICE.buffer,0,buffer_size); memcpy(
11、edED_TX_DEVICE.buffer,buffer,buffer_size); edED_TX_DEVICE.tx_len = length; wake_up_interruptible(&edED_TX_DEVICE.rwait); #endif return length; 字符设备的操作及其相关函数调用过程。 图 3 当ed_device模块被加载的时候,eddev_module_init()调用register_chrdev()内核API注册ed_tx和ed_rec两个字符设备。这个函数定义在: int register_chdev(unsigned int major,
12、 const char *, struct fle_operations *fops) 字符设备被注册成功后,内核把这两个字符设备加入到内核字符设备驱动表中。内核字符设备驱动表保留指向struct file_operations的一个数据指针。用户进程调用设备读写操作时,通过这个指针访问设备的操作函数, struct file_operations中的域大部分是指向函数的函数指针,指向用户自己编写的设备操作函数。 struct file_operations ed_ops =#ifdef LINUX_24 NULL, device_read, device_write, device_ioct
13、l, device_open, device_release, 注意到Linux2.4.x和Linux2.2.x内核中定义的struct file_operations是不一样的。device_read()、device_write()、device_ioctl()、device_open()、device_release()就是需要用户自己定义的函数操作了,这几个函数是最基本的操作,如果需要设备驱动程序完成更复杂的任务,还必须编写其他struct file_operations中定义的操作。eddev_module_init()除了注册设备及其操作外,它还有初始化字符设备结构struct e
14、d_device,分配内核缓存区所需要的空间的作用。在内核空间,分配内存空间的API函数是kmalloc()。 下面介绍一下字符设备的主要操作例程device_open()、device_release()、device_read()、devie_write()。字符设备文件操作结构ed_ops中定义的指向以上函数的函数指针的原形: device_open: int(*open)(struct inode *,struct file *) device_release: int (*release) (struct inode *, struct file *); device_read: s
15、size_t (*read) (struct file *, char *, size_t, loff_t *); device_write: ssize_t (*write) (struct file *, const char *, size_t, loff_t *);操作int device_open(struct inode *inode,struct file *file)是设备节点上的第一个操作,如果多个设备共享这一个操作函数,必须区分设备的设备号。我们使用inode-&i_rdev & 8 语句获得设备的主设备号,本文中的接收设备主设备号是200,发送设备号是201。每个字符设备
16、的file&private_data指向打开设备时候使用的file结构,private_data实际上可以指向用户定义的任何结构,这里只指向我们自己定义的struct ed_device,用来保存字符设备的一些基本信息,比如设备名、内核缓存区等。 操作ssize_t device_read(struct file *file,char *buffer,size_t length, loff_t *offset)是读取设备数据的操作。device_read()结构。 图4 从设备中读取数据(用户空间调用read()系统调用)的时候,需要从内核空间把数据拷贝到用户空间,copy_to_user()
17、可完成此功能,它和memcpy()此类函数有本质的区别,memcpy()不能完成不同用户空间数据的交换。如果需要数据临界区的保护,使用spin_lock()内核API负责加锁,spin_unlock()负责解锁,防止数据污染。由于串口守候进程server需要不断轮询设备,以查询是否有数据可读,如果用户进程不处于休眠状态,在用户空间查看进程使用资源情况,发现server占用了很多CPU资源。所以我们改进device_read(),使之在内核中轮询,当发现当前设备没有数据可读取,那么就阻塞用户进程,使用内核API add_wait_queue()可完成此功能,这时候用户进程并没有占用很多CPU资源
18、,而是处于休眠状态。当内核发现有数据可读的时候,调用remove_wait_queue()即可唤醒等待进程,这段 代码如下: DECLARE_WAITQUEUE(wait,current); add_wait_queue(&edp-&rwait,&wait); for(;) set_current_state(TASK_INTERRUPTIBLE); if ( file-&f_flags & O_NONBLOCK) break; /*其他代码 */ if ( signal_pending(current) schedule(); set_current_state(TASK_RUNNING);
19、remove_wait_queue(& 操作ssize_t device_write(struct file *file,const char *buffer, size_t length,loff_t *offset)向设备写入数据。拷贝数据的copy_from_user()和copy_to_user()的功能恰恰相反,它是从用户空间拷贝数据到内核空间,。 图 5 编写伪网络设备驱动程序 伪网络驱动程序和字符设备驱动程序一样,也必须初始化和注册。网络驱动需记录其发送和接收数据量的统计信息,所以我们定义一个记录这些信息的数据结构。struct ednet_priv struct net_dev
20、ice_stats stats;#else struct enet_statistics stats; struct sk_buff *skb; struct ednet_priv只有3个数据成员。Linux2.4.x 使用的网络数据状态统计结构是struct net_device_stats,而Linux 2.2.x则使用的是struct enet_statistics。同样,对控制网络接口设备的设备结构也有不同的定义:Linux2.4.x使用的是struct net_device,而Linux2.2.x却是struct device。struct net_device ednet_dev;
21、struct device ednet_dev; 伪网络驱动程序的也需要初始化和注册。和字符设备的注册不同之处是,它使用的是register_netdev(net_device *) kernel API。int ednet_module_init(void) int err; strcpy(ednet_dev.name, &ed0&); ednet_dev.init = ednet_init; if ( (err = register_netdev(&ednet_dev) ) printk(&ednet: error %i registering pseudo network device
22、&%s&n&, err, ednet_dev.name); return err; ednet_dev的name域是接口名,ednet_module_init()中赋予网络接口的名字为ed0,如果本网络设备被加载,使用ifconfig命令可以看到ed0。rootlocalhost pku# /sbin/ifconfiged0 Link encap:Ethernet HWaddr 00:45:44:30:30 inet addr:192.168.3.9 Bcast:192.168.3.255 Mask:255.255.255.0 UP BROADCAST RUNNING NOARP MULTIC
23、AST MTU:1500 Metric:1 RX packets:0 errors:0 dropped:0 overruns:0 frame: TX packets:0 carrier: collisions:0 txqueuelen:100 RX bytes:0 (0.0 b) TX bytes:0 (0.0 b) 我们看到我们的伪网络接口没有Interrupt和Base address,这是因为这个伪网络接口不和硬件打交道,也没有分配中断号和IO基址。否则,如果你看一个实实在在的网络接口(如下面的eth1),可以看到它的Interrupt号是11和IO Base address是0xa00
24、0。eth1 Link encap:Ethernet HWaddr 50:78:4C:43:1D:01192.168.21.202 Bcast:192.168.21.255 Mask: UP BROADCAST RUNNING MULTICAST MTU:356523 errors:266 errors:21542043 (20.5 Mb) TX bytes:19510 (19.0 Kb) Interrupt:11 Base address:0xa000 ednet_dev的init域是一个函数指针,指向用户定义的ednet_init()例程。ednet_init()添充net_device结
25、构,只有ednet_init()初始化成功后,系统才被加入到设备链表中。ednet_dev的初始化例程ednet_init()如下:int ednet_init(struct net_device *dev)int ednet_init(struct device *dev) ether_setup(dev); dev-&open = ednet_open;stop = ednet_release;hard_start_xmit = ednet_tx;get_stats = ednet_stats;change_mtu = ednet_change_mtu;hard_header = ednet_header;rebuild_header = ednet_rebuild_header;tx_timeout = ednet_tx_timeout;watchdog_timeo = timeout; /* We do not need the ARP protocol. */flags |= IFF_NOARP;#ifndef LINUX_20 hard_header_cache = NULL;#ifdef LINUX_
copyright@ 2008-2022 冰豆网网站版权所有
经营许可证编号:鄂ICP备2022015515号-1