上一篇我們介紹了字符設(shè)備架構(gòu)的大概原理、常用的數(shù)據(jù)結(jié)構(gòu)和函數(shù)。接下來,我們撰寫一個簡單的驅(qū)動程序和用戶程序,使用戶程序可以通過open、release、read、write等常用的文件操作函數(shù),來完成對驅(qū)動程序的打開、關(guān)閉、讀寫等控制。
編寫代碼前,我們再梳理一下思路。想要實現(xiàn)上述效果,我們的工作主要分為三大部分:
編寫驅(qū)動程序,并加載到內(nèi)核中,等待被用戶程序調(diào)用。
在console控制臺下使用mknod命令創(chuàng)建一個設(shè)備節(jié)點。
編寫用戶程序,通過調(diào)用設(shè)備節(jié)點間接控制驅(qū)動程序。
這三部分每一部分實現(xiàn)的具體步驟如下,下面這張圖只涉及到了模塊注冊時的操作和初始化操作,其他操作沒有反映出來。

下面我們來逐步實現(xiàn)一過程。
一、注冊設(shè)備號
首先使用cat /proc/device我們找到一個未被占用的主設(shè)備號,這里我們使用233作為新注冊設(shè)備的主設(shè)備號。

注冊設(shè)備號的操作在驅(qū)動程序中,通過調(diào)用register_chrdev_region函數(shù)完成。因此我們創(chuàng)建一個驅(qū)動程序cdev_driver.c,寫入下面的代碼。
#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
MODULE_LICENSE("GPL");
MODULE_AUTHOR("zz");
static int major = 233; //主設(shè)備號
static int minor = 0; //一系列次設(shè)備號中的第一個次設(shè)備號
static dev_t devno; //設(shè)備號
static int demo_init(void)
{
int rc;
devno = MKDEV(major, minor); //根據(jù)主設(shè)備號和次設(shè)備號合成設(shè)備號
rc = register_chrdev_region(devno, 1, "test"); //向系統(tǒng)中注冊設(shè)備號
if(rc < 0) {
pr_err("register_chrdev_region failed!");
return rc;
}
return 0;
}
static void demo_exit(void)
{
unregister_chrdev_region(devno, 1); //向系統(tǒng)中注銷設(shè)備號
return;
}
module_init(demo_init);
module_exit(demo_exit);
編寫Makefile。
ifneq ($(KERNELRELEASE),)
obj-m := cdev_driver.o
else
KDIR := /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)
all:
make -C $(KDIR) M=$(PWD) modules
clean:
rm -rf *.ko *.o *.mod.o *.symvers *.cmd *.mod.c *.order *.dwo *.mod.dwo *.mod
endif
編譯該文件,并在系統(tǒng)中加載該驅(qū)動模塊,可以看到系統(tǒng)中233的設(shè)備號被分配給名為“test”的設(shè)備。

二、實現(xiàn)file_operations結(jié)構(gòu)體
這一步同樣在驅(qū)動程序cdev_driver.c中,我們需要首先新建一個file_operations結(jié)構(gòu)體fops,并實現(xiàn)其中的open、release、read、write函數(shù)。
#define KMAX_LEN 32
static int demo_open(struct inode *ind, struct file *fp)
{
printk("demo open\n");
return 0;
}
static int demo_release(struct inode *ind, struct file *fp)
{
printk("demo release\n");
return 0;
}
static ssize_t demo_read(struct file *fp, char __user *buf, size_t size, loff_t *pos)
{
int rc;
char kbuf[KMAX_LEN] = "read test\n";
if (size > KMAX_LEN)
size = KMAX_LEN;
rc = copy_to_user(buf, kbuf, size); //將字符串kbuf拷貝到用戶空間下的buf中
if(rc < 0) {
return -EFAULT;
pr_err("copy_to_user failed!");
}
return size;
}
static ssize_t demo_write(struct file *fp, const char __user *buf, size_t size, loff_t *pos)
{
int rc;
char kbuf[KMAX_LEN];
if (size > KMAX_LEN)
size = KMAX_LEN;
rc = copy_from_user(kbuf, buf, size); //將用戶空間下的buf中的內(nèi)容拷貝到內(nèi)核空間中
if(rc < 0) {
return -EFAULT;
pr_err("copy_from_user failed!");
}
printk("%s", kbuf);
return size;
}
static struct file_operations fops = {
.open = demo_open,
.release = demo_release,
.read = demo_read,
.write = demo_write,
};
file_operations結(jié)構(gòu)體中的成員都是函數(shù)指針,我們的工作就是編寫四個函數(shù)demo_open、demo_release、demo_read、demo_write,并將它們分別賦值給.open、.release、.read、.write四個函數(shù)指針。其中demo_open和demo_release兩個函數(shù)比較簡單,打印兩句話即可,demo_read和demo_write分別實現(xiàn)讀寫操作。demo_read用于將內(nèi)核中定義的字符串“read test”讀出到buf中,并返回實際讀出的字符串長度size;demo_write用于向內(nèi)核中寫入一個長度為size的字符串buf。size不能超過限定的最大長度KMAX_LEN。
由于讀出和寫入操作是跨越用戶空間和內(nèi)核空間的,所以需要使用copy_to_user和copy_from_user兩個函數(shù)。前者用來從內(nèi)核空間拷貝到用戶空間,后者反之。
三、注冊字符設(shè)備結(jié)構(gòu)體
在寫好file_operations結(jié)構(gòu)體后,就可以調(diào)用cdev_init和cdev_add函數(shù),對字符設(shè)備結(jié)構(gòu)體進行初始化并加入系統(tǒng)了。在模塊卸載時,需要調(diào)用cdev_del移除字符設(shè)備結(jié)構(gòu)體。最終得到的完整的驅(qū)動程序的代碼如下。
#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>
MODULE_LICENSE("GPL");
MODULE_AUTHOR("zz");
static int major = 233;
static int minor = 0;
static dev_t devno;
#define KMAX_LEN 32
static int demo_open(struct inode *ind, struct file *fp)
{
printk("demo open\n");
return 0;
}
static int demo_release(struct inode *ind, struct file *fp)
{
printk("demo release\n");
return 0;
}
static ssize_t demo_read(struct file *fp, char __user *buf, size_t size, loff_t *pos)
{
int rc;
char kbuf[KMAX_LEN] = "read test\n";
if (size > KMAX_LEN)
size = KMAX_LEN;
rc = copy_to_user(buf, kbuf, size);
if(rc < 0) {
return -EFAULT;
pr_err("copy_to_user failed!");
}
return size;
}
static ssize_t demo_write(struct file *fp, const char __user *buf, size_t size, loff_t *pos)
{
int rc;
char kbuf[KMAX_LEN];
if (size > KMAX_LEN)
size = KMAX_LEN;
rc = copy_from_user(kbuf, buf, size);
if(rc < 0) {
return -EFAULT;
pr_err("copy_from_user failed!");
}
printk("%s", kbuf);
return size;
}
static struct file_operations fops = {
.open = demo_open,
.release = demo_release,
.read = demo_read,
.write = demo_write,
};
static struct cdev cd;
static int demo_init(void)
{
int rc;
devno = MKDEV(major, minor);
rc = register_chrdev_region(devno, 1, "test");
if(rc < 0) {
pr_err("register_chrdev_region failed!");
return rc;
}
cdev_init(&cd, &fops); //初始化字符設(shè)備結(jié)構(gòu)體
rc = cdev_add(&cd, devno, 1); //將字符設(shè)備結(jié)構(gòu)體加入系統(tǒng)中
if (rc < 0) {
pr_err("cdev_add failed!");
return rc;
}
return 0;
}
static void demo_exit(void)
{
cdev_del(&cd); //從系統(tǒng)中刪除字符設(shè)備結(jié)構(gòu)體
unregister_chrdev_region(devno, 1);
return;
}
module_init(demo_init);
module_exit(demo_exit);
四、應(yīng)用程序編寫
驅(qū)動程序完全編寫完畢,下面編寫應(yīng)用程序。在應(yīng)用程序中,我們要完成下面的操作:使用open函數(shù)打開字符設(shè)備,使用read函數(shù)從字符設(shè)備中讀出字符串“read test”,使用write函數(shù)向字符設(shè)備中寫入字符串“write test”,使用close釋放字符設(shè)備。我們將設(shè)備節(jié)點的名稱命名為test_chr_dev。代碼如下。
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
int main()
{
int rc;
char buf[32];
int fd = open("/dev/test_chr_dev", O_RDWR);
if (fd < 0) {
printf("open file failed!\n");
return -1;
}
read(fd, buf, 32);
printf("%s", buf);
write(fd, "write test\n", 32);
close(fd);
return 0;
}
使用gcc對代碼進行編譯。
gcc app.c -o app
五、程序運行
進入root模式,首先使用mknod指令向系統(tǒng)中創(chuàng)建設(shè)備節(jié)點。

查看dev目錄可以看到新增加的設(shè)備節(jié)點。
然后加載驅(qū)動模塊,再運行應(yīng)用程序??梢钥吹阶x功能正常。

dmesg打印一下內(nèi)核信息,可以看到設(shè)備打開、寫功能、釋放正常。
