Linux字符設備驅動實現


Linux字符設備驅動實現

要求

編寫一個字符設備驅動,並利用對字符設備的同步操作,設計實現一個聊天程序。可以有一個讀,一個寫進程共享該字符設備,進行聊天;也可以由多個讀和多個寫進程共享該字符設備,進行聊天

主要過程

實現

字符驅動設備

/*
參考:深入淺出linux設備驅動開發
*/
#include <linux/module.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <asm/uaccess.h>
#include <linux/wait.h>
#include <linux/semaphore.h>
#include <linux/sched.h>
#include <linux/cdev.h>
#include <linux/types.h>
#include <linux/kdev_t.h>
#include <linux/device.h>

#define MAXNUM 100
#define MAJOR_NUM 456 //主設備號 ,沒有被使用

struct dev{
    struct cdev devm; //字符設備
    struct semaphore sem;
    wait_queue_head_t outq;//等待隊列,實現阻塞操作
    int flag; //阻塞喚醒標志
    char buffer[MAXNUM+1]; //字符緩沖區
    char *rd,*wr,*end; //讀,寫,尾指針
}globalvar;
static struct class *my_class;
int major=MAJOR_NUM;

static ssize_t globalvar_read(struct file *,char *,size_t ,loff_t *);
static ssize_t globalvar_write(struct file *,const char *,size_t ,loff_t *);
static int globalvar_open(struct inode *inode,struct file *filp);
static int globalvar_release(struct inode *inode,struct file *filp);
/*
結構體file_operations在頭文件 linux/fs.h中定義,用來存儲驅動內核模塊提供的對設備進行各種操作的函數的指針。
該結構體的每個域都對應着驅動內核模塊用來處理某個被請求的事務的函數的地址。
設備"gobalvar"的基本入口點結構變量gobalvar_fops 
*/
struct file_operations globalvar_fops =
{
    /*
    標記化的初始化格式這種格式允許用名字對這類結構的字段進行初始化,這就避免了因數據結構發生變化而帶來的麻煩。
    這種標記化的初始化處理並不是標准 C 的規范,而是對 GUN 編譯器的一種(有用的)特殊擴展
    */
    //用來從設備中獲取數據. 在這個位置的一個空指針導致 read 系統調用以 -EINVAL("Invalid argument") 失敗. 一個非負返回值代表了成功讀取的字節數( 返回值是一個 "signed size" 類型, 常常是目標平台本地的整數類型).
    .read=globalvar_read,
    //發送數據給設備. 如果 NULL, -EINVAL 返回給調用 write 系統調用的程序. 如果非負, 返回值代表成功寫的字節數.
    .write=globalvar_write,
    //盡管這常常是對設備文件進行的第一個操作, 不要求驅動聲明一個對應的方法. 如果這個項是 NULL, 設備打開一直成功, 但是你的驅動不會得到通知.
    .open=globalvar_open,
    //當最后一個打開設備的用戶進程執行close ()系統調用時,內核將調用驅動程序的release () 函數:release 函數的主要任務是清理未結束的輸入/輸出操作、釋放資源、用戶自定義排他標志的復位等。
    .release=globalvar_release,
};
//內核模塊的初始化
static int globalvar_init(void)
{

    /*
    int register_chrdev(unsigned int major, unsigned int baseminor,unsigned int count, const char *name,const struct file_operations *fops)
    返回值提示操作成功還是失敗。負的返回值表示錯誤;0 或正的返回值表明操作成功。
    major參數是被請求的主設備號,name 是設備的名稱,該名稱將出現在 /proc/devices 中, 
    fops是指向函數指針數組的指針,這些函數是調用驅動程序的入口點,
    在 2.6 的內核之后,新增了一個 register_chrdev_region 函數,
    它支持將同一個主設備號下的次設備號進行分段,每一段供給一個字符設備驅動程序使用,使得資源利用率大大提升,
    */    
    int result = 0;
    int err = 0;
    /*
    宏定義:#define MKDEV(major,minor) (((major) << MINORBITS) | (minor))
    成功執行返回dev_t類型的設備編號,dev_t類型是unsigned int 類型,32位,用於在驅動程序中定義設備編號,
    高12位為主設備號,低20位為次設備號,可以通過MAJOR和MINOR來獲得主設備號和次設備號。
    在module_init宏調用的函數中去注冊字符設備驅動
    major傳0進去表示要讓內核幫我們自動分配一個合適的空白的沒被使用的主設備號
    內核如果成功分配就會返回分配的主設備號;如果分配失敗會返回負數
    */
    dev_t dev = MKDEV(major, 0);
    if(major)
    {
        //靜態申請設備編號
        result = register_chrdev_region(dev, 1, "charmem");
    }
    else
    {
        //動態分配設備號
        result = alloc_chrdev_region(&dev, 0, 1, "charmem");
        major = MAJOR(dev);
    }
    if(result < 0)
        return result;
    /*
    file_operations這個結構體變量,讓cdev中的ops成員的值為file_operations結構體變量的值。
    這個結構體會被cdev_add函數想內核注冊cdev結構體,可以用很多函數來操作他。
    如:
    cdev_alloc:讓內核為這個結構體分配內存的
    cdev_init:將struct cdev類型的結構體變量和file_operations結構體進行綁定的
    cdev_add:向內核里面添加一個驅動,注冊驅動
    cdev_del:從內核中注銷掉一個驅動。注銷驅動
    */
    //注冊字符設備驅動,設備號和file_operations結構體進行綁定
    cdev_init(&globalvar.devm, &globalvar_fops);
    /*
    #define THIS_MODULE (&__this_module)是一個struct module變量,代表當前模塊,
    與那個著名的current有幾分相似,可以通過THIS_MODULE宏來引用模塊的struct module結構,
    比如使用THIS_MODULE->state可以獲得當前模塊的狀態。
    現在你應該明白為啥在那個歲月里,你需要毫不猶豫毫不遲疑的將struct usb_driver結構里的owner設置為THIS_MODULE了吧,
    這個owner指針指向的就是你的模塊自己。
    那現在owner咋就說沒就沒了那?這個說來可就話長了,咱就長話短說吧。
    不知道那個時候你有沒有忘記過初始化owner,
    反正是很多人都會忘記,
    於是在2006年的春節前夕,在咱們都無心工作無心學習等着過春節的時候,Greg堅守一線,去掉了 owner,
    於是千千萬萬個寫usb驅動的人再也不用去時刻謹記初始化owner了。
    咱們是不用設置owner了,可core里不能不設置,
    struct usb_driver結構里不是沒有owner了么,
    可它里面嵌的那個struct device_driver結構里還有啊,設置了它就可以了。
    於是Greg同時又增加了usb_register_driver()這么一層,
    usb_register()可以通過將參數指定為THIS_MODULE去調用它,所有的事情都挪到它里面去做。
    反正usb_register() 也是內聯的,並不會增加調用的開銷。
    */
    globalvar.devm.owner = THIS_MODULE;
    err = cdev_add(&globalvar.devm, dev, 1);
    if(err)
        printk(KERN_INFO "Error %d adding char_mem device", err);
    else
    {
        printk("globalvar register success\n");
        sema_init(&globalvar.sem,1); //初始化信號量
        init_waitqueue_head(&globalvar.outq); //初始化等待隊列
        globalvar.rd = globalvar.buffer; //讀指針
        globalvar.wr = globalvar.buffer; //寫指針
        globalvar.end = globalvar.buffer + MAXNUM;//緩沖區尾指針
        globalvar.flag = 0; // 阻塞喚醒標志置 0
    }
    /*
    定義在/include/linux/device.h
    創建class並將class注冊到內核中,返回值為class結構指針
    在驅動初始化的代碼里調用class_create為該設備創建一個class,再為每個設備調用device_create創建對應的設備。
    省去了利用mknod命令手動創建設備節點
    */
    my_class = class_create(THIS_MODULE, "chardev0");
    device_create(my_class, NULL, dev, NULL, "chardev0");
    return 0;
}
/*
在大部分驅動程序中,open 應完成如下工作:
● 遞增使用計數。--為了老版本的可移植性
● 檢查設備特定的錯誤(諸如設備未就緒或類似的硬件問題)。
● 如果設備是首次打開,則對其進行初始化。
● 識別次設備號,並且如果有必要,更新 f_op 指針。
● 分配並填寫被置於 filp->private_data 里的數據結構。
*/
static int globalvar_open(struct inode *inode,struct file *filp)
{
    try_module_get(THIS_MODULE);//模塊計數加一
    printk("This chrdev is in open\n");
    return(0);
}
/*
release都應該完成下面的任務:
● 釋放由 open 分配的、保存在 filp->private_data 中的所有內容。
● 在最后一次關閉操作時關閉設備。字符設備驅動程序
● 使用計數減 1。
如果使用計數不歸0,內核就無法卸載模塊。
並不是每個 close 系統調用都會引起對 release 方法的調用。
僅僅是那些真正釋放設備數據結構的 close 調用才會調用這個方法,
因此名字是 release 而不是 close。內核維護一個 file 結構被使用多少次的計數器。
無論是 fork 還是 dup 都不創建新的數據結構(僅由 open 創建),它們只是增加已有結構中的計數。
*/
static int globalvar_release(struct inode *inode,struct file *filp)
{
    module_put(THIS_MODULE); //模塊計數減一
    printk("This chrdev is in release\n");
    return(0);
}
static void globalvar_exit(void)
{
    device_destroy(my_class, MKDEV(major, 0));
    class_destroy(my_class);
    cdev_del(&globalvar.devm);
    /*
    參數列表包括要釋放的主設備號和相應的設備名。
    參數中的這個設備名會被內核用來和主設備號參數所對應的已注冊設備名進行比較,如果不同,則返回 -EINVAL。
    如果主設備號超出了所允許的范圍,則內核同樣返回 -EINVAL。
    */
    unregister_chrdev_region(MKDEV(major, 0), 1);//注銷設備
}
/*
ssize_t read(struct file *filp, char *buff,size_t count, loff_t *offp);
參數 filp 是文件指針,參數 count 是請求傳輸的數據長度。
參數 buff 是指向用戶空間的緩沖區,這個緩沖區或者保存將寫入的數據,或者是一個存放新讀入數據的空緩沖區。
最后的 offp 是一個指向“long offset type(長偏移量類型)”對象的指針,這個對象指明用戶在文件中進行存取操作的位置。
返回值是“signed size type(有符號的尺寸類型)”

主要問題是,需要在內核地址空間和用戶地址空間之間傳輸數據。
不能用通常的辦法利用指針或 memcpy來完成這樣的操作。由於許多原因,不能在內核空間中直接使用用戶空間地址。
內核空間地址與用戶空間地址之間很大的一個差異就是,用戶空間的內存是可被換出的。
當內核訪問用戶空間指針時,相對應的頁面可能已不在內存中了,這樣的話就會產生一個頁面失效
*/
static ssize_t globalvar_read(struct file *filp,char *buf,size_t len,loff_t *off)
{
    if(wait_event_interruptible(globalvar.outq,globalvar.flag!=0)) //不可讀時 阻塞讀進程
    {
        return -ERESTARTSYS;
    }
    /*
    down_interruptible 可以由一個信號中斷,但 down 不允許有信號傳送到進程。
    大多數情況下都希望信號起作用;否則,就有可能建立一個無法殺掉的進程,並產生其他不可預期的結果。
    但是,允許信號中斷將使得信號量的處理復雜化,因為我們總要去檢查函數(這里是 down_interruptible)是否已被中斷。
    一般來說,當該函數返回 0 時表示成功,返回非 0 時則表示出錯。
    如果這個處理過程被中斷,它就不會獲得信號量 , 因此,也就不能調用 up 函數了。
    因此,對信號量的典型調用通常是下面的這種形式:
    if (down_interruptible (&sem))
        return -ERESTARTSYS;
    返回值 -ERESTARTSYS通知系統操作被信號中斷。
    調用這個設備方法的內核函數或者重新嘗試,或者返回 -EINTR 給應用程序,這取決於應用程序是如何設置信號處理函數的。
    當然,如果是以這種方式中斷操作的話,那么代碼應在返回前完成清理工作。

    使用down_interruptible來獲取信號量的代碼不應調用其他也試圖獲得該信號量的函數,否則就會陷入死鎖。
    如果驅動程序中的某段程序對其持有的信號量釋放失敗的話(可能就是一次出錯返回的結果),
    那么其他任何獲取該信號量的嘗試都將阻塞在那里。
    */
    if(down_interruptible(&globalvar.sem)) //P 操作
    {
        return -ERESTARTSYS;
    }
    globalvar.flag = 0;
    printk("into the read function\n");
    printk("the rd is %c\n",*globalvar.rd); //讀指針
    if(globalvar.rd < globalvar.wr)
        len = min(len,(size_t)(globalvar.wr - globalvar.rd)); //更新讀寫長度
    else
        len = min(len,(size_t)(globalvar.end - globalvar.rd));
    printk("the len is %d\n",len);
    /*
    read 和 write 代碼要做的工作,就是在用戶地址空間和內核地址空間之間進行整段數據的拷貝。
    這種能力是由下面的內核函數提供的,它們用於拷貝任意的一段字節序列,這也是每個 read 和 write 方法實現的核心部分:
    unsigned long copy_to_user(void *to, const void *from,unsigned long count);
    unsigned long copy_from_user(void *to, const void *from,unsigned long count);
    雖然這些函數的行為很像通常的 memcpy 函數,但當在內核空間內運行的代碼訪問用戶空間時,則要多加小心。
    被尋址的用戶空間的頁面可能當前並不在內存,於是處理頁面失效的程序會使訪問進程轉入睡眠,直到該頁面被傳送至期望的位置。
    例如,當頁面必須從交換空間取回時,這樣的情況就會發生。對於驅動程序編寫人員來說,
    結果就是訪問用戶空間的任何函數都必須是可重入的,並且必須能和其他驅動程序函數並發執行。
    這就是我們使用信號量來控制並發訪問的原因.
    這兩個函數的作用並不限於在內核空間和用戶空間之間拷貝數據,它們還檢查用戶空間的指針是否有效。
    如果指針無效,就不會進行拷貝;另一方面,如果在拷貝過程中遇到無效地址,則僅僅會復制部分數據。
    在這兩種情況下,返回值是還未拷貝完的內存的數量值。
    如果發現這樣的錯誤返回,就會在返回值不為 0 時,返回 -EFAULT 給用戶。
    負值意味着發生了錯誤,該值指明發生了什么錯誤,錯誤碼在<linux/errno.h>中定義。
    比如這樣的一些錯誤:-EINTR(系統調用被中斷)或 -EFAULT (無效地址)。
    */
    if(copy_to_user(buf,globalvar.rd,len))
    {
        printk(KERN_ALERT"copy failed\n");
        /*
        up遞增信號量的值,並喚醒所有正在等待信號量轉為可用狀態的進程。
        必須小心使用信號量。被信號量保護的數據必須是定義清晰的,並且存取這些數據的所有代碼都必須首先獲得信號量。
        */
        up(&globalvar.sem);
        return -EFAULT;
    }
    printk("the read buffer is %s\n",globalvar.buffer);
    globalvar.rd = globalvar.rd + len;
    if(globalvar.rd == globalvar.end)
        globalvar.rd = globalvar.buffer; //字符緩沖區循環
    up(&globalvar.sem); //V 操作
    return len;
}
static ssize_t globalvar_write(struct file *filp,const char *buf,size_t len,loff_t *off)
{
    if(down_interruptible(&globalvar.sem)) //P 操作
    {
        return -ERESTARTSYS;
    }
    if(globalvar.rd <= globalvar.wr)
        len = min(len,(size_t)(globalvar.end - globalvar.wr));
    else
        len = min(len,(size_t)(globalvar.rd-globalvar.wr-1));
    printk("the write len is %d\n",len);
    if(copy_from_user(globalvar.wr,buf,len))
    {
        up(&globalvar.sem); //V 操作
        return -EFAULT;
    }
    printk("the write buffer is %s\n",globalvar.buffer);
    printk("the len of buffer is %d\n",strlen(globalvar.buffer));
    globalvar.wr = globalvar.wr + len;
    if(globalvar.wr == globalvar.end)
    globalvar.wr = globalvar.buffer; //循環
    up(&globalvar.sem);
    //V 操作
    globalvar.flag=1; //條件成立,可以喚醒讀進程
    wake_up_interruptible(&globalvar.outq); //喚醒讀進程
    return len;
}
module_init(globalvar_init);
module_exit(globalvar_exit);
MODULE_LICENSE("GPL");

讀者程序

#include<sys/types.h>
#include<unistd.h>
#include<sys/stat.h>
#include<stdio.h>
#include<fcntl.h>
#include<string.h>
int main()
{
    int fd,i;
    char msg[101];
    fd= open("/dev/chardev0",O_RDWR,S_IRUSR|S_IWUSR);
    if(fd!=-1)
    {
        while(1)
        {
            for(i=0;i<101;i++)
                msg[i]='\0';
            read(fd,msg,100);
            printf("%s\n",msg);
            if(strcmp(msg,"quit")==0)
            {
                close(fd);
                break;
            }
        }
    }
    else
    {
        printf("device open failure,%d\n",fd);
    }
    return 0;
}

寫者程序

#include<sys/types.h>
#include<unistd.h>
#include<sys/stat.h>
#include<stdio.h>
#include<fcntl.h>
#include<string.h>
int main()
{
    int fd;
    char msg[100];
    fd= open("/dev/chardev0",O_RDWR,S_IRUSR|S_IWUSR);
    if(fd!=-1)
    {
        while(1)
        {
            printf("Please input the globar:\n");
            scanf("%s",msg);
            write(fd,msg,strlen(msg));
            if(strcmp(msg,"quit")==0)
            {
                close(fd);
                break;
            }
        }
    }
    else
    {
        printf("device open failure\n");
    }
    return 0;
}

Makefile

ifneq ($(KERNELRELEASE),)
	obj-m := globalvar.o#obj-m 指編譯成外部模塊
else
	KERNELDIR := /lib/modules/$(shell uname -r)/build #定義一個變量,指向內核目錄
	PWD := $(shell pwd)
modules:
	$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
endif
clean:
	$(MAKE) -C $(KERNELDIR) M=$(PWD) clean

運行

運行:
make
insmod globalvar.ko
gcc read.c -o read
gcc write.c -o write
./read
./write

可dmesg查看打印信息

卸載:
//rm /dev/chardev0
rmmod globalvar


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM