Driver:模块参数、系统调用、字符设备驱动框架
来源:互联网 发布:windows 10企业版激活 编辑:程序博客网 时间:2024/06/05 02:36
1、模块参数
./a.out xxx yyy zzz
int main (int argc, char** argv) {...}// 用户空间
insmod xxx.ko mmm nnn
1)定义全局变量
2)将该全局变量声明为模块参数
'module_param (name, type, perm);
// 指定模块参数,用于加载模块或模块加载以后传递参数给模块
@name:变量的名称
@type:变量的数据类型,支持类型如下:
bool
int
short
long
charp // char*
perm:模块参数的访问权限,类似文件权限
rwxrwxrwx 0777 user group other
'module_param_array (name, type, nump, perm);
// 将一个数组声明为模块参数
@type:数组成员成名为模块参数
@nump:数组元素个数指针(有效成员的地址)
验证:
#:'insmod moduleparam.ko
[ 305.153000] irq = 0
[ 305.153000] pstr = jiangyuan
[ 305.153000] fish[0] = 0
[ 305.154000] fish[1] = 0
[ 305.155000] fish[2] = 0
[ 305.156000] fish[3] = 0
[ 305.157000] fish[4] = 0
[ 305.158000] fish[5] = 0
[ 305.159000] fish[6] = 0
[ 305.160000] fish[7] = 0
[ 305.161000] fish[8] = 0
[ 305.162000] fish[9] = 0
#:'rmmod moduleparam
#:'insmod moduleparam.ko irq=10 pstr="hello,world" fish=1,2,3,4,5
[ 448.455000] irq = 10
[ 448.455000] pstr = hello,world
[ 448.456000] fish[0] = 1
[ 448.457000] fish[1] = 2
[ 448.459000] fish[2] = 3
[ 448.460000] fish[3] = 4
[ 448.461000] fish[4] = 5
#:' ls /sys/module/moduleparam/parameters/ -l
total 0
-rw-r--r-- 1 root 0 4096 Jan 1 00:58 fish
-rw------- 1 root 0 4096 Jan 1 00:58 irq
#:'cat /sys/module/moduleparam/parameters/irq
10
#:'cat /sys/module/moduleparam/parameters/fish
1,2,3,4,5
#:'echo 111 >/sys/module/moduleparam/parameters/irq
#:'echo 222,333,444,555 >/sys/module/moduleparam/parameters/fish
#:'cat /sys/module/moduleparam/parameters/irq
111
#:'cat /sys/module/moduleparam/parameters/fish
222,333,444,555
#:'rmmod moduleparam
[ 531.125000] irq = 111
[ 531.125000] pstr = hello,world
[ 531.125000] fish[0] = 222
[ 531.126000] fish[1] = 333
[ 531.127000] fish[2] = 444
[ 531.128000] fish[3] = 555
【总结】'以上参数用在驱动代码调试阶段。'比如:特殊功能寄存器的赋值...
/sys/目录类似于/proc目录:基于内存的虚拟文件系统。
在2.6内核中引入了/sys/目录
导出驱动内核模型
2、系统调用
【注意点】谈谈对系统调用的理解?
2.1 系统调用的作用
是用户态切换到内核态的一种方式。
unix的C编程程序大多数情况下运行于用户态,当产生系统调用就进入内核态。
2.2 系统调用时如何实现的
1.进程先将系统调用号填充寄存器;
2.调用一个特殊的指令swi;
3.让用户进程跳转到内核事先定义好的一个位置;
4.内核位置是ENTRY(vector_swi);//entry-common.s
5.检查系统调用号,这个号告诉内核请求哪种服务;
6.查看系统调用表(sys_call_table) 找到所调用的内核函数入口地址;
7.调用该函数,执行,执行完毕返回到用户进程
eg:
open ("a.txt", ...)
适当的值:如果是open,值就是5
arch/arm/include/asm/unistd.h // 定义内核源码 - 共365个
寄存器:对于ARM处理器 r7
特殊的指令:
arm ---> swi / svc
x86 ---> int
(调用后产生一个中断异常,硬件自动做4件事...)
固定的位置:
entry-common.S (e:\porting\kernel\arch\arm\kernel)
ENTRY (vector_swi):
sys_call_table[R7]
sys_call_table 存在于calls.s
sys_open
2.3 添加一个新的系统调用
1)在内核中增加一个新的函数
$:'cd kernel/
$:'vi arch/arm/kernel/sys_arm.c
135 asmlinkage int sys_add (int x, int y)
136 {
137 printk ("enter %s\n", __func__);
138 return x + y;
139 }
2)更新unistd.h
$:'vi arch/arm/include/asm/unistd.h
407 #define __NR_add (__NR_SYSCALL_BASE+378)
3)更新系统调用表 sys_call_table
$:'vi arch/arm/kernel/calls.S
390 CALL(sys_add) /* 378 */
4)重新编译内核
$:'make uImage
5)开发板使用新的内核
$:'cp arch/arm/boot/uImage /tftpboot/
#:'tftp 48000000 uImage
#:'bootm 48000000
6)编写测试程序,调用编号为378的内核函数
open ("a.txt", O_RDONLY);
syscall (5, "a.txt", O_RDONLY);
$:'cp test ../../rootfs/
// nfs网络加载的开发板根文件系统在rootfs里面
$:'cp /opt/arm-<tab>/arm-<tab>/sysroot/lib/libgcc_s.so* ../../rootfs/lib/ -d
#:'./test
[ 13.627000] enter sys_add
res = 300
<tips>
$:'find ./ -name "libgcc*"
按文件名查找,结果给出找到的所有路径
3、字符设备驱动框架
字符设备:'读写顺序固定,读写过程中不涉及缓存。'如键盘... (较多)
块设备:读写顺序不固定,读写过程中有缓存。如磁盘/硬盘...(基本已标准化)
网络设备:读写顺序固定,读写过程中有缓存。如网卡...(可能涉及一点移植)
linux内核主要是使用C语言实现的,但是其中运用了大量的面向对象的编程思想。
使用'结构体'来实现面向对象编程。
实现一个字符设备驱动,实则就是实例化一个 struct cdev:
1) 定义struct cdev类型变量;
2) 初始化变量;
3) 注册变量。
struct cdev {
dev_t dev; // 设备号(3.1)
const struct file_operations *ops; // 操作函数集合 (3.2)
...
};
3.1 【设备号】
dev_t
← typedef __kernel_dev_t
← typedef __u32 __kernel_dev_t
← #define __u32 unsigned int
设备号(32bit) = 主设备号(高12bit) + 次设备号(低20bit)
主设备号:用来区分不同类型的设备。
次设备号:用来区分同类设备中的不同个体。
#:'ls /dev/ttySAC* -l
3.1.1 静态注册设备号
选出一个内核中未被使用的主设备号,为我所用。
主设备的取值范围: 0~255
如何查看内核哪些主设备号没有被占用?
1) #:' cat /proc/devices
显示信息为:[主设备号] [使用设备的模块]
比如 200 没被占用,就可用。
2) $:' cat Documentation/devices.txt | less
【静态注册设备号的方法】
'register_chrdev_region'
int register_chrdev_region(dev_t from, unsigned count, const char *name)
功能:注册一个范围的设备号
参数:
@from:要注册的连续多个设备号中的第一个,必须是主设备号
0x12300000
@count:要注册的设备号个数
3 要注册的设备号就是 0x12300000
0x12300001
0x12300002
@name:名称
返回值: 0 - 成功,负数 - 失败。
'unregister_chrdev_region'
void unregister_chrdev_region(dev_t from, unsigned count)
功能:注销设备号
参数:同register_chrdev_region
返回值:无
#:' insmod char_drv.ko
#:'cat /proc/devices
200 jiangyuan-1610
3.1.2 动态注册设备号
由内核帮我们挑一个未被使用的设备号,和我们自己选中的次设备号拼成设备号。
【动态注册设备号的方法】
'alloc_chrdev_region'
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)
功能:动态分配一个设备号范围
参数:
@dev:传出参数 → 用于返回注册的多个设备号中的第一个。
@baseminor:自己规定的起始次设备号
@count:连续注册的设备号个数
@name:名称
返回值: 0 - 成功,负数 - 失败。
假如:
baseminor = 100
内核给分配的主设备号假如是 200
count = 3
相当于向内核注册以下设备号:
200 << 20 | 100
200 << 20 | 100 + 1
200 << 20 | 100 + 2
'unregister_chrdev_region'
void unregister_chrdev_region(dev_t from, unsigned count)
功能:注销设备号 ('静态和动态均是其注销')
参数:同register_chrdev_region
返回值:无
#:' insmod char_drv.ko
#:'cat /proc/devices
#:'rmmod char_drv.ko
#:' cat /proc/devices
3.2 【操作函数集合】
完成一个字符设备驱动就是实例化一个 cdev 结构体。
实例化一个cdev过程,主要工作就是实现其对应的操作函数的集合。
struct file_operations {
read ()
write ()
open ()
...
};
以上数据结构是所有字符设备驱动要实现的函数的合集,再去完成某个具体硬件字符设备驱动时,只需要实现其中的一部分函数就可以了。(根据具体的情况)
3.3 内核中提供操作cdev的API
struct cdev{
dev
file_operation
...
};
'cdev_init'// cdev的初始化函数
void cdev_init(struct cdev *cdev, const struct file_operations *fops)
功能:初始化一个设备结构体
参数:
@cdev:要初始化的设备结构体变量地址
@fops:要操作的设备的结构体变量地址
返回值:无
'cdev_add' // 注册cdev
int cdev_add(struct cdev *p, dev_t dev, unsigned count)
功能:添加一个字符设备到系统
参数:
@p:要注册到系统中去的cdev地址
@dev:对应的设备号
@count:连续注册的个数,一个就取1
返回值: 0 - 成功,负数 - 失败。
'cdev_del' // 注销cdev
void cdev_del(struct cdev *p)
功能:删除一个cdev字符设备
参数:
@p:要从系统注销的cdev地址
返回值:无
/** 代码演示 - **/
(暂略)
实验步骤:
1) 安装查看模块
#:' insmod char_drv.ko
#:'cat /proc/devices
244 ...
2) 创建设备文件
#:'mknod /dev/leds c 244 100
3) 写一个test.c
$:' vi test.c
$:' arm-cortex_a9-linux-gnueabi-gcc test.c -o test
$:' cp test ../../rootfs/
$:'./test
enter led_open success...
open /dev/leds successed...
now is closing device!enter led_release success...
实验总结:
没有增加新的系统调用,但也调用到了open和release函数...
设备文件时用户态程序调用硬件驱动的媒介(c major minor)。
<tips>
linux内核uImage:
'make menuconfig---> Kernel hacking ---> show timing information on printks
当选中这个选项后,启动内核,会在日志信息前面加上时间戳。取消选中,即可取消时间戳。
./a.out xxx yyy zzz
int main (int argc, char** argv) {...}// 用户空间
insmod xxx.ko mmm nnn
1)定义全局变量
2)将该全局变量声明为模块参数
'module_param (name, type, perm);
// 指定模块参数,用于加载模块或模块加载以后传递参数给模块
@name:变量的名称
@type:变量的数据类型,支持类型如下:
bool
int
short
long
charp // char*
perm:模块参数的访问权限,类似文件权限
rwxrwxrwx 0777 user group other
'module_param_array (name, type, nump, perm);
// 将一个数组声明为模块参数
@type:数组成员成名为模块参数
@nump:数组元素个数指针(有效成员的地址)
/** 代码演示 - moduleparam.c **/#include <linux/init.h>#include <linux/module.h>MODULE_LICENSE ("GPL");int irq = 0;char* pstr = "jiangyuan";int fish[10];int nr_fish = 10; module_param (irq, int, 0600);module_param (pstr, charp, 0); module_param_array (fish, int, &nr_fish, 0644);int __init moduleparam_init (void) { int i = 0; printk ("irq = %d\n", irq); printk ("pstr = %s\n", pstr); for (i = 0; i < nr_fish; i++) { printk ("fish[%d] = %d\n", i, fish[i]); } return 0;}void __exit moduleparam_exit (void) { int i = 0; printk ("irq = %d\n", irq); printk ("pstr = %s\n", pstr); for (i = 0; i < nr_fish; i++) { printk ("fish[%d] = %d\n", i, fish[i]); } }module_init (moduleparam_init);module_exit (moduleparam_exit);
/** Makefile **/obj-m += moduleparam.oall: make -C /home/tarena/jy/driver/kernel M=$(PWD) modules cp *.ko ../../rootfs -vclean: make -C /home/tarena/jy/driver/kernel M=$(PWD) clean
验证:
#:'insmod moduleparam.ko
[ 305.153000] irq = 0
[ 305.153000] pstr = jiangyuan
[ 305.153000] fish[0] = 0
[ 305.154000] fish[1] = 0
[ 305.155000] fish[2] = 0
[ 305.156000] fish[3] = 0
[ 305.157000] fish[4] = 0
[ 305.158000] fish[5] = 0
[ 305.159000] fish[6] = 0
[ 305.160000] fish[7] = 0
[ 305.161000] fish[8] = 0
[ 305.162000] fish[9] = 0
#:'rmmod moduleparam
#:'insmod moduleparam.ko irq=10 pstr="hello,world" fish=1,2,3,4,5
[ 448.455000] irq = 10
[ 448.455000] pstr = hello,world
[ 448.456000] fish[0] = 1
[ 448.457000] fish[1] = 2
[ 448.459000] fish[2] = 3
[ 448.460000] fish[3] = 4
[ 448.461000] fish[4] = 5
#:' ls /sys/module/moduleparam/parameters/ -l
total 0
-rw-r--r-- 1 root 0 4096 Jan 1 00:58 fish
-rw------- 1 root 0 4096 Jan 1 00:58 irq
#:'cat /sys/module/moduleparam/parameters/irq
10
#:'cat /sys/module/moduleparam/parameters/fish
1,2,3,4,5
#:'echo 111 >/sys/module/moduleparam/parameters/irq
#:'echo 222,333,444,555 >/sys/module/moduleparam/parameters/fish
#:'cat /sys/module/moduleparam/parameters/irq
111
#:'cat /sys/module/moduleparam/parameters/fish
222,333,444,555
#:'rmmod moduleparam
[ 531.125000] irq = 111
[ 531.125000] pstr = hello,world
[ 531.125000] fish[0] = 222
[ 531.126000] fish[1] = 333
[ 531.127000] fish[2] = 444
[ 531.128000] fish[3] = 555
【总结】'以上参数用在驱动代码调试阶段。'比如:特殊功能寄存器的赋值...
/sys/目录类似于/proc目录:基于内存的虚拟文件系统。
在2.6内核中引入了/sys/目录
导出驱动内核模型
2、系统调用
【注意点】谈谈对系统调用的理解?
2.1 系统调用的作用
是用户态切换到内核态的一种方式。
unix的C编程程序大多数情况下运行于用户态,当产生系统调用就进入内核态。
2.2 系统调用时如何实现的
1.进程先将系统调用号填充寄存器;
2.调用一个特殊的指令swi;
3.让用户进程跳转到内核事先定义好的一个位置;
4.内核位置是ENTRY(vector_swi);//entry-common.s
5.检查系统调用号,这个号告诉内核请求哪种服务;
6.查看系统调用表(sys_call_table) 找到所调用的内核函数入口地址;
7.调用该函数,执行,执行完毕返回到用户进程
eg:
open ("a.txt", ...)
适当的值:如果是open,值就是5
arch/arm/include/asm/unistd.h // 定义内核源码 - 共365个
寄存器:对于ARM处理器 r7
特殊的指令:
arm ---> swi / svc
x86 ---> int
(调用后产生一个中断异常,硬件自动做4件事...)
固定的位置:
entry-common.S (e:\porting\kernel\arch\arm\kernel)
ENTRY (vector_swi):
sys_call_table[R7]
sys_call_table 存在于calls.s
sys_open
2.3 添加一个新的系统调用
1)在内核中增加一个新的函数
$:'cd kernel/
$:'vi arch/arm/kernel/sys_arm.c
135 asmlinkage int sys_add (int x, int y)
136 {
137 printk ("enter %s\n", __func__);
138 return x + y;
139 }
2)更新unistd.h
$:'vi arch/arm/include/asm/unistd.h
407 #define __NR_add (__NR_SYSCALL_BASE+378)
3)更新系统调用表 sys_call_table
$:'vi arch/arm/kernel/calls.S
390 CALL(sys_add) /* 378 */
4)重新编译内核
$:'make uImage
5)开发板使用新的内核
$:'cp arch/arm/boot/uImage /tftpboot/
#:'tftp 48000000 uImage
#:'bootm 48000000
6)编写测试程序,调用编号为378的内核函数
open ("a.txt", O_RDONLY);
syscall (5, "a.txt", O_RDONLY);
/** 代码演示 - test.c **/#include <stdio.h>#include <unistd.h>#include <sys/syscall.h>int add (int x, int y) {return syscall (378, x, y);}int main (void){int res = 0;res = add (100, 200);printf ("res = %d\n", res);return 0;}$:'arm-cortex_a9-linux-gnueabi-gcc test.c -o test
$:'cp test ../../rootfs/
// nfs网络加载的开发板根文件系统在rootfs里面
$:'cp /opt/arm-<tab>/arm-<tab>/sysroot/lib/libgcc_s.so* ../../rootfs/lib/ -d
#:'./test
[ 13.627000] enter sys_add
res = 300
<tips>
$:'find ./ -name "libgcc*"
按文件名查找,结果给出找到的所有路径
3、字符设备驱动框架
字符设备:'读写顺序固定,读写过程中不涉及缓存。'如键盘... (较多)
块设备:读写顺序不固定,读写过程中有缓存。如磁盘/硬盘...(基本已标准化)
网络设备:读写顺序固定,读写过程中有缓存。如网卡...(可能涉及一点移植)
linux内核主要是使用C语言实现的,但是其中运用了大量的面向对象的编程思想。
使用'结构体'来实现面向对象编程。
实现一个字符设备驱动,实则就是实例化一个 struct cdev:
1) 定义struct cdev类型变量;
2) 初始化变量;
3) 注册变量。
struct cdev {
dev_t dev; // 设备号(3.1)
const struct file_operations *ops; // 操作函数集合 (3.2)
...
};
3.1 【设备号】
dev_t
← typedef __kernel_dev_t
← typedef __u32 __kernel_dev_t
← #define __u32 unsigned int
设备号(32bit) = 主设备号(高12bit) + 次设备号(低20bit)
主设备号:用来区分不同类型的设备。
次设备号:用来区分同类设备中的不同个体。
#:'ls /dev/ttySAC* -l
3.1.1 静态注册设备号
选出一个内核中未被使用的主设备号,为我所用。
主设备的取值范围: 0~255
如何查看内核哪些主设备号没有被占用?
1) #:' cat /proc/devices
显示信息为:[主设备号] [使用设备的模块]
比如 200 没被占用,就可用。
2) $:' cat Documentation/devices.txt | less
【静态注册设备号的方法】
'register_chrdev_region'
int register_chrdev_region(dev_t from, unsigned count, const char *name)
功能:注册一个范围的设备号
参数:
@from:要注册的连续多个设备号中的第一个,必须是主设备号
0x12300000
@count:要注册的设备号个数
3 要注册的设备号就是 0x12300000
0x12300001
0x12300002
@name:名称
返回值: 0 - 成功,负数 - 失败。
'unregister_chrdev_region'
void unregister_chrdev_region(dev_t from, unsigned count)
功能:注销设备号
参数:同register_chrdev_region
返回值:无
/** 代码演示 - char_drv.c **/#include <linux/init.h>#include <linux/module.h>#include <linux/fs.h> // 正常情况下找到使用函数的地方,头文件信息可全拷MODULE_LICENSE ("GPL");dev_t dev; // 存储设备号unsigned int major = 200; // 主设备号unsigned int minor = 0; // 次设备号int __init char_drv_init (void){ /* 注册设备号 */ // dev = major << 20 | minor; // 设备号 // #define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi)) dev = MKDEV (major, minor); register_chrdev_region (dev, 1, "jiangyuan-1610"); return 0;}void __exit char_drv_exit (void){ /* 注销设备号 */ unregister_chrdev_region (dev, 1); }module_init (char_drv_init);module_exit (char_drv_exit);/** Makefile **/obj-m+= char_drv.oall:make -C /home/tarena/jy/driver/kernel M=$(PWD) modulescp *.ko ../../rootfs -vclean:make -C /home/tarena/jy/driver/kernel M=$(PWD) clean验证:
#:' insmod char_drv.ko
#:'cat /proc/devices
200 jiangyuan-1610
3.1.2 动态注册设备号
由内核帮我们挑一个未被使用的设备号,和我们自己选中的次设备号拼成设备号。
【动态注册设备号的方法】
'alloc_chrdev_region'
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)
功能:动态分配一个设备号范围
参数:
@dev:传出参数 → 用于返回注册的多个设备号中的第一个。
@baseminor:自己规定的起始次设备号
@count:连续注册的设备号个数
@name:名称
返回值: 0 - 成功,负数 - 失败。
假如:
baseminor = 100
内核给分配的主设备号假如是 200
count = 3
相当于向内核注册以下设备号:
200 << 20 | 100
200 << 20 | 100 + 1
200 << 20 | 100 + 2
'unregister_chrdev_region'
void unregister_chrdev_region(dev_t from, unsigned count)
功能:注销设备号 ('静态和动态均是其注销')
参数:同register_chrdev_region
返回值:无
/** 代码演示 - char_drv.c **/#include <linux/init.h>#include <linux/module.h>#include <linux/fs.h>MODULE_LICENSE ("GPL");dev_t dev; // 存储设备号unsigned int major = 0; // 主设备号unsigned int minor = 100; // 次设备号int __init char_drv_init (void){ if (major) { /* 静态注册 */ dev = MKDEV (major, minor); register_chrdev_region (dev, 1, "jiangyuan-1610"); } else { /* 动态注册 */ alloc_chrdev_region (&dev, minor, 1, "jiangyuan-alloc-1610"); } return 0;}void __exit char_drv_exit (void){ /* 注销设备号 */ unregister_chrdev_region (dev, 1); }module_init (char_drv_init);module_exit (char_drv_exit);验证:
#:' insmod char_drv.ko
#:'cat /proc/devices
#:'rmmod char_drv.ko
#:' cat /proc/devices
3.2 【操作函数集合】
完成一个字符设备驱动就是实例化一个 cdev 结构体。
实例化一个cdev过程,主要工作就是实现其对应的操作函数的集合。
struct file_operations {
read ()
write ()
open ()
...
};
以上数据结构是所有字符设备驱动要实现的函数的合集,再去完成某个具体硬件字符设备驱动时,只需要实现其中的一部分函数就可以了。(根据具体的情况)
3.3 内核中提供操作cdev的API
struct cdev{
dev
file_operation
...
};
'cdev_init'// cdev的初始化函数
void cdev_init(struct cdev *cdev, const struct file_operations *fops)
功能:初始化一个设备结构体
参数:
@cdev:要初始化的设备结构体变量地址
@fops:要操作的设备的结构体变量地址
返回值:无
'cdev_add' // 注册cdev
int cdev_add(struct cdev *p, dev_t dev, unsigned count)
功能:添加一个字符设备到系统
参数:
@p:要注册到系统中去的cdev地址
@dev:对应的设备号
@count:连续注册的个数,一个就取1
返回值: 0 - 成功,负数 - 失败。
'cdev_del' // 注销cdev
void cdev_del(struct cdev *p)
功能:删除一个cdev字符设备
参数:
@p:要从系统注销的cdev地址
返回值:无
/** 代码演示 - **/
(暂略)
实验步骤:
1) 安装查看模块
#:' insmod char_drv.ko
#:'cat /proc/devices
244 ...
2) 创建设备文件
#:'mknod /dev/leds c 244 100
3) 写一个test.c
$:' vi test.c
$:' arm-cortex_a9-linux-gnueabi-gcc test.c -o test
$:' cp test ../../rootfs/
$:'./test
enter led_open success...
open /dev/leds successed...
now is closing device!enter led_release success...
实验总结:
没有增加新的系统调用,但也调用到了open和release函数...
设备文件时用户态程序调用硬件驱动的媒介(c major minor)。
<tips>
linux内核uImage:
'make menuconfig---> Kernel hacking ---> show timing information on printks
当选中这个选项后,启动内核,会在日志信息前面加上时间戳。取消选中,即可取消时间戳。
0 0
- Driver:模块参数、系统调用、字符设备驱动框架
- 字符设备驱动模块
- 字符设备驱动框架
- Linux设备驱动第二天(数组参数传递,模块相互调用、printk、内核GPIO函数、系统调用)
- linux驱动开发之字符设备框架 -调用过程分析
- 模块驱动添加字符设备驱动
- Linux字符设备驱动框架
- linux 字符设备驱动框架
- Linux字符设备驱动框架
- linux ------ 字符设备驱动框架
- 内核字符设备驱动框架
- linux 字符设备驱动框架
- 字符设备驱动基本框架
- 字符设备驱动框架分析
- Linux字符设备驱动框架
- SylixOS字符设备驱动框架
- 字符设备驱动框架1
- Linux字符设备驱动框架
- Objective-C 类,数据成员,函数成员,对象,
- Spring+SpringMVC+MyBatis+easyUI整合基础篇(三)搭建步骤
- Python代码优化实践
- 【华为面试】将输入的数字转换为大写形式
- javaScript 中变量
- Driver:模块参数、系统调用、字符设备驱动框架
- jqGrid单元格/行编辑模式下getRowData如何获取数据行
- hdu 2108 Shape of HDU
- Java 版 Prim 算法求最小生成树
- java wait用法详解
- NTP注意事项
- 配置YUM源的步骤
- Objective-C 运行时编程指南 之 Declared Properties
- AccessibilityService 一个类似辅助的类