Linux字符驱动详解
2017-01-19 02:14
274 查看
Linux中将设备分为三类 分别是字符设备,块设备,网络设备
应用程序通过open,read,write等系统调用访问相应的驱动程序,而字符驱动程序通过file_operations向上提供接口。具体调用如上图
本次介绍的是字符设备驱动程序,字符设备驱动程序呢 其实就是只能一个字节一个字节读写的设备,不能随机读取设备内存中的某一数据,读取数据需要按照先后数据。字符设备是面向流的设备,常见的字符设备有鼠标、键盘、串口、控制台和LED设备等。
搞懂了字符设备驱动的概念和字符设备驱动和应用程序之间的关系之后我们就可以来讨论下怎么写一个简单的字符设备驱动程序了
开发板:s3c2440
linux内核版本:linux-2.6.22
首先跟大家讲些主设备号 和 次设备号的概念
一个字符设备或者块设备都有一个主设备号和次设备号。主设备号和次设备号统称为设
备号。主设备号用来表示一个特定的驱动程序。次设备号用来表示使用该驱动程序的各
设备。
这里使用的register_chrdev_region或者alloc_chrdev_region函数注册字符设备
其实使用register_chrdev也是可以的 但是区别是register_chrdev函数注册的话
register_chrdev函数
__register_chrdev_region函数
函数参数分析
1.major 主设备号 如果为0 即随机分配 可以指定自己想设立的设备号
2.name 设备名称
3.fops: file operations associated with this devices文件系统的接口指针
可见register_chrdev函数一开始调用了__register_chrdev_region判断主设备号是否等于0并且有效检查设备号是否有效,注册设备到全局变量chrdevs[i]中。
然后调用注册字符设备三个步骤
来注册一个字符设备 这是linux2.4内核版本的写法 这种方法简便有效 直接调用一个register_chrdev函数就可以注册字符设备 但是缺点是次设备号未做处理 此种做法会导致(major, 0), (major, 1), …, (major, 255)都对应hello_fops 而我们可以用另外一个写法就是上述做法
意思是可以使用两种方法分配设备号 第一种方法是
1.静态申请
2.动态分配
静态申请就是已知设备号的情况下可以分配主设备号和次设备号,而动态分配就是事先不知道主设备号的情况下 系统为你自动分配一个空闲的设备号 主次设备号可以用以下方法获得
也可以使用下列宏通过主次设备号生成dev_t:
对于alloc_chrdev_region函数参数分析
1.dev_t *dev linux内核中,设备号用dev_t来描述,2.6.22中定义如下:
typedef u_long dev_t;
在32位机中是4个字节,高12位表示主设备号,低12位表示次设备号。
2.unsigned baseminor :first of the requested range of minor numbers
次设备号,要在一定范围内从0开始
3.unsigned count : the number of minor numbers required
次设备号的范围
4.const char *name:设备名称
register_chrdev_region函数也是和上述差不多 就不多分析 对号入座就行
字符设备注册函数分析完 下面就是设备文件的创建
自动创建设备节点的方法
利用udev(mdev)来实现设备文件的自动创建,首先应保证支持udev(mdev),由busybox配置。
创建文件的方法就两句话:“先创建一个类 然后在类下创建设备”
当然也可以手工创建
使用mknod手工创建:mknod filename type major minor
接下来就是注销函数
上面只是向系统注册了一个字符设备 分配了设备号 并且创建了设备节点 但是应用程序不能访问它 所以我们需要file_operations结构体
在这里我们只是简单的使用了open函数
具体想再添加对这个设备的读写或者其他功能的话就可以在file_operations里添加并且根据我们的需求添加
小结
应用程序通过open,read,write等系统调用访问相应的驱动程序,而字符驱动程序通过file_operations向上提供接口。具体调用如上图
本次介绍的是字符设备驱动程序,字符设备驱动程序呢 其实就是只能一个字节一个字节读写的设备,不能随机读取设备内存中的某一数据,读取数据需要按照先后数据。字符设备是面向流的设备,常见的字符设备有鼠标、键盘、串口、控制台和LED设备等。
搞懂了字符设备驱动的概念和字符设备驱动和应用程序之间的关系之后我们就可以来讨论下怎么写一个简单的字符设备驱动程序了
开发板:s3c2440
linux内核版本:linux-2.6.22
/* 入口函数 */ static int hello_init(void) { dev_t devid; /* 3. 告诉内核 */ #if 0 major = register_chrdev(0, "hello", &hello_fops); /* (major, 0), (major, 1), ..., (major, 255)都对应hello_fops */ #else if (major) { devid = MKDEV(major, 0); register_chrdev_region(devid, HELLO_CNT, "hello"); /* (major,0~1) 对应 hello_fops, (major, 2~255)都不对应hello_fops */ } else { alloc_chrdev_region(&devid, 0, HELLO_CNT, "hello"); /* (major,0~1) 对应 hello_fops, (major, 2~255)都不对应hello_fops */ major = MAJOR(devid); } cdev_init(&hello_cdev, &hello_fops); cdev_add(&hello_cdev, devid, HELLO_CNT); #endif cls = class_create(THIS_MODULE, "hello"); class_device_create(cls, NULL, MKDEV(major, 0), NULL, "hello0"); /* /dev/hello0 */ class_device_create(cls, NULL, MKDEV(major, 1), NULL, "hello1"); /* /dev/hello1 */ class_device_create(cls, NULL, MKDEV(major, 2), NULL, "hello2"); /* /dev/hello2 */ return 0; }
首先跟大家讲些主设备号 和 次设备号的概念
一个字符设备或者块设备都有一个主设备号和次设备号。主设备号和次设备号统称为设
备号。主设备号用来表示一个特定的驱动程序。次设备号用来表示使用该驱动程序的各
设备。
这里使用的register_chrdev_region或者alloc_chrdev_region函数注册字符设备
其实使用register_chrdev也是可以的 但是区别是register_chrdev函数注册的话
register_chrdev函数
int register_chrdev(unsigned int major, const char *name, const struct file_operations *fops) { struct char_device_struct *cd; struct cdev *cdev; char *s; int err = -ENOMEM; cd = __register_chrdev_region(major, 0, 256, name); if (IS_ERR(cd)) return PTR_ERR(cd); cdev = cdev_alloc(); if (!cdev) goto out2; cdev->owner = fops->owner; cdev->ops = fops; kobject_set_name(&cdev->kobj, "%s", name); for (s = strchr(kobject_name(&cdev->kobj),'/'); s; s = strchr(s, '/')) *s = '!'; err = cdev_add(cdev, MKDEV(cd->major, 0), 256); if (err) goto out; cd->cdev = cdev; return major ? 0 : cd->major; out: kobject_put(&cdev->kobj); out2: kfree(__unregister_chrdev_region(cd->major, 0, 256)); return err; }
__register_chrdev_region函数
static struct char_device_struct * __register_chrdev_region(unsigned int major, unsigned int baseminor, int minorct, const char *name) { struct char_device_struct *cd, **cp; int ret = 0; int i; cd = kzalloc(sizeof(struct char_device_struct), GFP_KERNEL); if (cd == NULL) return ERR_PTR(-ENOMEM); mutex_lock(&chrdevs_lock); /* temporary */ if (major == 0) { for (i = ARRAY_SIZE(chrdevs)-1; i > 0; i--) { if (chrdevs[i] == NULL) break; } if (i == 0) { ret = -EBUSY; goto out; } major = i; ret = major; } cd->major = major; cd->baseminor = baseminor; cd->minorct = minorct; strncpy(cd->name,name, 64); i = major_to_index(major); for (cp = &chrdevs[i]; *cp; cp = &(*cp)->next) if ((*cp)->major > major || ((*cp)->major == major && (((*cp)->baseminor >= baseminor) || ((*cp)->baseminor + (*cp)->minorct > baseminor)))) break; /* Check for overlapping minor ranges. */ if (*cp && (*cp)->major == major) { int old_min = (*cp)->baseminor; int old_max = (*cp)->baseminor + (*cp)->minorct - 1; int new_min = baseminor; int new_max = baseminor + minorct - 1; /* New driver overlaps from the left. */ if (new_max >= old_min && new_max <= old_max) { ret = -EBUSY; goto out; } /* New driver overlaps from the right. */ if (new_min <= old_max && new_min >= old_min) { ret = -EBUSY; goto out; } } cd->next = *cp; *cp = cd; mutex_unlock(&chrdevs_lock); return cd; out: mutex_unlock(&chrdevs_lock); kfree(cd); return ERR_PTR(ret); }
函数参数分析
1.major 主设备号 如果为0 即随机分配 可以指定自己想设立的设备号
2.name 设备名称
3.fops: file operations associated with this devices文件系统的接口指针
可见register_chrdev函数一开始调用了__register_chrdev_region判断主设备号是否等于0并且有效检查设备号是否有效,注册设备到全局变量chrdevs[i]中。
然后调用注册字符设备三个步骤
字符设备的注册分为三个步骤: (1)分配cdev: struct cdev *cdev_alloc(void); (2)初始化cdev: void cdev_init(struct cdev *cdev, const struct file_operations *fops); (3)添加cdev: int cdev_add(struct cdev *p, dev_t dev, unsigned count)
来注册一个字符设备 这是linux2.4内核版本的写法 这种方法简便有效 直接调用一个register_chrdev函数就可以注册字符设备 但是缺点是次设备号未做处理 此种做法会导致(major, 0), (major, 1), …, (major, 255)都对应hello_fops 而我们可以用另外一个写法就是上述做法
if (major) { devid = MKDEV(major, 0); register_chrdev_region(devid, HELLO_CNT, "hello"); /* (major,0~1) 对应 hello_fops, (major, 2~255)都不对应hello_fops */ } else { alloc_chrdev_region(&devid, 0, HELLO_CNT, "hello"); /* (major,0~1) 对应 hello_fops, (major, 2~255)都不对应hello_fops */ major = MAJOR(devid); } cdev_init(&hello_cdev, &hello_fops); cdev_add(&hello_cdev, devid, HELLO_CNT);
意思是可以使用两种方法分配设备号 第一种方法是
1.静态申请
int register_chrdev_region(dev_t from, unsigned count, const char *name); /** * register_chrdev_region() - register a range of device numbers * @from: the first in the desired range of device numbers; must include * the major number. * @count: the number of consecutive device numbers required * @name: the name of the device or driver. * * Return value is zero on success, a negative error code on failure. */
2.动态分配
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name); /** * alloc_chrdev_region() - register a range of char device numbers * @dev: output parameter for first assigned number * @baseminor: first of the requested range of minor numbers * @count: the number of minor numbers required * @name: the name of the associated device or driver * * Allocates a range of char device numbers. The major number will be * chosen dynamically, and returned (along with the first minor number) * in @dev. Returns zero or a negative error code. */
静态申请就是已知设备号的情况下可以分配主设备号和次设备号,而动态分配就是事先不知道主设备号的情况下 系统为你自动分配一个空闲的设备号 主次设备号可以用以下方法获得
MAJOR(dev_t dev); MINOR(dev_t dev);
也可以使用下列宏通过主次设备号生成dev_t:
MKDEV(int major,int minor);
对于alloc_chrdev_region函数参数分析
1.dev_t *dev linux内核中,设备号用dev_t来描述,2.6.22中定义如下:
typedef u_long dev_t;
在32位机中是4个字节,高12位表示主设备号,低12位表示次设备号。
2.unsigned baseminor :first of the requested range of minor numbers
次设备号,要在一定范围内从0开始
3.unsigned count : the number of minor numbers required
次设备号的范围
4.const char *name:设备名称
register_chrdev_region函数也是和上述差不多 就不多分析 对号入座就行
字符设备注册函数分析完 下面就是设备文件的创建
自动创建设备节点的方法
利用udev(mdev)来实现设备文件的自动创建,首先应保证支持udev(mdev),由busybox配置。
创建文件的方法就两句话:“先创建一个类 然后在类下创建设备”
static struct class *cls; cls = class_create(THIS_MODULE, "hello"); class_device_create(cls, NULL, MKDEV(major, 0), NULL, "hello0"); /* /dev/hello0 */
当然也可以手工创建
使用mknod手工创建:mknod filename type major minor
接下来就是注销函数
static void hello_exit(void) { class_device_destroy(cls, MKDEV(major, 0)); class_device_destroy(cls, MKDEV(major, 1)); class_device_destroy(cls, MKDEV(major, 2)); class_destroy(cls); cdev_del(&hello_cdev); unregister_chrdev_region(MKDEV(major, 0), HELLO_CNT);
上面只是向系统注册了一个字符设备 分配了设备号 并且创建了设备节点 但是应用程序不能访问它 所以我们需要file_operations结构体
struct file_operations ***_ops={ .owner = THIS_MODULE, .llseek = ***_llseek, .read = ***_read, .write = ***_write, .ioctl = ***_ioctl, .open = ***_open, .release = ***_release, 。。。 。。。 }; struct module *owner; /*第一个 file_operations 成员根本不是一个操作; 它是一个指向拥有这个结构的模块的指针. 这个成员用来在它的操作还在被使用时阻止模块被卸载. 几乎所有时间中, 它被简单初始化为 THIS_MODULE, 一个在 <linux/module.h> 中定义的宏.这个宏比较复杂,在进行简单学习操作的时候,一般初始化为THIS_MODULE。*/ loff_t (*llseek) (struct file * filp , loff_t p, int orig); /*(指针参数filp为进行读取信息的目标文件结构体指针;参数 p 为文件定位的目标偏移量;参数orig为对文件定位 的起始地址,这个值可以为文件开头(SEEK_SET,0,当前位置(SEEK_CUR,1),文件末尾(SEEK_END,2)) llseek 方法用作改变文件中的当前读/写位置, 并且新位置作为(正的)返回值. loff_t 参数是一个"long offset", 并且就算在 32位平台上也至少 64 位宽. 错误由一个负返回值指示. 如果这个函数指针是 NULL, seek 调用会以潜在地无法预知的方式修改 file 结构中的位置计数器( 在"file 结构" 一节中描述).*/ ssize_t (*read) (struct file * filp, char __user * buffer, size_t size , loff_t * p); /*(指针参数 filp 为进行读取信息的目标文件,指针参数buffer 为对应放置信息的缓冲区(即用户空间内存地址), 参数size为要读取的信息长度,参数 p 为读的位置相对于文件开头的偏移,在读取信息后,这个指针一般都会移动,移动的值为要读取信息的长度值) 这个函数用来从设备中获取数据. 在这个位置的一个空指针导致 read 系统调用以 -EINVAL("Invalid argument") 失败. 一个非负返回值代表了成功读取的字节数( 返回值是一个 "signed size" 类型, 常常是目标平台本地的整数类型).*/ ssize_t (*aio_read)(struct kiocb * , char __user * buffer, size_t size , loff_t p); /*可以看出,这个函数的第一、三个参数和本结构体中的read()函数的第一、三个参数是不同 的, 异步读写的第三个参数直接传递值,而同步读写的第三个参数传递的是指针,因为AIO从来不需要改变文件的位置。 异步读写的第一个参数为指向kiocb结构体的指针,而同步读写的第一参数为指向file结构体的指针,每一个I/O请求都对应一个kiocb结构体); 初始化一个异步读 -- 可能在函数返回前不结束的读操作.如果这个方法是 NULL, 所有的操作会由 read 代替进行(同步地). (有关linux异步I/O,可以参考有关的资料,《linux设备驱动开发详解》中给出了详细的解答)*/ ssize_t (*write) (struct file * filp, const char __user * buffer, size_t count, loff_t * ppos); /*(参数filp为目标文件结构体指针,buffer为要写入文件的信息缓冲区,count为要写入信息的长度, ppos为当前的偏移位置,这个值通常是用来判断写文件是否越界) 发送数据给设备. 如果 NULL, -EINVAL 返回给调用 write 系统调用的程序. 如果非负, 返回值代表成功写的字节数. (注:这个操作和上面的对文件进行读的操作均为阻塞操作)*/ ssize_t (*aio_write)(struct kiocb *, const char __user * buffer, size_t count, loff_t * ppos); /*初始化设备上的一个异步写.参数类型同aio_read()函数;*/ int (*readdir) (struct file * filp, void *, filldir_t); /*对于设备文件这个成员应当为 NULL; 它用来读取目录, 并且仅对文件系统有用.*/ unsigned int (*poll) (struct file *, struct poll_table_struct *); /*(这是一个设备驱动中的轮询函数,第一个参数为file结构指针,第二个为轮询表指针) 这个函数返回设备资源的可获取状态,即POLLIN,POLLOUT,POLLPRI,POLLERR,POLLNVAL等宏的位“或”结果。 每个宏都表明设备的一种状态,如:POLLIN(定义为0x0001)意味着设备可以无阻塞的读,POLLOUT(定义为0x0004)意味着设备可以无阻塞的写。 (poll 方法是 3 个系统调用的后端: poll, epoll, 和 select, 都用作查询对一个或多个文件描述符的读或写是否会阻塞. poll 方法应当返回一个位掩码指示是否非阻塞的读或写是可能的, 并且, 可能地, 提供给内核信息用来使调用进程睡眠直到 I/O 变为可能. 如果一个驱动的 poll 方法为 NULL, 设备假定为不阻塞地可读可写. (这里通常将设备看作一个文件进行相关的操作,而轮询操作的取值直接关系到设备的响应情况,可以是阻塞操作结果,同时也可以是非阻塞操作结果)*/ int (*ioctl) (struct inode *inode, struct file *filp, unsigned int cmd, unsigned long arg); /*(inode 和 filp 指针是对应应用程序传递的文件描述符 fd 的值, 和传递给 open 方法的相同参数. cmd 参数从用户那里不改变地传下来, 并且可选的参数 arg 参数以一个 unsigned long 的形式传递, 不管它是否由用户给定为一个整数或一个指针. 如果调用程序不传递第 3 个参数, 被驱动操作收到的 arg 值是无定义的. 因为类型检查在这个额外参数上被关闭, 编译器不能警告你如果一个无效的参数被传递给 ioctl, 并且任何关联的错误将难以查找.) ioctl 系统调用提供了发出设备特定命令的方法(例如格式化软盘的一个磁道, 这不是读也不是写). 另外, 几个 ioctl 命令被内核识别而不必引用 fops 表. 如果设备不提供 ioctl 方法, 对于任何未事先定义的请求(-ENOTTY, "设备无这样的 ioctl"), 系统调用返回一个错误.*/ int (*mmap) (struct file *, struct vm_area_struct *); /*mmap 用来请求将设备内存映射到进程的地址空间. 如果这个方法是 NULL, mmap 系统调用返回 -ENODEV. (如果想对这个函数有个彻底的了解,那么请看有关“进程地址空间”介绍的书籍)*/ int (*open) (struct inode * inode , struct file * filp ) ; /*(inode 为文件节点,这个节点只有一个,无论用户打开多少个文件,都只是对应着一个inode结构; 但是filp就不同,只要打开一个文件,就对应着一个file结构体,file结构体通常用来追踪文件在运行时的状态信息) 尽管这常常是对设备文件进行的第一个操作, 不要求驱动声明一个对应的方法. 如果这个项是 NULL, 设备打开一直成功, 但是你的驱动不会得到通知. 与open()函数对应的是release()函数。*/ int (*flush) (struct file *); /*flush 操作在进程关闭它的设备文件描述符的拷贝时调用; 它应当执行(并且等待)设备的任何未完成的操作. 这个必须不要和用户查询请求的 fsync 操作混淆了. 当前, flush 在很少驱动中使用; SCSI 磁带驱动使用它, 例如, 为确保所有写的数据在设备关闭前写到磁带上. 如果 flush 为 NULL, 内核简单地忽略用户应用程序的请求.*/ int (*release) (struct inode *, struct file *); /*release ()函数当最后一个打开设备的用户进程执行close()系统调用的时候,内核将调用驱动程序release()函数: void release(struct inode inode,struct file *file),release函数的主要任务是清理未结束的输入输出操作,释放资源,用户自定义排他标志的复位等。 在文件结构被释放时引用这个操作. 如同 open, release 可以为 NULL.*/ int(*synch)(struct file *,struct dentry *,int datasync); //刷新待处理的数据,允许进程把所有的脏缓冲区刷新到磁盘。 int (*aio_fsync)(struct kiocb *, int); /*这是 fsync 方法的异步版本.所谓的fsync方法是一个系统调用函数。系统调用fsync 把文件所指定的文件的所有脏缓冲区写到磁盘中(如果需要,还包括存有索引节点的缓冲区)。 相应的服务例程获得文件对象的地址,并随后调用fsync方法。通常这个方法以调用函数__writeback_single_inode()结束, 这个函数把与被选中的索引节点相关的脏页和索引节点本身都写回磁盘。*/ int (*fasync) (int, struct file *, int); //这个函数是系统支持异步通知的设备驱动,下面是这个函数的模板: static int ***_fasync(int fd,struct file *filp,int mode) { struct ***_dev * dev=filp->private_data; return fasync_helper(fd,filp,mode,&dev->async_queue);//第四个参数为 fasync_struct结构体指针的指针。 //这个函数是用来处理FASYNC标志的函数。(FASYNC:表示兼容BSD的fcntl同步操作)当这个标志改变时,驱动程序中的fasync()函数将得到执行。 } /*此操作用来通知设备它的 FASYNC 标志的改变. 异步通知是一个高级的主题, 在第 6 章中描述. 这个成员可以是NULL 如果驱动不支持异步通知.*/ int (*lock) (struct file *, int, struct file_lock *); //lock 方法用来实现文件加锁; 加锁对常规文件是必不可少的特性, 但是设备驱动几乎从不实现它. ssize_t (*readv) (struct file *, const struct iovec *, unsigned long, loff_t *); ssize_t (*writev) (struct file *, const struct iovec *, unsigned long, loff_t *); /*这些方法实现发散/汇聚读和写操作. 应用程序偶尔需要做一个包含多个内存区的单个读或写操作; 这些系统调用允许它们这样做而不必对数据进行额外拷贝. 如果这些函数指针为 NULL, read 和 write 方法被调用( 可能多于一次 ).*/ ssize_t (*sendfile)(struct file *, loff_t *, size_t, read_actor_t, void *); /*这个方法实现 sendfile 系统调用的读, 使用最少的拷贝从一个文件描述符搬移数据到另一个. 例如, 它被一个需要发送文件内容到一个网络连接的 web 服务器使用. 设备驱动常常使 sendfile 为 NULL.*/ ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int); /*sendpage 是 sendfile 的另一半; 它由内核调用来发送数据, 一次一页, 到对应的文件. 设备驱动实际上不实现 sendpage.*/ unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long); /*这个方法的目的是在进程的地址空间找一个合适的位置来映射在底层设备上的内存段中. 这个任务通常由内存管理代码进行; 这个方法存在为了使驱动能强制特殊设备可能有的任何的对齐请求. 大部分驱动可以置这个方法为 NULL.[10]*/ int (*check_flags)(int) //这个方法允许模块检查传递给 fnctl(F_SETFL...) 调用的标志. int (*dir_notify)(struct file *, unsigned long); //这个方法在应用程序使用 fcntl 来请求目录改变通知时调用. 只对文件系统有用; 驱动不需要实现 dir_notify.
在这里我们只是简单的使用了open函数
static int hello_open(struct inode *inode, struct file *file) { printk("hello_open\n"); return 0; } 构造file_operations static struct hello_fops = { .owner = THIS_MODULE, .open = hello_open, };
具体想再添加对这个设备的读写或者其他功能的话就可以在file_operations里添加并且根据我们的需求添加
小结
相关文章推荐
- linux中秒字符设备驱动(宋宝华设备驱动开发详解第10章)
- 嵌入式Linux字符设备入门之--LED驱动详解
- 不再害怕驱动!Linux字符设备驱动详解
- linux字符设备驱动之file_operations结构体知识详解
- Linux设备驱动开发详解-第6章字符设备驱动(一)-globalmem
- linux 驱动编写之虚拟字符设备的编写实例详解
- Linux字符设备驱动详解
- Linux设备驱动开发详解-第6章字符设备驱动(二)-支持2个globalmem
- 嵌入式Linux字符设备驱动模型详解
- Linux字符设备驱动的register_chrdev()与unregister_chrdev()
- linux2.6字符设备驱动编程第一例:globalmem
- linux 设备模型详解 驱动
- tr命令详解 Linux字符转换
- Linux字符设备驱动学习
- Linux驱动程序开发(4) - 字符设备驱动(3)-LED设备驱动和应用程序
- Linux设备驱动开发详解--笔记6--字符设备驱动
- LED灯显示字符驱动程序-linux2.6LED显示驱动
- 编写Linux并行接口字符设备驱动
- Linux设备驱动开发详解--笔记3--Linux内核及内核编程
- 详解linux系列之字符界面下MySQL+apache+php的源代码安装