小默

ldd3读书笔记

Google 笔记本
ldd3读书笔记

最近编辑过的 2011年1月3日 
Q:implicit declaration of function 'NIPQUAD'
A:


 Using NIPQUAD() with NIPQUAD_FMT, %d.%d.%d.%d or %u.%u.%u.%u
can be replaced with %pI4

-		dprintf("SRC: %u.%u.%u.%u. Mask: %u.%u.%u.%u. Target: %u.%u.%u.%u.%s\n",
-			NIPQUAD(src_ipaddr),
-			NIPQUAD(arpinfo->smsk.s_addr),
-			NIPQUAD(arpinfo->src.s_addr),
+		dprintf("SRC: %pI4. Mask: %pI4. Target: %pI4.%s\n",
+			&src_ipaddr,
+			&arpinfo->smsk.s_addr,
+			&arpinfo->src.s_addr,
 			arpinfo->invflags & ARPT_INV_SRCIP ? " (INV)" : "");



网卡驱动分析

---
1 找到网卡对应驱动模块,测试

用lshw查看网卡对应的驱动。我的是pcnet32。
 *-network
                description: Ethernet interface
                product: 79c970 [PCnet32 LANCE]
                vendor: Hynix Semiconductor (Hyundai Electronics)
                physical id: 1
                bus info: pci@0000:02:01.0
                logical name: eth1
                version: 10
                serial: 00:0c:29:b5:16:0d
                width: 32 bits
                clock: 33MHz
                capabilities: bus_master rom ethernet physical logical
                configuration: broadcast=yes driver=pcnet32 driverversion=1.35 ip=192.168.159.132 latency=64 link=yes maxlatency=255 mingnt=6 multicast=yes
                resources: irq:19 ioport:2000(size=128) memory:dc400000-dc40ffff

lsmod看到对应模块:
$lsmod

到 /usr/src/kernels/linux-2.6.36/drivers/net 下,ls可以看到 pcnet32.c 和对应编译好的.ko模块。
$ ls | grep pcnet32

rmmod右上角网络标识图成了叉叉。
insmod pcnet32.ko 网络标识图重新连上。
(难道每次手动停止/启动网络连接,实际就是调用了卸载/加载模块?囧)

--
2  尝试编译驱动
把pcnet32.c拷贝出来。
扫了眼net下面的Makefile,300行,吓死我鸟。
然后写了个4行的Makefile,居然编译插入运行一些正常,囧翻了。。。

--
3  分析代码

--
4  分析在网络模块堆叠中的位置,数据流向

--
5  还没有想好。。改下代码?
2.6.20内核以后的skbuff.h头文件中将struct sk_buff结构体修改了,h中包含有传输层的报文头,nh中包含有网络层的报文头,而mac中包含的是链路层的报文头。
linux-2.6.20以后的内核头文件sk_buff.h中这三个成员提取到单独的变量对应关系如下:
h-->transport_header;
nh-->network_header;
mac-->mac_header;
struct net


以前查过一次,还有一点印象
struct net是一个网络名字空间namespace,在不同的名字空间里面可以有自己的转发信息库,有自己的一套net_device等等。
默认情况下都是使用init_net这个全局变量
Linux 系统内核空间与用户空间通信的实现与分析
https://www.ibm.com/developerworks/cn/linux/l-netlink/

推荐使用netlink套接字实现中断环境和用户态进程通信。

CPU运行状态有4种:处理硬中断;处理软中断;运行于内核态,但有进程上下文;运行于用户态进程。
前三种状态,CPU运行于内核空间。

当CPU运行进程上下文时,可以被阻塞。这时可以通过 copy_from_user()/copy_to_user() 实现内核态和用户态数据的拷贝。

当CPU运行于硬/软中断环境下时,不可以被阻塞。这时有2种方法实现和用户态的通信。
第1种方法:用一个和进程上下文相关的内核线程,完成从用户态接收数据的任务;再通过一个临界区将数据传给中断过程。缺点:中断过程不能实时接收来自用户态进程的数据。
第2中方法:使用netlink套接字。用一个软中断调用用户实现指定的接收函数;再通过临界区将数据传给中断过程。因为使用软中断来接收数据,所以可以保证数据接收的实时性。
Q:软中断不可以被阻塞,怎么从用户空间接收数据。

netlink套接字用法:


--

IMP2

--Makefile

obj-m += imp2_k.o
KDIR := /lib/modules/$(shell uname -r)/build

all:
make -C $(KDIR) M=$(shell pwd) modules
gcc -o imp2_u imp2_u.c
clean:
make -C $(DIR) M=$(shell pwd) clean
rm -f *odule* imp2_k

--imp2.h

#ifndef _IMP2_H_
#define _IMP2_H_

#define IMP2_U_PID 0  /*用户空间进程ID*/
#define IMP2_K_MSG 1  /*内核发出的消息*/
#define IMP2_CLOSE 2  /*用户空间进程关闭*/

#define NL_IMP2  31  /*自定义的netlink协议类型*/

/*nlmsghdr 后的数据*/
struct packet_info{
__u32 src;
__u32 dest;
};

#endif

--imp2_u.c

#include <unistd.h>
#include <stdio.h>
#include <linux/types.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <asm/types.h>
#include <linux/netlink.h>
#include <signal.h>
#include "imp2.h"

struct msg_to_kernel{
struct nlmsghdr hdr;
};

struct u_packet_info{
struct nlmsghdr hdr;
struct packet_info icmp_info;
};

static int skfd;

/*信号SIGINT的处理函数-告诉内核用户端关闭*/
static void sig_int(int signo)
{
struct sockaddr_nl kpeer;
struct msg_to_kernel message;

/*内核端地址*/
memset(&kpeer, 0, sizeof(kpeer));
kpeer.nl_family = AF_NETLINK;
kpeer.nl_pid = 0;
kpeer.nl_groups = 0;

/*要传递的消息*/
memset(&message, 0, sizeof(message));
message.hdr.nlmsg_len = NLMSG_LENGTH(0);
message.hdr.nlmsg_flags = 0;
message.hdr.nlmsg_type = IMP2_CLOSE;
message.hdr.nlmsg_pid = getpid();

/*sendto - send a message on a socket*/
sendto(skfd, /*int sockfd*/
   &message,  /*const void *buf*/
   &message.hdr.nlmsg_len,  /*size_t len*/
   0,  /*int flags*/
   (struct sockaddr *)(&kpeer), /*const struct sockaddr *dest_addr*/
   sizeof(kpeer)); /*socklen_t addrlen*/

close(skfd);

exit(0);
}

int main(void)
{
struct sockaddr_nl local;
struct sockaddr_nl kpeer;
int kpeerlen;
struct msg_to_kernel message;
struct u_packet_info info;
int sendlen = 0;
int rcvlen = 0;
struct in_addr addr;

/*创建套接字,设置用户端地址,绑定套接字和地址*/
skfd = socket(PF_NETLINK, SOCK_RAW, NL_IMP2);
/*netlink套接字,用户层端地址*/
local.nl_family = AF_NETLINK;
local.nl_pid = getpid();
local.nl_groups = 0;
if(bind(skfd, (struct sockaddr *)&local, sizeof(local)) != 0){
printf("bind() error\n");
return -1;
}

/*设置SIGINT信号的处理函数为sig_int*/
signal(SIGINT, sig_int);

/*netlink套接字,内核端地址*/
memset(&kpeer, 0, sizeof(kpeer));
kpeer.nl_family = AF_NETLINK;
kpeer.nl_pid = 0;
kpeer.nl_groups = 0;

/*把用户端进程的pid传递给内核*/
memset(&message, 0, sizeof(message));
message.hdr.nlmsg_len = NLMSG_LENGTH(0);
message.hdr.nlmsg_flags = 0;
message.hdr.nlmsg_type = IMP2_U_PID;
message.hdr.nlmsg_pid = local.nl_pid;
sendto(skfd, &message, message.hdr.nlmsg_len, 0,
(struct sockaddr *)&kpeer, sizeof(kpeer));

/*循环接收内核发来的数据*/
while(1){
kpeerlen = sizeof(struct sockaddr_nl);
rcvlen = recvfrom(skfd, &info, sizeof(struct u_packet_info),
0, (struct sockaddr *)&kpeer, &kpeerlen);

addr.s_addr = info.icmp_info.src;
printf("src: %s, ", inet_ntoa(addr));
addr.s_addr = info.icmp_info.dest;
printf("dest: %s\n", inet_ntoa(addr));
}

return 0;
}

-- imp2_k.c

/* 从netfilter的NF_IP_PRE_ROUTING点截获ICMP数据报,记录源地址和目录地址
 * 使用netlink套接字将信息传递给用户进程,再由用户进程打印到终端上
 */
 
#ifndef __KERNEL__
#define __KERNEL__
#endif

#ifndef MODULE
#define MODULE
#endif

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/types.h>
#include <linux/netdevice.h>
#include <linux/skbuff.h>
#include <linux/netfilter_ipv4.h>
#include <linux/inet.h>
#include <linux/in.h>
#include <linux/ip.h>
#include <linux/netlink.h>
#include <linux/spinlock.h>
#include <linux/semaphore.h>
#include <net/sock.h>
#include "imp2.h"

DECLARE_MUTEX(receive_sem);

static struct sock *nlfd;

struct{
__u32 pid;
rwlock_t lock;
}user_proc;

/*netlink的接收回调函数,负责接收用户层端的数据。运行在软中断环境*/
static void kernel_receive(struct sk_buff *_skb)
{
struct sk_buff *skb = NULL;
struct nlmsghdr *nlh = NULL;

if(down_trylock(&receive_sem))
return;

/*检查数据合法性*/
skb = skb_get(_skb);

if(skb->len >= sizeof(struct nlmsghdr)){
nlh = (struct nlmsghdr *)skb->data;
if((nlh->nlmsg_len >= sizeof(struct nlmsghdr)) && (skb->len >= nlh->nlmsg_len)){
/*用户层端进程id*/
if(nlh->nlmsg_type == IMP2_U_PID){
write_lock_bh(&user_proc.lock);
user_proc.pid = nlh->nlmsg_pid;
write_unlock_bh(&user_proc.lock);
}
/*用户端进程关闭,置pid为0*/
else if(nlh->nlmsg_type == IMP2_CLOSE){
write_lock_bh(&user_proc.lock);
if(nlh->nlmsg_pid == user_proc.pid)
user_proc.pid = 0;
write_unlock_bh(&user_proc.lock);
}
}
}
kfree_skb(skb);
up(&receive_sem);
}

/*给netlink的用户端发送数据*/
static int send_to_user(struct packet_info *info)
{
int ret;
int size;
unsigned char *old_tail;
struct sk_buff *skb;
struct nlmsghdr *nlh;
struct packet_info *packet;

size = NLMSG_SPACE(sizeof(*info)); /*字节对齐后的数据报长度*/

/*开辟一个新的套接字缓冲*/
skb = alloc_skb(size, GFP_ATOMIC);
old_tail = skb->tail;

/*填写数据报相关信息*/
nlh = NLMSG_PUT(skb, 0, 0, IMP2_K_MSG, size-sizeof(*nlh));

/*将struct packet_info作为nlmsg的数据*/
packet = NLMSG_DATA(nlh);
memset(packet, 0, sizeof(struct packet_info));
packet->src = info->src;
packet->dest = info->dest;

/*计算经过字节对其后nlmsg的长度*/
nlh->nlmsg_len = skb->tail - old_tail;
NETLINK_CB(skb).dst_group = 0;

/*发送数据*/
read_lock_bh(&user_proc.lock);
ret = netlink_unicast(nlfd, skb, user_proc.pid, MSG_DONTWAIT);
read_unlock_bh(&user_proc.lock);

return ret;

/*若发送失败,则撤销套接字*/
nlmsg_failure:
if(skb)
kfree_skb(skb);

return -1;
}

/* netfilter NF_IP_PRE_ROUTING点的挂接函数
 * 截获ip数据报
 */
static unsigned int get_icmp(unsigned int hook,
struct sk_buff *pskb,
const struct net_device *in,
const struct net_device *out,
int (*okfn)(struct sk_buff *))
{
struct iphdr *iph = NULL;
struct packet_info info;

iph = ip_hdr(pskb);
/*如果传输层协议是ICMP*/
if(iph->protocol == IPPROTO_ICMP){
read_lock_bh(&user_proc.lock);
if(user_proc.pid != 0){
read_unlock_bh(&user_proc.lock);
info.src = iph->saddr;
info.dest = iph->daddr;
send_to_user(&info); /*将数据发送给netlink的用户层端*/
}
else
read_unlock_bh(&user_proc.lock);
}

return NF_ACCEPT;
}

/*给netfilter框架的NF_IP_PRE_ROUTING点上挂接函数get_icmp()*/
static struct nf_hook_ops imp2_ops = {
.hook = get_icmp, /*hook被调用时执行的回调函数*/
.pf = PF_INET, /*协议族,<linux/socket.h>。ipv4使用PF_INET*/
.hooknum = 0, /*安装的这个函数,对应具体的hook类型*/
.priority = NF_IP_PRI_FILTER - 1,
};

static int __init init(void)
{
rwlock_init(&user_proc.lock);

/* 创建netlink套接字,指明接收数据的回调函数kernel_receive()
 * 这里协议NL_IMP2是自定义的*/
nlfd = netlink_kernel_create(&init_net, NL_IMP2, 0, kernel_receive, NULL, THIS_MODULE);
if(!nlfd){
printk("cannot create a netlink socket\n");
return -1;
}

/*注册netfilter hook函数*/
return nf_register_hook(&imp2_ops);
}

static void __exit fini(void)
{
/*释放套接字所占资源*/
if(nlfd){
sock_release(nlfd->sk_socket);
}
/*注销netfilter hook函数*/
nf_unregister_hook(&imp2_ops);
}

module_init(init);
module_exit(fini);

at the beginning

# 一个驱动程序的角色是提供机制,而不是策略 2010-11-15 09:26 小默

“一个驱动程序的角色是提供机制,而不是策略。”--ldd3

机制:提供什么能力
策略:如何使用这些能力
机制和策略由软件不同部分,或完全不同的软件实现。

比如第一次实习时:
我们这边负责写驱动,只关注实现什么功能,怎么实现这样功能,这是机制。我们可以直接在设备管理器中安装卸载,或者用命令行安装卸载使用等,随意,也就是开发过程完全不考虑策略。
等开发进行到了一定阶段,又招了另外一名同学负责界面,这是策略。用户怎么使用这个驱动,操作界面是怎样的,是由他来负责的。

不知道是不是这个意思O(∩_∩)O~~  回复  更多评论 删除评论  修改评论

# makefile写法 2010-11-15 17:14 小默

对于单个.c文件的hello world例子:
obj-m := hello.o
从目标文件hello.o简历一个模块hello.ko

如果模块来自两个源文件file1.c和file2.c:
obj-m := module.o
module-objs := file1.o file2.o

上面的命令,必须在内核系统上下建立文中被调用

-----

在任意当前工作目录中,需要在makefile中指明源码树的路径。

ifneq ($(KERNELRELEASE),)
obj-m := hello.o
else
KERNELDIR ?= /lib/modules/$(shell uname -r)/build //TODO 不是在源码树中么/(ㄒoㄒ)/~~
PWD := $(shell pwd)
default:
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
endif

创建一个模块需要调用两次上面的makefile
第一次:没有设置变量KERNELRELEASE,通过已安装模块目录中的符号链接指回内核建立树;然后运行default中的make命令,第二次使用makefile
第二次:已经设置了变量KERNELRELEASE,直接obj-m := hello.o创建模块  回复  更多评论 删除评论  修改评论

# 加载和卸载模块 2010-11-15 17:57 小默

modprobe实现和insmod一样加载模块到内核的功能
不同的是,加载前会检查模块中是否有当前内核中没有定义的symbol,如果有,在模块搜索路径中寻找其它模块是否含有上面的symbol,有的话,自动加载关联模块
insmod对于这种情况,会报错unresolved symbols

查看当前加载模块:
lsmod
cat /proc/modules
  回复  更多评论 删除评论  修改评论

# 如果你的模块需要输出符号给其他模块使用 2010-11-15 18:12 小默

EXPORT_SYMBOL(name);
EXPORT_SYMBOL_GPL(name);  回复  更多评论 删除评论  修改评论

# 模块参数 2010-11-15 18:40 小默

说10次hello,Mom
# insmod hellop howmany=10 whom="Mom"

--

static char *whom = "world"; //必须给默认值
static int howmany = 1;
module_param(howmany, int, S_IRUGO);
module_param(whom, charp, S_IRUGO);

--

S_IRUGO 允许所有人读
S_IRUGO | S_IWUSR 允许所有人读,允许root改变参数

如果参数被sysfs修改,不会通知模块。不要使参数可写,除非准备好检测参数改变。

--
数组参数:
module_param_array(name, type, num, perm);   回复  更多评论 删除评论  修改评论

# 设备主次编号 2010-11-16 13:24 小默

$ ls -l /dev
...
crw-rw---- 1 vcsa tty 7, 132 Nov 15 17:16 vcsa4
crw-rw---- 1 vcsa tty 7, 133 Nov 15 17:16 vcsa5
crw-rw---- 1 vcsa tty 7, 134 Nov 15 17:16 vcsa6
crw-rw---- 1 root root 10, 63 Nov 15 17:16 vga_arbiter
drwxr-xr-x 2 root root 80 Nov 15 17:16 vg_colorfulgreen
crw-rw-rw- 1 root root 1, 5 Nov 15 17:16 zero
...

输出第一列是c的是字符设备,第一列b块设备

修改日期前的两个数字。
第一个是主设备编号:标识设备相连的驱动
第二个是次设备编号:决定引用哪个设备

-------
设备编号的内部表示

dev_t 在<linux/types.h>中定义,32位,12位主编号,20位次编号。

获得一个dev_t的主或次编号:<linux/kdev_t.h>
MAJOR(dev_t dev);
MINOR(dev_t dev);

将主次编号转换成dev_t:
MKDEV(int major, int minor);

--------
分配和释放设备编号

建立一个字符驱动时,做的第一件事就是获取一个或多个设备编号使用:
<linux/fs.h>
int register_chrdev_region(dev_t first, unsigned int count, char *name);
first要分配的起始设备编号
count请求的连续设备编号的总数
name连接到这个编号范围的设备的名字,会出现在/proc/devices和sysfs中
成功返回0,出错返回负的错误码。

如果事先不知道使用哪个设备编号,使用下面函数,内核会分配一个主设备编号:
int alloc_chrdev_region(dev_t *dev, unsigned int firstminor, unsigned int count, char *name);
dev是一个输出参数,返回分配范围的第一个数
firstminor请求第一个要用的次编号,常是0

设备编号的释放:
void unregister_chrdev_region(dev_t first, unsigned int count);  回复  更多评论 删除评论  修改评论

# scull安装脚本 2010-11-16 13:57 小默

脚本scull_load:
1 #!/bin/sh
2 module="scull"
3 device="scull"
4 mode="664"

6 # invoke insmod with all arguments we got
7 # and use a pathname, as newer modutils don't look in. by default
8 /sbin/insmod ./$module.ko $* || exit 1 # 插入模块,使用获取的所有参数($*)

10 # remove stale nodes删除无效的节点,不能删除device0,device1,device2...阿。。TODO
11 rm -f /dev/${device}[0-3]
12 
13 major=$(awk "\\$2==\"$module\" {print \\$1}" /proc/devices) #TODO 没有搞明白
14 mknod /dev/${device}0 c $major 0 #创建4个虚拟设备
15 mknod /dev/${device}1 c $major 1
16 mknod /dev/${device}2 c $major 2
17 mknod /dev/${device}3 c $major 3
18 
19 # give appropriate group/permissions, and change the group.
20 # Not all distributions have staff, some have "wheel" instead. #TODO 神马意思?
21 group="staff"
22 grep -q '^staff:' /etc/group || group="wheel"
23 # 改变设备的组和模式。脚本必须以root运行,但设备使用可能需要其它用户写。
24 chgrp $group /dev/${device}[0-3]
25 chmod $mode /dev/${device}[0-3]
26 
  回复  更多评论 删除评论  修改评论

# file_operations结构 2010-11-16 15:24 小默

一些处理文件的回调函数

1 // init file_operations
2 struct file_operations scull_fops = {
3 .owner = THIS_MODULE,
4 .llseek = scull_llseek,
5 .read = scull_read,
6 .write = scull_write,
7 .ioctl = scull_ioctl,
8 .open = scull_open,
9 .release = scull_release,
10 };  回复  更多评论 删除评论  修改评论

# 注册字符设备 2010-11-16 15:25 小默

12 // 使用struct scull_dev结构表示每个设备
13 // TODO 没有理解什么意思
14 struct scull_dev {
15 struct scull_qset *data; // pointer to first quantum set
16 int quantum; // the current quantum size
17 int qset; // the current array size
18 unsigned long sizee; //amount of data stored here
19 unsigned int access_key; // used by sculluid and scullpriv
20 struct semaphore sem; // matual exclusion semaphore
21 struct cdev cdev; // 字符设备结构
22 };
23 
24 // 初始化struct cdev,并添加到系统中
25 static void scull_setup_cdev(struct scull_dev *dev, int index)
26 {
27 int err, devno = MKDEV(scull_major, scull_minor + index);
28 
29 // TODO 初始化已经分配的结构. 不是很理解
30 // cdev结构嵌套在struct scull_dev中,必须调用cdev_init()来初始化cdev结构
31 cdev_init(&dev->cdev, &scull_fops);
32 dev->cdev.owner = THIS_MODULE;
33 dev->cdev.ops = &scull_fops;
34 // 添加到系统中
35 err = cdev_add(&dev->cdev, devno, 1);
36 if(err)
37 printk(KERN_NOTICE "Error %d adding scull%d", err, index);
38 }
  回复  更多评论 删除评论  修改评论

# system-config-selinux 2010-11-27 02:54 小默

system-config-selinux
什么时候改了,汗 //TODO  回复  更多评论 删除评论  修改评论

# read & write 2010-11-30 22:12 小默

// 表示每个设备
struct scull_dev{
struct scull_qset *data; // pointer to first quantum set
int quantum; // the current quantum size - 当前量子和量子集大小
int qset; // the current array size - 每个内存区域为一个量子,数组为一个量子集
unsigned long size; // amount of data stored here
unsigned int access_key; // used by sculluid and scullpriv
struct semaphore sem; // mutual exclusion semaphore

struct cdev cdev; // char device structure
};

// 量子集,即一个内存区域的数组
struct scull_qset{
void **data;
struct scull_qset *next;
};

ssize_t scull_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos)
{
struct scull_dev *dev = file->private_data;
struct scull_qset *dptr; // 量子集中的第一个元素
int quantum = dev->quantum, qset = dev->qset; // 当前量子和量子集大小
int itemsize = quantum * qset; // listitem中的字节数=量子大小*量子集大小
int item, s_pos, q_pos, rset;
ssize_t retval = 0;

if(down_interruptible(&dev->sem)) // TODO
return -ERESTARTSYS;
if(*f_pos > dev->size)
goto out;
if(*f_pos + count > dev->size)
count = dev->size - *f_pos;

// 查找listitem, qset index, and 量子中的偏移量
item = (long)*f_pos / itemsize;
rest = (long)*f_pos % itemsize;
s_pos = rest / quantum;
q_pos = rest % quantum;

// 遍历list到右侧
dptr = scull_follow(dev, item); // 量子集中的第一个元素
if(dptr == NULL || !dptr->data || !dptr->data[s_pos])
goto out;

// 只读取到这个量子的尾部
if(count > quantum - q_pos)
count = quantum - q_pos;

if(copy_to_user(buf, dptr->data[s_pos] + q_pos, count)){
retval = -EFAULT;
goto out;
}
*f_pos += count;
retval = count;

out:
up(&dev->sem);
return retval;
}

// 一次处理单个量子
ssize_t scull_write(struct file *filp, const char __user *buf, size_t count, loff_t *f_pos)
{
struct scull_dev *dev = filp->private_data;
struct scull_qset *dptr;
int quantum = dev->quantum, qset = dev->qset;
int itemsize = quantum * qset;
int item, s_pos, q_pos, rest;
ssize_t retval = -ENOMEM; // value used in "goto out" statements
if(down_interruptible(&dev->sem))
return -ERESTARTSYS;

// 查找列表元素,qset index and 量子中的偏移量
item = (long)*f_pos / itemsize;
rest = (long)*f_pos % itemsize;
s_pos = rest / quantum;
q_pos = rest % quantum;

// 遍历list到右侧
dptr = scull_follow(dev, item);
if(dptr == NULL):
goto out;
if(!dptr->data){
dptr->data = kmalloc(qset * sizeof(char), GPL_KERNEL);
if(!dptr->data)
goto out;
memset(dptr->data, 0, qset * sizeof(char *));
}
if(!dptr->data[s_pos]){
dptr->data[s_pos] = kmalloc(quantum, GFP_KERNEL);
if(!dptr->data[s_pos])
goto out;
}

// 只写到这个量子的结束
if(count > quantum-q_pos)
count = quantum - q_pos;
// 从用户空间拷贝一整段数据to from count
if(copy_from_user(dptr->data[s_pos]+q_pos, buf, count)){
retval = -EFAULT;
goto out;
}
*f_pos += count;
retval = count;

// 更新size
if(dev->size < *f_pos)
dev->size = *f_pos;

out:
up(&dev->sem);
return retval;
}

//-------------------------------


// read和write的"矢量"版本
// readv轮流读取指示的数量到每个缓存;writev收集每个缓存的内容到一起并且作为单个写操作送出它们。
// count参数告诉有多少iovec结构,这些结构由应用程序创建,但是内核在调用驱动之前拷贝它们到内核空间。
ssize_t (*readv)(struct file *filp, const struct iovec *iov, unsigned long count, loff_t *ppos);
ssize_t (*writev)(struct file *filp, const struct iovec *iov, unsigned long count, loff_t *ppos);

// iovec描述了一块要传送的数据
struct iovec{
void __user *iov_base; // 开始于iov_base(在用户空间)
__kernel_size_t iov_len; // 并有iov_len长
};
  回复  更多评论 删除评论  修改评论

# 重定向控制台消息 2010-11-30 22:44 小默

// 重定向控制台消息
// 使用一个参数指定接收消息的控制台的编号
int main(int argc, char **argv)
{
char bytes[2] = {11, 0}; // 11 是 TIOCLINUX 的功能号

if(argc == 2) bytes[1] = atoi(argv[1]); // the chosen console
else{
fprintf(stderr, "%s: need a single arg\n", argv[0]);
exit(1);
}
// TIOCLINUX传递一个指向字节数组的指针作为参数,数组的第一个字节是一个数(需要指定的子命令)。
// 当子命令是11时,下一个字节指定虚拟控制台。
if(ioctl(STDIN_FILENO, TIOCLINUX, bytes)<0){ // use stdin
fprintf(stderr, "%s: ioctl(stdin, TIOCLINUX): %s\n", argv[0], stderror(errno));
exit(1);
}
exit(0);
}
  回复  更多评论 删除评论  修改评论

# Implementing files in /proc 2010-12-04 00:42 小默

// 在proc里实现文件,在文件被读时产生数据。
// 当一个进程读你的/proc文件,内核分配了一页内存,驱动可以写入数据返回给用户空间。
// buf 写数据的缓冲区;start有关数据写在页中哪里;eof必须被驱动设置,表示写数据结束;data用来传递私有数据。
// 假定不会有必要产生超过一页的数据,并且因此忽略了start和offset值。
int scull_read_procmem(char *buf, char **start, off_t offset, int count, int *eof, void *data)
{
int i, j, len = 0;
int limit = count - 80; // Don't print more than this

for(i = 0; i < scull_nr_devs && len <= limit; i++){ // TODO scull_nr_devs ?
struct scull_dev *d = &scull_devices[i];
struct scull_qset *qs = d->data;
if(down_interruptible(&d->sem))
return -ERESTARTSYS;
// 设备号,量子集大小,量子大小,存储的数据量
len += sprintf(buf+len, "\nDevice %i: qset %i, q %i, sz %li\n", i, d->qset, d->quantum, d->size); 
for(; qs && len <= limit; qs = qs->next){ //scan the list 遍历量子链表
// 元素地址、链表地址
len += sprintf(buf + len, " item at %p, qset at %p\n", qs, qs->data); // %p 显示一个指针
if(qs->data && !qs->next) // dump only the last item
for(j = 0; j < d->qset, j++){
if(qs->data[j])
len += sprintf(buf+len, "%4i: %8p\n", j, qs->data[j]);
}

}
up(&scull_devices[i]);
}
*eof = 1;
return len; // 返回实际在页中写了多少数据
}


/// 移除entry的一些问题
// 移除可能发生在文件正在被使用时。/proc入口没有owner,没有引用计数。
// 内核不检查注册的名字是否已经存在,可能会有多个entry使用相同名称。而且在访问和remove_proc_entry时,它们没有区别。。。悲剧。。。  回复  更多评论 删除评论  修改评论

# The seq_file interface 2010-12-04 10:56 小默

// 创建一个虚拟文件,遍历一串数据,这些数据必须返回用户空间。
// start, next, stop, show

// sfile 总被忽略;pos指从哪儿开始读,具体意义完全依赖于实现。
// seq_file典型的实现是遍历一感兴趣的数据序列,pos就用来指示序列中的下一个元素。
// 在scull中,pos简单地作为scull_array数组的索引。
// 原型
void *start(struct seq_file *sfile, loff_t *pos);
// 在scull中的实现
static void *scull_seq_start(struct seq_file *s, loff_t *pos)
{
if(*pos >= scull_nr_devs)
return NULL; // no more to read
return scull_devices + *pos; // 返回供迭代器使用的私有数据
}

// next把迭代器后挪一位,返回NULL表示没有更多数据了
// v:上一次start/next调用返回的迭代器 TODO ???返回的不是私有数据么?
// pos: 文件中的当前位置。
void *next(struct seq_file *sfile, void *v, loff_t *pos);
// scull的实现
static void *scull_seq_next(struct seq_file *s, void *v, loff_t *pos)
{
(*pos)++;
if(*pos >= scull_nr_devs)
return NULL;
return scull_devices + *pos;
}

// 内核完成迭代器,调用stop清理
void stop(struct seq_file *sfile, void *v);
// scull没有要清理的东西,stop方法是空的

// start到stop期间不会有sleep或者非原子操作,可以放心的在start中获得信号量或自旋锁。整个调用序列都是原子的。天书啊 TODO ???

// 在start和stop期间,内核调用show输出迭代器v生成的数据到用户空间
int show(struct seq_file *sfile, void *v);
// 输出,等效于用户空间的printf。返回非0值表示缓冲满,输出的数据会被丢弃。不过大多数实现都忽略返回值。
int seq_sprintf(struct seq_file *sfile, const char *fmt, ...);
// 等效于用户空间的putc和puts
int seq_putc(struct seq_file *sfile, char c);
int seq_puts(struct seq_file *sfile, const char *s);
// 如果s中有esc中的数据,这些数据用8进制输出。常见的esc是"\t\n\\",用于保持空格,避免搞乱输出。
int seq_escape(struct seq_file *m, const char *s, const char *esc);
// scull中show实现
static int scull_seq_show(struct seq_file *s, void *v)
{
struct scull_dev *dev = (struct scull_dev *)v;
struct scull_qset *d;
int i;

if(down_interrutible(&dev->sem))
return -ERESTARTSYS;
seq_printf(s, "\nDevice %i: qset %i, q %i, sz %li\n",
(int)(dev - scull_devices), dev->qset,
dev->quantum, dev->size);
for(d = dev->data; d; d = d->next){ // 遍历链表
seq_printf(s, " item at %p, qset at %p\n", d, d->data);
if(d->data && !d->next) // dump only the last item
for(i = 0; i < dev->qset; i++){
if(d->data[i])
seq_printf(s, " %4i: %8p\n", i, d->data[i]);
}
}
up(&dev->sem);
return 0;
}

// 迭代器:指向scull_dev的一个指针,囧。。。
// 迭代器操作集
static struct seq_operations scull_seq_ops = {
.start = scull_seq_start,
.next = scull_seq_next,
.stop = scull_seq_stop,
.show = scull_seq_show
};

/// 用file_operations结构结构,实现内核read/seek文件的所有操作。
// 创建一个open方法,连接文件和seq_file操作 TODO 没看懂
static int scull_proc_open(struct inode *inode, struct file *file)
{
// seq_open 连接文件和上面定义的scull_seq_ops
return seq_open(file, &scull_seq_ops);
}
// file_operations结构
static struct fle_operations scull_proc_ops = {
.owner = THIS_MODULE,
.open = scull_proc_open,
.read = seq_read,
.llseek = seq_lseek,
.release = seq_release
};

// 在/proc中创建设备
entry = create_proc_entry("scullseq", 0, NULL);
if(entry)
entry->proc_fops = &scull_proc_ops;
// create_proc_entry原型
struct proc_dir_entry *create_proc_entry(const char *name,
mode_t, mode,
struct proc_dir_entry *parent);  回复  更多评论 删除评论  修改评论

# strace命令 - debug 2010-12-04 14:12 小默

略 O(∩_∩)O~~  回复  更多评论 删除评论  修改评论

# ldd3_4.5_Debugging System Faults 2010-12-05 14:46 小默

讲了两部分

一、system opps
空指针引用,或者局部变量赋值覆盖了原eip,导致页错误

二、系统挂起 // TODO 看得云里雾里的
死循环等引起
假挂起:鼠标键盘等外设没有响应了,系统实际正常。可以看时间。。。
插入schedule调用防止死循环,作用是允许其他进程从当前进程窃取时间。讲了些弊端,没看懂
sysrq:没看懂
  回复  更多评论 删除评论  修改评论

# re: 盖楼 2010-12-26 06:13 小默

所谓原子操作,就是该操作绝不会在执行完毕前被任何其他任务或事件打断,也就说,它的最小的执行单位,不可能有比它更小的执行单位。
原子操作主要用于实现资源计数,很多引用计数(refcnt)就是通过原子操作实现的。

原子类型定义:
typedef struct
{
volatile int counter;
}
atomic_t; 

volatile修饰字段告诉gcc不要对该类型的数据做优化处理,对它的访问都是对内存的访问,而不是对寄存器的访问。  回复  更多评论删除评论  修改评论

# spinlock_t 2010-12-26 16:24 小默

17 typedef struct {
18 volatile unsigned int lock;
19 #ifdef CONFIG_DEBUG_SPINLOCK
20 unsigned magic;
21 #endif
22 } spinlock_t;  回复  更多评论 <a id="AjaxHolder_Comments_CommentList_ctl24_DeleteLink"

ch03 字符驱动

3.4 字符设备注册

struct cdev  <linux/cdev.h>

1. 分配并初始化一个cdev结构。

如果期望获得一个独立的cdev结构:

struct cdev *my_cdev = cdev_alloc();
my_cdev
->ops = &my_fops;


如果想把cdev嵌入到自定义的设备结构中:

void cdev_init(struct cdev *cdev, struct file_operations *fops);


上面两种方法,都需要初始化owner成员为THIS_MODULE

2.把新建立的cdev结构告诉内核

// 把已经分配、初始化的cdev结构通知内核。立即生效。出错时返回一个负的错误码。
int cdev_add(struct cdev *dev, 
               dev_t num,           
// 设备响应的第一个设备号
               unsigned int count);  // 关联到设备上的设备号数量


3.移除cdev结构

// 从系统中移除一个字符设备
void cdev_del(struct cdev *dev);


scull中字符设备注册

 // 注册字符设备
static void scull_setup_cdev(struct scull_dev *dev, int index)
{
    
int err, devno = MKDEV(scull_major, scull_minor + index); // MKDEV 把主次设备号合成为一个dev_t结构

    
// 分配并初始化cdev结构。struct cdev内嵌在struct scull_dev中
    cdev_init(&dev->cdev, &scull_fops);
    dev
->cdev.owner = THIS_MODULE;
    dev
->cdev.ops = &scull_fops;
    
// 通知内核,立即生效
    err = cdev_add (&dev->cdev,     // struct cdev *p: the cdev structure for the device
                    devno,          // dev_t dev: 第一个设备号
                    1);             // unsigned count: 该设备连续次设备号的数目
    /* Fail gracefully if need be */
    
if (err)
        printk(KERN_NOTICE 
"Error %d adding scull%d", err, index);
}




// 注册字符设备过去的方法
int register_chrdev(unsigned int major, const char *name, struct file_operations *fops);
int unregister_chrdev(unsigned int major, const char *name);
 

3.5 open 和 release

3.5.1 open 方法

open方法在驱动中用来做一些初始化准备工作。

在多数驱动中,open进行的工作:
- 检查设备特定的错误(device-not-ready等)
- 如果设备第一次打开,初始化设备
- 更新 f_op 指针,如果需要的话
- 给 filp->private_data 赋值

open原型:

int (*open)(struct inode *inode, struct file *filp);

如何确定打开的是哪个设备:
- inode中i_cdev成员,是之前建立的cdev结构。通常我们需要的不是cdev,而是包含cdev的scull_dev结构,这时使用    
container_of(pointer, container_type, container_field);
- 查看存储在inode中的次设备号。如果使用register_chrdev注册设备,必须采用这种方法。

scull设备是全局和永久的,没有维护打开计数,不需要"如果设备第一次打开,初始化设备"的操作。  // TODO
scull_open:
/// 打开设备
int scull_open(struct inode *inode, struct file *filp)
{
    
struct scull_dev *dev; /* device information */

    
// 文件私有数据,设置成对应的scull_dev
    dev = container_of(inode->i_cdev, struct scull_dev, cdev);
    filp
->private_data = dev; /* for other methods */

    
/* now trim to 0 the length of the device if open was write-only */
    
// 文件以只读模式打开时,截断为0
    if ( (filp->f_flags & O_ACCMODE) == O_WRONLY) {
        
if (down_interruptible(&dev->sem)) // 申请新号量,可中断
            return -ERESTARTSYS;
        
// 释放整个数据区
        scull_trim(dev); /* ignore errors */ 
        up(
&dev->sem);  // 释放信号量
    }
    
return 0;          /* success */
}

3.5.2 release方法

release进行的工作:
- 释放open在file->private_data中申请的一切
- 在调用最后一个close时关闭设备

scull的基本形式没有需要关闭的设备。
scull_release:
int scull_release(struct inode *inode, struct file *filp)
{
    
return 0;
}


close 和 release
并不是每一个close系统调用,都会引起对release的调用。
内核对一个打开的file结构,维护一个引用计数。dup和fork都只递增现有file结构的引用计数。只有当引用计数为0时,对close的调用才引起对release的调用。
3.6 scull的内存使用 

how & why 执行内存分配
scull 使用的内存区(被称为device)长度可变,随写的内容增多而增长;修剪通过用短文件重写device实现。

1. 内核中管理内存使用的两个核心函数:
void *kmalloc(size_t size, int flags);
void kfree(void *ptr);
kmalloc试图申请size字节内存,返回指向内存的指针,或者申请失败时返回NULL。flags指定如何使用内存,在这里先用GFP_KERNEL。
kfree用来释放kmalloc申请的内存。不能用kfree释放其它不是kmalloc申请的内存。

kmalloc不是申请大块内存的有效方法,但为了代码简单易懂,在这里暂时使用kmalloc。另外,我们没有限制“device”区域大小:理论上不该给数据施加武断的限制;实践上scull可以暂时吃光所有系统内存来模拟低内存环境下的测试。可以使用 cp /dev/zero /dev/scull0 吃光所有RAM内存,也可以用 dd 工具指定分配给scull的内存。

2. scull device结构:
在scull中,每个device是一系列指向scull_device结构的指针组成的链表。每个scull_device结构默认指向最多4M的内存区:有多个量子集,每个量子集有多个量子。每个scull_device的布局如下:


struct scull_qset {
    void **data;
    struct scull_qset *next;
};
3. 量子和量子集大小设置:
在scull中单写一个字节需要8000kb或1200kb内存:量子用掉4000;量子集用掉4000kb或8000kb(根据在目标平台上一个指针代码32位或者64位)。但是,当写入数据量比较大时,链表的开销还可以:每4M字节一个链表。

为量子和量子集选择合适的值是策略问题,而不是机制。scull驱动不应该强制使用特定值,而是可以由用户设置:编译时改变scull.h中宏SCULL_QUANTUM和SCULL_QSET;模块加载时设置scull_quantum和scull_qset;运行时通过ioctl改变。

4. scull中的内存管理的代码段

// 释放整个数据区。简单遍历列表并且释放它发现的任何量子和量子集。
// 在scull_open 在文件为写而打开时调用。
// 调用这个函数时必须持有信号量。
/*
 * Empty out the scull device; must be called with the device
 * semaphore held.
 */
int scull_trim(struct scull_dev *dev)
{
    struct scull_qset *next, *dptr;
    int qset = dev->qset;   /* "dev" is not-null */ // 量子集大小
    int i;

    // 遍历多个量子集。dev->data 指向第一个量子集。
    for (dptr = dev->data; dptr; dptr = next) { /* all the list items */
        if (dptr->data) { // 量子集中有数据
            for (i = 0; i < qset; i++) // 遍历释放当前量子集中的每个量子。量子集大小为qset。 
                kfree(dptr->data[i]);
            kfree(dptr->data); // 释放量子指针数组
            dptr->data = NULL; 
        }
        // next获取下一个量子集,释放当前量子集。
        next = dptr->next;
        kfree(dptr);
    }
    // 清理struct scull_dev dev中变量的值
    dev->size = 0;
    dev->quantum = scull_quantum;
    dev->qset = scull_qset;
    dev->data = NULL;
    return 0;
}

3.7 read 和 wirte

read和write进行在内核和用户空间之间复制数据的工作。
原型:
ssize_t read(struct file *filp, char _ _user *buff,
    size_t count, loff_t *offp);
ssize_t write(struct file *filp, const char _ _user *buff,
    size_t count, loff_t *offp);
filp是指向文件的指针。count是要读写字节数。buff指向用户空间地址。offp是指向long offset type对象的指针,指明要访问的文件位置。
返回实际传输的字节数,出错返回负值。

访问用户空间地址char __user *buff:

驱动中不应该直接对用户空间地址指针 *buff 解引用。原因:
- 用户空间地址指针在内核中可能根本无效,这取决于主机体系结构和内核配置。
- 即便这个指针在内核中代表同一个东西,但用户空间是分页的,地址可能不在RAM中。直接解引用 用户空间地址指针 可能引发页错误。
- 用户程序提供的指针可能是错误的或者恶意的。盲目解引用用户提供的指针,将允许用户访问系统内存中任何地方。

应该通过内核提供的函数访问用户空间。
scull中复制一整段数据,使用下面两个函数:
unsigned long copy_to_user(void _ _user *to,
                           const void *from, 
                           unsigned long count);
unsigned long copy_from_user(void *to, 
                             const void _ _user *from, 
                             unsigned long count);
使用上面两个函数需要注意的一点:当用户空间地址不在内存中时,进程会投入睡眠等待内存调度,所以任何存取用户空间的函数必须是可重入的。
数据传送完成后需要修改*offp为当前的文件位置。
read参数:

读device时需要考虑的特殊情况:当前读位置大于设备大小

这种情况发生在:进程a在读设备,同时进程b写设备将长度截断为0。此时scull的read返回0表示已到文件尾(没有更多的数据可以读)

ch04 调试技术

4.1 内核中的调试支持

编译内核时,"kernel hacking"菜单中有一些调试选项。
书中列了一些,暂时没有看
4.2 debugging by print

扫下目录,暂时没有兴趣细看o(╯□╰)o

4.2.1 printk
4.2.2 重定向控制台消息
4.2.3 消息是如何被记录的
4.2.4 关闭或打开消息
4.2.5 速率限制
4.2.6 打印设备号 
4.3-4.6 心情不好,不想看鸟~

ch05 并发和竞争

早期内核不支持多处理器,只有硬件中断服务涉及到并发问题。
5.1 scull 的缺陷
write逻辑中的一段代码,scull需要考虑申请的内存是否获得:
    if (!dptr->data[s_pos]) {
        dptr->data[s_pos] = kmalloc(quantum, GFP_KERNEL);
        if (!dptr->data[s_pos])
            goto out;
    }
假设在同一时刻,A和B两个进程试图独立地写同一个scull设备的相同偏移。两个进程同时到达if判断,此时sptr->data[s_pos]为NULL,两个进程都申请了一块内存赋值给dptr->data[s_pos]。dptr->data[s_pos]最后只会保留一个值,假设B赋值在A之后,A申请的内存就被丢掉无法再获得,从来产生内存泄露(memory leak)。
上面是一个竞争状态(race condition)的示例,产生竞争状态的原因是没有控制对 共享数据 的访问。
5.2 并发和它的管理 

竞争条件是伴随着资源的共享访问而来的。内核代码应该有最小的共享,其中最明显的应用就是尽可能少地使用全局变量。

共享无处不在。硬件是共享的,软件资源常常需要共享给多个线程。需要牢记全局变量远非唯一的共享方式。当把指针传递到内核其他部分时,可能就创建了一个新的共享。

资源共享的hard rule:如果硬软件资源被多于一个执行线程共享,并且不同线程对统一资源可能产生不同的视图,则必须明确管理对资源的访问。比如5.1末尾提到的例子,B看不到A已经为dptr->data[s_pos]分配了内存,于是B才重新分配了一块内存,并覆盖掉了A的内存指针。

另外一个重要的规则:如果内核代码创建了一个可能被内核其它部分共享的对象,这个对象必须一直存在直到没有外部引用。这里有2层意思:只有做好所有准备后才能告诉内核设备存在了;必须记录共享对象的引用数。
5.3 信号量和互斥

我们的目的是对scull数据结构的操作是原子的(atomic):也就是说在有其它线程会影响到这个数据结构前,操作必须一次完成。为此需要设置一个临界区(critical sections):其中的代码在任何时刻只能由一个线程执行。

内核提供了多种原语来满足不同临界区的要求。在这个例子中,scull驱动可以睡眠等待:只有用户的直接请求会产生对scull数据的访问,不会有来自中断处理或者异步上下文中的访问,另外scull也没有持有关键的系统资源。所以可以使用锁机制:可能导致进程在等待访问临界区时睡眠。

另外很重要的一点,我们在临界区中将执行另外一个可能会导致线程休眠的操作:kmalloc申请内存。所以我们选用的锁原语,必须能够在拥有锁的线程休眠时正常工作。在这里最合适的机制是信号量。

一个信号量本质上是一个整数值,它和一对PV函数混合使用。希望进入临界区的线程在相关信号量上调用P函数:如果信号量值>0,值减1,,线程继续;否则线程休眠等待其它人释放信号量。用V解锁信号量:增加信号量值,并在必要时唤醒等待线程。

信号量的初始值表示总共有几个资源可供同时使用。当信号量用于互斥时(mutual exclusion,避免多个线程同时在一个临界区中运行),信号量的值应该初始化为1。这时,信号量也常常称为互斥体。

访问临界区的原语
| 锁:可能导致进程休眠。
| | 信号量:能够在拥有锁的线程休眠时正常工作。
| | | 互斥体:初始化为1的信号量,用户互斥。

5.3.1 Linux 信号量的实现

<linux/semaphore.h>
struct semaphore

信号量有多种声明和初始化的方法。

void sema_init(struct semaphore *sem, int val);
创建信号量sem,赋予初始值val。

DECLARE_MUTEX(name);
DECLARE_MUTEX_LOCKED(name);
第一个宏初始化信号量name为1。
第二个宏初始化信号量name为0。这时互斥体的初始状态是锁定的,在允许任何线程访问之前,必须被显示解锁。

void init_MUTEX(struct semaphore *sem);
void init_MUTEX_LOCKED(struct semaphore *sem);
用于互斥体必须在运行时初始化,例如在动态分配互斥体的情况下。

void down(struct semaphore *sem);
int down_interruptible(struct semaphore *sem);
int down_trylock(struct semaphore *sem);
down是linux下的P操作,减少信号量的值,并在必要时一直等待。
down_interruptible在等待时可被用户中断。如果操作被中断,该函数返回非0值,并且线程不会获得信号量。在使用down_interrupible时,必须检查返回值。非中断操作会建立不可杀进程(ps输出中的D state),一般不提倡使用。
down_trylock如果不能获得信号量,立即返回非零值,永远不会休眠。

void up(struct semaphore *sem);
up是linux下的V操作。
任何获得信号量的线程都必须通过up释放信号量。如果线程在拥有信号量的时候出错,必须在将错误返回给调用者之前,释放信号量。

5.3.2 在scull中使用信号量

使用锁原语的关键:明确要保护的资源;确保对该资源的每次访问都使用了合适的锁原语。

在scull驱动中,要限制访问的资源都在scull_dev结构中。我们为每个scull设备使用一个单独的信号量:没有理由让一个进程在其他进程访问别的设备时等待;允许在不同设备上的操作并行进行。

信号量必须在scull设备对系统中其他部分可用前被初始化。我们在scull_init_module中,在调用scull_setup_cdev(向内核注册设备)前调用了init_MUTEX。
必须仔细检查代码,确保在不拥有该信号量时不访问scull_dev结构。例如,在scull_write开始处先申请dev->sem。
最后,scull_write必须释放信号量。

5.3.3 Reader/Writer 信号量

semaphore对所有要访问数据的线程互斥,但实际上多个线程同时读数据不会出错。

rwsem允许一个线程写数据 或者 多个线程读数据,写优先级大于读。也就是说,读写不能同时进行,读写同时申请rwsem时,写优先获得。结果是,如果有大量线程等待写数据,可能读申请很久都得不到满足,饿死。所以,最好在很少需要写数据,并且写数据用时很短时使用rwsem。

下面是使用rwsem的接口:

<linux/rwsem.h>
struct rw_semaphore

void init_rwsem(struct rw_semaphore *sem);
rwsem必须在运行时显式的初始化。

void down_read(struct rw_semaphore *sem);
int down_read_trylock(struct rw_semaphore *sem);
void up_read(struct rw_semaphore *sem);
down_read可能会使进程进入不可中断的休眠。
down_read_trylock在获得访问权限时返回非0,其它情况下返回0。 --注意返回值和一般内核函数不同:一般内核函数出错返回非0。

void down_write(struct rw_semaphore *sem);
int down_write_trylock(struct rw_semaphore *sem);
void up_write(struct rw_semaphore *sem);
void downgrade_write(struct rw_semaphore *sem);
在结束修改后调用downgrage_write,允许其它读用户访问。
5.4 complete 机制

问题提出:

内核编程中经常遇到一种情况,启动当前线程外部进行一项任务,并等待它完成。例如:创建一个新的内核线程或用户空间进程;对一个已有进程的某个请求;某个硬件动作。

这时可以使用信号量来同步这两个任务:start外部任务后马上down(sem);当外部任务完成工作时up(sem)。
    struct semphore sem;

    init_MUTEX_LOCKED(&sem);
    start_external_task(&sem);
    down(&sem);

    当外部任务完成工作时,up(&sem);

但信号量并不是完成这项工作的最好方法:这里调用down的线程几乎总是要等待,会影响性能;信号量声明为自动变量,在某些情况下,信号量围在外部线程调用up前消失。

解决方法:

complete机制,允许一个线程告诉另一个线程某项工作已完成。

<linux/completion.h>
struct completion

DECLARE_COMPLETION(my_completion);
创建completion。

struct completion my_completion;
/* .. */
init_completion(&my_completion);
动态的创建和初始化completion。

void wait_for_completion(struct completion *c);
等待completion。该函数执行的是非中断等待,如果没有人完成该任务,则将产生一个不可杀进程。

void complete(struct completion *c);
void complete_all(struct completion *c);
complete唤醒一个线程,complete_all唤醒等待的所有线程。
用complete完成的completion可以重复使用,用complete_all完成的completion在重新使用前要重新初始化:INIT_COMPLETION(struct completion c)。

completion机制的典型应用:内核模块exit时等待线程终止
thread
{
    ...
    while(1){
    if(condition) break;
    }
    收尾工作
    complete_and_exit(struct completion *c, long retval);
}

module_exit
{
    满足condition
    wait_for_complete
}

TODO:wait_for_complete和down在等在时有什么区别?为什么后者效率就高了?
5.5 自旋锁

自旋锁可在不能休眠的代码中使用,如中断处理例程。 // TODO:休眠和自旋 有什么区别?优劣之处?

一个自旋锁是一个互斥设备,只能有两个值:locked和unlocked。它通常是一个int值的单个bit。申请锁的代码test这个bit:如果锁可用,则set这个bit,执行临界区代码;否则代码进入重复测试这个bit的忙循环,直到锁可用。

自旋锁的实现:"test and set"的过程必须以原子方式完成;当存在自旋锁时,申请自旋锁的处理器进入忙循环,做不了任何有用的工作。

自旋锁最初是为了在多处理器上使用设计的。工作在可抢占内核的单处理器行为类似于SMP。如果非抢占式的单处理器系统进入自旋,会永远自旋下去;因此,非抢占式的单处理器上的自旋锁被优化为什么都不过(但改变IRQ掩码状态的例程是个例外)。

5.5.1 自旋锁API介绍

<linux/spinlock.h> spinlock_t

初始化,在编译时或运行时:
spinlock_t mylock = SPIN_LOCK_UNLOCKED;
void spin_lock_init(spinlock_t *lock);

获得锁:
void spin_lock(spinlock_t *lock);

释放已经获得的锁:
void spin_unlock(spinlock_t *lock);

5.5.2 自旋锁和原子上下文

使用自旋锁的核心规则:任何拥有自旋锁的代码都必须是原子的。它不能休眠,不能因为任何原因放弃处理器(除了某些中断服务)。

许多内核函数可以休眠:在用户空间和内核空间复制数据,用户空间页可能要从磁盘上换入,需要休眠;分配内存的操作(kmalloc)在等待可用内存时会休眠。
内核抢占的情况由自旋锁代码本身处理:代码拥有自旋锁时,在相关处理器上的抢占会被禁止。

在拥有自旋锁时,需要禁止中断。考虑一个例子:驱动程序已经获得了一个控制对设备访问的自旋锁;设备产生了一个中断,中断处理例程被调用,中断处理例程在访问这个设备之前,也需要这个锁;如果中断处理例程在最初拥有锁的代码所在的处理器上自旋,非中断代码将没有任何机会来释放这个锁,处理器将永远自旋下去。 //TODO Q:在一个CPU上被中断的代码,必须在同一CPU上恢复执行?

拥有锁的时间要尽可能短:避免其它CPU自旋等待相同锁的时间过长;本CPU无法调度高优先级进程(抢占被禁止)。

5.5.3 自旋锁函数

获得自旋锁的函数有4个:
void spin_lock(spinlock_t *lock);
void spin_lock_irqsave(spinlock_t *lock, unsigned long flags);
void spin_lock_irq(spinlock_t *lock);
void spin_lock_bh(spinlock_t *lock);
spin_lock_irpsave会在获取自旋锁之前,禁止本地CPU上的中断,并把先前中断状态保存在flags中。spin_lock_irp不跟踪中断标志,用于明确知道没有其它代码禁止本地处理器的中断时。spin_lock_bh只禁止软件中断,会让硬件中断继续打开。
如果某个自旋锁可以被运行在软件中断或硬件中断上下文的代码中获得,则必须使用某个禁止中断的spin_lock形式。

对应有4个释放自旋锁的函数:
void spin_unlock(spinlock_t *lock);
void spin_unlock_irqrestore(spinlock_t *lock, unsigned long flags);
void spin_unlock_irq(spinlock_t *lock);
void spin_unlock_bh(spinlock_t *lock);
spin_lock_irqsave和spin_unlock_irqsave必须在同一函数中调用。

非阻塞的自旋锁操作:
int spin_trylock(spinlock_t *lock);
int spin_trylock_bh(spinlock_t *lock);
spin_trylock和spin_trylock_bh在获得自旋锁时返回非零值,否则返回0.

5.5.4 读取者/写入者 自旋锁

与rwsem类似,允许多个reader同时进入临界区,writer必须互斥访问。
可能造成reader饥饿,原因同rwsem。

下面是相关API。

静/动态初始化:

rwlock_t my_rwlock = RW_LOCK_UNLOCKED; /* Static way */

rwlock_t my_rwlock;
rwlock_init(&my_rwlock);  /* Dynamic way */

reader API:

void read_lock(rwlock_t *lock);
void read_lock_irqsave(rwlock_t *lock, unsigned long flags);
void read_lock_irq(rwlock_t *lock);
void read_lock_bh(rwlock_t *lock);

void read_unlock(rwlock_t *lock);
void read_unlock_irqrestore(rwlock_t *lock, unsigned long flags);
void read_unlock_irq(rwlock_t *lock);
void read_unlock_bh(rwlock_t *lock);

writer API:

void write_lock(rwlock_t *lock);
void write_lock_irqsave(rwlock_t *lock, unsigned long flags);
void write_lock_irq(rwlock_t *lock);
void write_lock_bh(rwlock_t *lock);
int write_trylock(rwlock_t *lock);

void write_unlock(rwlock_t *lock);
void write_unlock_irqrestore(rwlock_t *lock, unsigned long flags);
void write_unlock_irq(rwlock_t *lock);
void write_unlock_bh(rwlock_t *lock);
5.6 锁陷阱

5.6.1 不明确的规则

在创建可被并行访问对象的同时,定义控制访问的锁。

如果某个已经获得锁的函数,试图调用其他同样试图获得同一锁的函数,代码就会死锁。

有些函数需要假定调用者已经获得了某些锁:如果函数供外部调用,必须显式的处理锁定;如果是内部函数,必须显式说明这种假定,防止以后忘记。

5.6.2 锁的顺序规则

在必须获得多个锁时,应该始终以相同的顺序获得:假设线程1和线程2都要获得LOCK1、LOCK2,如果线程1拿到了LOCK1,同时线程2拿到了LOCK2,就出现死锁了。

两条TIPS。如果需要获得一个局部锁(如设备锁)和一个内核更中心位置的锁,先获得局部锁。如果需要同时拥有信号量和自旋锁,必须先获得信号量:在拥有自旋锁时调用down(可能导致休眠)是个严重错误。

5.6.3 细粒度锁和粗粒度锁的对比

粗粒度锁影响性能,细粒度锁增加复杂性。

应该在最初使用粗粒度锁,抑制自己过早考虑优化的欲望,因为真正的性能瓶颈常出现在非预期的地方。
ch06 高级字符驱动程序操作

6.1 ioctl

支持通过设备驱动程序执行各种类型的硬件控制。

用户空间原型:
int ioctl(int fd, unsigned long cmd, ...);

内核空间原型:
int (*ioctl)(struct inode *inode, struct file *filp, unsigned int cmd, unsigned long arg);
注意不管用户控件传递的参数的值还是指针,在内核中都用unsigned long表示。

大多数ioctl实现都包括一个switch语句,根据cmd参数选择对应操作。

6.1.1 选择ioctl命令

两个关于选择ioctl编号的文档。include/asm/ioctl.h定义了要使用的位字段:类型(幻数)、序数、传送方向、参数大小等。Documentation/ioctl-numbet.txt中罗列了内核所使用的幻数,选择自己的幻数时注意避免和内核冲突。

4个位字段:type是8位宽的幻数;number是8位宽的序数;direction数据传输方向:该字段是个位掩码,可用AND从中分解出_IOC_READ和_IOC_WRITE;size是所涉及的用户数据大小。

构造cmd的宏(在<linux/ioctl.h>中包含的<asm/ioctl.h>中定义):
_IO(type, nr)构造无参数的ioctl命令;_IOR(type, nr, datatype)读数据;_IOW(type, nr, datatype)写数据;_IORW(type, nr, datatype)双向传输。
type和number通过参数type和nr传入,size通过对datatype取sizeof获得。

对cmd解码的宏:
_IOC_DIR(nr), _IOC_TYPE(nr), _IOC_NR(nr), and _IOC_SIZE(nr).

6.1.2 返回值

cmd不匹配任何合法操作时,返回-EINVAL。

6.1.3 预定义命令

内核预定义了一些ioctl命令。其中一类是可用于任何文件的命令,幻数都是“T”。

6.1.4 使用ioctl参数

如果ioctl的附加参数是指向用户空间的指针,必须确保指向的用户空间是合法的。

acess_ok函数验证地址:检查所引用的内存是否位于当前进程有访问权限的区域;特别地,要确保地址没有指向内核空间的内存区。

6.1.5 权能与受限操作

<linux/capability.h>

基于权能(capability)的系统把特权操作分成独立的组。

<sys/sched.h>
int capable(int capability);
检查进程是否拥有权能capability。
6.3 poll 和 select

驱动程序决定,是否可以对一个或多个打开的文件,做非阻塞的读取或写入。

驱动程序的poll方法:
unsigned int (*poll)(struct file *filp, poll_table *wait);
当用户程序在与驱动相关联的文件描述符上执行poll/select时被调用。
分两步处理:调用poll_wait向poll_table添加一个或多个 可指示poll状态变化的等待队列;返回一个描述操作是否可以利益执行的位掩码。

void poll_wait(struct file *, wait_queue_head_t *, poll_table);
ch12 pci驱动

PCI是一套完整的规则,定义计算机的各个部分如何交互。PCI驱动负责发现硬件并访问。

PCI体系结构的目的:提高计算机和外设间数据传输性能;尽可能地和平台无关;简化外设的添加和移除。

PCI设备在启动时配置。设备驱动必须可以访问配置信息,并完成设备初始化。不需要PCIqudo
12.1.1 PCI寻址

struct dev_pci 代表pci设备。

每个PCI外设由 <总线号,设备号,功能号> 确定。Linux另外引入了PCI domains的概念,每个domain可以有256根总线,每根总线可以有32个设备,每个设备下可以有8个功能号(例如一个audio设备有一个CD-ROM功能号):所以每个功能可以由16位的地址表示。
查看16位硬件地址:lspci;/proc/pci;/proc/bus/pci。地址一般按16进制表示:<8位总线号:8位设备号和功能号>;<8位总线号:5位设备号,3位功能号>;<domain:bus:device:funciton>。

较新的工作站至少有两根总线。每两根总线用特殊外设bridges连接。PCI系统的总体布局是一棵树:总线通过bridges连接到上层总线上,直到树根(0号总线)。

外设板答复3类地址空间的查询:内存单元,I/O端口,配置寄存器空间。同一PCI总线上的所有设备共享前两个地址(也就是说,访问一个内存单元时,该总线上的所有设备都能看到总线周期)。配置空间利用了地理寻址,同一时刻只对应一个slot,不会产生冲突。

下面的一堆没有看懂。

12.1.2 启动时间 

当PCI设备上电时,它只响应配置信息的存取:设备没有内存空间,也没有I/O端口被映射到内存空间;中断报告也被关闭。

系统启动时,固件(bios或内核)读取每个PCI外设的配置信息,为其分配地址去。在驱动存取设备时,它的内存区和I/O区已经被映射到处理器的地址空间。

通过读/proc/bus/pcidrivers 和 /proc/bus/pci/*/* 可查看PCI设备列表和配置寄存器。

12.1.3 配置寄存器和初始化

所有PCI设备都有至少256字节的配置地址空间:前64个字节是标准的,剩下的依赖于设备。

PCI寄存器是小端模式。如果需要在主机序和PCI序之间转换,参考<asm/byteorder.h>

寄存器类型:
vendorID    16位,硬件制造商
deviceID    16位,由硬件制造商选择。vendorID和deviceID合起来被称作“签名”,驱动依靠签名来标识设备。
classID 16位,类。一个驱动可以同时支持几个属于同一类,但有不同签名的设备。
subsystem venderID/subsystem deviceID 进一步标识设备。

struct pci_device_id 定义一个驱动支持的PCI设备的列表:
__u32 vendor;    // 驱动支持的设备的PCI供应商和设备ID,PCI_ANY_ID表示支持任何。
__u32 device;
__u32 subvendor;
__u32 subdevice;
__u32 class;    // 驱动支持的PCI设备种类,PCI_ANY_ID表示支持任何类
__u32 class_mask;
kernel_ulong_t driver_data;    // 用来持有信息,也可以来标识设备。

初始化一个struct pci_device_id:
PCI_DEVICE(vendor, device);
PCI_DEVICE(device_class, device_class_mask);

12.1.4 MODULEDEVICETABLE宏

MODULEDEVICETABLE将pci_device_id结构输出到用户空间,供热插拔和模块加载系统知道驱动模块匹配的硬件。

该宏创建 指向struct pci_device_id的 局部变量_mod_pci_device_table;depmod遍历所有模块,将查找到的_mod_pci_device_table添加到文件 /lib/modules/KERNEL_VERSION/modules.pcimap中;当有新的PCI设备连到计算机上,热插拔系统使用文件modules.pcimap找到对应的驱动并加载。

12.1.5 注册一个PCI驱动

PCI驱动结果,包含许多回调函数和变量。
struct pci_driver 
const char *name;    // 驱动名称,在内核所有PCI驱动里必须是唯一的。可以在/sys/bud/pci/drivers中查看。
const struct pci_device_id *id_tables;    // 指向struct pci_device_id表的指针。
int (*probe)(struct pci_dev *dev, const struct pci_device_id *id);    // PCI核心发现与该驱动匹配的struct pci_dev时,调用probe。如果驱动支持该pci_dev,正确初始化设备并返回0;否则返回一个错误值。
int (*remove)(struct pci_dev);    // 当struct pci_dev从系统中移除时,由PCI核心调用;PCI驱动被移除时,也会调用remove函数来移除驱动支持的设备。
int (*suspend)(struct pci_dev *dev, u32 state);    // 当struct pci_dev被挂起时,由PCI核心调用。state指明挂起状态。可选的实现。
int (*resume)(struct pci_dev *dev);    // 当pci_dev恢复时,由PCI核心调用。

总之,创建一个pci驱动时,至少只需初始化4个成员:
static struct pci_driver pci_driver = {
    .name = "pci_skel",
    .id_table = ids,
    .probe = probe,
    .remove = remove,
};

注册一个PCI驱动:
int pci_register_driver(struct pci_driver *drv);
.注册struct pci_driver到PCI核心,成功返回0,失败返回一个错误值。它不返回绑定到驱动上的设备号;实际上,当没有设备被绑定到驱动上时,也算注册成功:驱动把自己注册到PCI核心,当PCI核心发现有新设备时,PCI核心调用驱动提供的回调函数初始化设备。

卸载一个PCI驱动:
void pci_unregister_driver(struct pci_driver *drv);
该函数移除绑定到这个驱动上的所有设备,然后调用驱动的remove函数。 TODO Q:调用remove 和 移除绑定到驱动上的设备 不一样么?

12.1.6 老式PCI探测

老版本内核中,PCI驱动手动浏览系统中PCI设备。在2.6中已经去掉了驱动浏览PCI设备列表的功能(处于安全考虑),而是由PCI核心发现设备时调用已经注册的驱动。

查找一个特定PCI设备:
struct pci_dev *pci_get_device(unsigned int vendor, unsigned int device, struct pci_dev *from);
struct pci_dev *pci_get_subsys(unsigned int vendor, unsigned int device, unsigned int ss_vendor, unsigned int ss_device, struct pci_dev *from);
如果找到匹配设备,递增struct pci_dev中的found引用计数;当驱动用完该pci_dev时,需要调用pci_dev_put递减found引用计数。
from表示从哪个设备开始探索,如果from设为NULL,表示返回第一个匹配的设备。
该函数不能从中断上下文中调用。

在指定的pci bus上查找设备:
struct pci_dev *pci_get_slot(struct pci_bus *bus, unsigned int devfn);
devfn指明pci设备的功能号。

12.1.7 使能pci设备

int pci_enable_device(struct pci_dev *dev);
在PCI驱动的probe()函数中,必须调用pci_enable_device(),之后驱动才可以存取PCI设备的资源。

12.1.8 存取配置空间

驱动已经探测到设备后,需要读写3个地址空间:内存、I/O端口和配置空间。其中,需要先读取配置空间,获取设备被映射到的内存和I/O空间。

配置空间可以通过8/16/32为数据传输来读写:
int pci_read_config_byte(struct pci_dev *dev, int where, u8 *val);
int pci_read_config_word(struct pci_dev *dev, int where, u16 *val);
int pci_read_config_dword(struct pci_dev *dev, int where, u32 *val);
int pci_write_config_byte(struct pci_dev *dev, int where, u8 val);
int pci_write_config_word(struct pci_dev *dev, int where, u16 val);
int pci_write_config_dword(struct pci_dev *dev, int where, u32 val);
where指从配置空间开始的字节偏移,读取到的值从val返回。
word和dword函数负责从小端到处理器本地字节序的转换。

12.1.9 存取I/O和内存空间

一个PCI设备至多有6个I/O地址区:每个地址区要么是内存区,要么是I/O区。I/O寄存器不应该被缓存,所以对于在内存中实现I/O寄存器的设备,必须在配置寄存器中设置一个"内存可预取"位,表明内存区是否可预取。

一个PCI设备使用6个32位的配置寄存器,指明它的地址区大小和位置。这6个配置寄存器的符号名是PCI_ADDRESS_0到PCI_BASE_ADDRESS_5。

获取设备的地址区:
unsigned long pci_resource_start(struct pci_dev *dev, int bar);
unsigned long pci_resource_end(struct pci_dev *dev, int bar);
unsigned long pci_resource_flags(struct pci_dev *dev, int bar);
返回第bar个地址区的第一个地址,最后一个地址,flags资源标识。
资源标识定义在<linux/ioport.h>中。

12.1.10 PCI中断

linux启动时,固件给设备分配一个唯一的中断号。
PCI设备的中断号存储在配置寄存器60(PCI_INTERRUPT_LINE),1字节宽。
如果不支持中断,寄存器61(PCI_INTERRUPT_PIN)是0.
ch17 网络驱动程序

网络驱动程序异步接收来自外部世界的数据包,发送给内核。

octet指一组8个的数据位,是能为网络设备和协议所理解的最小单位。


17.1 snull设计

snull模拟了和远程主机的回话,不依赖于硬件。只支持ip流。

17.1.1 分配ip号

snull创建了两个接口。通过一个接口传输的数据,都会出现在另外一个接口上。

如果给两个接口使用相同的ip号,内核会使用回环通道,而不会通过snull,所以在传输中需要修改源及目的地址。 // TODO


17.2 连接到内核

推荐阅读 lookup.c plip.c e100.c

17.2.1 设备注册

网络接口没有主次设备号一类的东西,而是对每个新检测到的接口,向全局的网络连接设备链表中插入一个数据结构 net_device。
<linux/netdevice.h>
struct net_device *snull_devs[2];
struct net_device包含了一个kobject和引用计数。

由于和其它结构相关,必须动态分配:
struct net_device *alloc_netdev(int sizeof_priv, const char *name, void(*setup)(struct net_device *));
sizeof_priv是驱动程序私有数据大小;name接口名称,在用户空间可见;setup初始化函数,用来设置net_device结构剩余部分。
网络子系统对alloc_netdev函数,为不同种类的接口封装了许多函数,最常用的是alloc_etherdev<linux/etherdevice.h>。

调用register_netdev注册设备,立即生效。

17.2.2 初始化每个设备

在调用register_netdev之前,必须完成初始化。
先调用ether_setup(struct net_device)为结构设置默认值,然后再给我们关心的成员赋值。在scull中,flags设置为IFF_NOARP,表示接口不使用ARP协议;hard_header_cache设置为NULL,禁止对ARP的缓存。

struct net_device中priv成员保存设备私有数据,和net_device结构同时分配内存(字符驱动中fops->private是单独分配的)。不推荐直接访问priv指针,一般通过netdev_priv函数访问:
struct scull_priv *priv = netdev_priv(dev);

17.2.3 模块的卸载

for (i = 0; i < 2;  i++) {
    if (snull_devs[i]) {
        unregister_netdev(snull_devs[i]);
        snull_teardown_pool(snull_devs[i]);
        free_netdev(snull_devs[i]);
    }
}

unregister_netdev从系统中删除接口;free_netdev将net_device结构返回给系统(引用计数减1);snull_teardown_pool是内部清楚函数,必须在free_netdev之前调用:调用free_netdev之后不能再引用设备或私有数据区。
17.3 net_device结构细节

//一堆成员列表,米有看。。。
17.4 打开和关闭

在接口能够传输数据包之前,内核必须打开接口并赋予其地址。
用ifconfig给接口赋予地址时,执行两个任务:通过ioctl(SIOCSIFADDR)赋予地址,该任务由内核进行;通过ioctl(SIOCSIFFLAGS)设置dev->flags中的IFF_UP标志以打开接口,该命令会调用设备的open方法。

在接口被关闭时,ifconfig使用ioctl(SIOCSIFFLAGS)清除IFF_UP标志,然后调用stop函数。

open的工作:将MAC地址从硬件设备复制到dev->dev_addr;启动接口的传输队列(允许接口传输数据包)。

stop的工作:关闭接口的传输队列、

void netif_start_queue(struct net_device *dev);
void netif_stop_queue(struct net_device *dev);
cg_cdev

insmod后
# cat /dev/devices
可以看到字符设备中列的有cg_cdev。
--
    /dev/...目录和/proc/devices/...下的设备的关系

    /proc/devices/中的设备是通过insmod加载到内核的,它的主设备号(major)作为mknod的参数。 
/dev/*.* 是通过mknod加上去的,格式:mknod device c/b major minor 注意要在/dev目录下建立设备节点,如:cd /dev 然后,mknod nrf24l01 c 238 0,用户程序可通过设备名/dev/nrf24l01来调用驱动程序从而访问硬件。 

--

使用awk工具可以从/proc/devices 文件中获取设备号,建立一个.sh的脚本文件加入脚本:

module=xxx
major=`awk "\\$2==\"$module\" {print \\$1}" /proc/devices`
insmod xxx.ko
mknod /dev/xxx c $major 0

xxx为设备名

posted on 2011-01-05 23:24 小默 阅读(2927) 评论(0)  编辑 收藏 引用 所属分类: Linux


只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   博问   Chat2DB   管理


导航

统计

留言簿(13)

随笔分类(287)

随笔档案(289)

漏洞

搜索

积分与排名

最新评论

阅读排行榜