1. 程式人生 > >Linux字元裝置驅動註冊三種方法以及核心分析

Linux字元裝置驅動註冊三種方法以及核心分析

       Linux驅動是使用者訪問底層硬體的橋樑,驅動有可以簡單分成三類:字元裝置、塊裝置、網路裝置。其中最多的是字元裝置,其中字元裝置的註冊方法主要有三種:雜項設備註冊、早期字元設備註冊、標準字元設備註冊。以及詳細介紹各類方法註冊。

開發環境:

PC:WMworkstation 12  執行Ubuntu12 32位虛擬機器

開發板:友善之臂Tiny4412 (EXYNOS4412 Cortex-A9)

Linux核心版本:Linux 3.5

PC核心閱讀器:SourceInsight 

一、雜項裝置(misc device):

在核心路徑下include\linux\miscdevice.h檔案有以下內容:

struct miscdevice  {     int minor;     const char *name;     const struct file_operations *fops;     struct list_head list;     struct device *parent;     struct device *this_device;     const char *nodename;     umode_t mode; };

extern int misc_register(struct miscdevice * misc); extern int misc_deregister(struct miscdevice *misc);

通過上述函式的宣告可知雜項裝置的註冊函式為:misc_register,登出函式為misc_deregister,通過sourceinsight搜尋參考程式碼:

可知要使用一個雜項裝置必須要有一個struct miscdevice 結構體,根據miscdevice.h結構體的定義和相關核心程式碼可知:

需要至少實現結構體內部三個引數:

   int minor;//次裝置號(主裝置號預設為10,其中雜項裝置是通過早期字元裝置靜態新增到核心中,在早期裝置說明)    const char *name;//裝置名稱,dev下建立的裝置節點名稱    const struct file_operations *fops;//檔案操作指標,為使用者層與驅動層訪問的介面

以上很明顯的闡述了驅動註冊之前需要的準備工作:對struct miscdevice結構體進行定義並且賦值,然而內部引數並非這麼簡單:struct file_operations 還巢狀一層結構體,為檔案操作集合結構體。根據核心程式碼,我們可以找到相關所有檔案操作集合的函式介面:

位於核心根目錄下:include\linux下的Fs.h檔案下定義了檔案操作集合函式介面。通過在驅動層實現這些介面以便對驅動層的訪問。通過上述分析,雜項設備註冊方式很簡單的暫時概括為:1、準備工作:結構體的定義與賦值、介面函式的實現。2、將結構體傳入雜項設備註冊函式,實現註冊。3、使用者層函式的編寫。

講到這裡可能還不理解使用者層如何通過檔案操作集合定義的函式介面去實現訪問核心驅動,以一個簡單的程式碼為例:

static struct file_operations fops_led= {     .open=open_led,     .write=write_led,     .read=read_led,     .release=release_led, };

static struct miscdevice misc_led= {     .minor=MISC_DYNAMIC_MINOR, /*自動分配次裝置號*/     .name="tiny4412_led",      /*裝置節點的名稱*/     .fops=&fops_led           /*檔案操作集合*/ };

在驅動層定義的檔案操作集合為:open、write、read、release。

open:當用戶層通過open(dev\裝置結點)時對應驅動層實現的open函式會被呼叫,當用戶層通過open會產生一個檔案描述符,使用者層再通過檔案描述符去read、write操作時,會對應呼叫驅動層的read、write,通過這些函式的來訪問驅動。相關程式碼:

驅動層:

#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/miscdevice.h>
#include <linux/fs.h>
static int open_led (struct inode *my_inode, struct file *my_file)
{
	printk("open_led呼叫成功!\n");
	return 0;	
}
static ssize_t read_led(struct file *my_file, char __user *buff, size_t cnt, loff_t *loff)
{
	printk("read_led呼叫成功!\n");
	return 0;
}

static ssize_t write_led(struct file *my_file, const char __user *buff, size_t cnt, loff_t *loff)
{
	printk("write_led呼叫成功!\n");
	return 0;
}

static int release_led(struct inode *my_inode, struct file *my_file)
{
    printk("release_led呼叫成功!\n");
	return 0;
}

static struct file_operations fops_led=
{
	.open=open_led,
	.write=write_led,
	.read=read_led,
	.release=release_led,
};

static struct miscdevice misc_led=
{
	.minor=MISC_DYNAMIC_MINOR, /*自動分配次裝置號*/
	.name="tiny4412_led",      /*裝置節點的名稱*/
	.fops=&fops_led           /*檔案操作集合*/
};

static int __init tiny4412_led_init(void)
{
	int err;
	err=misc_register(&misc_led);  //雜項設備註冊函式
	if(err<0)
	{
		printk("提示: 驅動安裝失敗!\n");
		return err;
	}
    printk("提示: 驅動安裝成功!\n");
    return err;
}

static void __exit tiny4412_led_exit(void)
{
	int err;
	err=misc_deregister(&misc_led);  //雜項設備註銷函式
	if(err<0)
	{
		printk("提示: 驅動解除安裝失敗!\n");
	}
    printk("提示: 驅動解除安裝成功!\n");
}
module_init(tiny4412_led_init);  /*指定驅動的入口函式*/
module_exit(tiny4412_led_exit); /*指定驅動的出口函式*/
MODULE_LICENSE("GPL");       /*指定驅動許可證*/

使用者層:

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int main(int argc,char **argv)
{
	int fd;
	if(argc!=2)
	{
		printf("執行方式: ./app <裝置檔案>\n");
		return 0;
	}
	
	fd=open(argv[1],O_RDWR);
	if(fd<0)
	{
		printf("%s 裝置檔案開啟失敗!\n",argv[1]);
		return 0;
	}
	
	int data;
	read(fd,&data,4);
	write(fd,&data,4);
	close(fd);
	return 0;
}

那麼問題又來了,misc_register怎麼工作的呢?跳到misc_register函式裡面:

在driver\char下的misc.c檔案中,主要任務是先初始化連結串列,由於整個連結串列在檔案開始已經靜態初始化了:

所以第一步只需判斷新新增的裝置是否已經存在,通過list_for_each_entry遍歷連結串列與新新增的裝置對比,這個是個巨集原型是一個for迴圈主要是遍歷作用。當裝置是新的再執行第二步是否是自動註冊次裝置號,通過傳入結構體成員minor 是否為 MISC_DYNAMIC_MINOR巨集來判斷,是的話找到定位一個未被註冊的次裝置號,並且對minor賦值。第三步通過MKDEV函式將主裝置號與次裝置號合成為裝置的裝置號。第四步建立裝置節點,裝置節點是訪問驅動的一個介面,通過函式device_create建立, 將misc->name成員作為節點名稱,所以對結構體賦值時最好能體現裝置的特徵。第五步也是最後一步將此裝置新增到核心通過函式list_add。

然後精彩的地方來了,在misc.c檔案中發現了一個系統初始化自動執行的函式:subsys_initcall(misc_init);很顯然,這個初始化關乎到了雜項裝置能否正常執行,在這裡先埋下伏筆,其函式為:

二、早期字元裝置的註冊

在剛剛的include\linux下Fs.h裡面還有一個函式用於註冊字元裝置,這個函式也在misc.c裡面呼叫過,可知雜項裝置也是通過這個函式來實現註冊:

static inline int register_chrdev(unsigned int major,//主裝置號

                                                const char *name,//裝置結點名稱                                                 const struct file_operations *fops)

函式的返回值為:主裝置號

通過sourceinsight可以檢視函式的函式內部內容:

static inline int register_chrdev(unsigned int major, const char *name,  const struct file_operations *fops) {     return __register_chrdev(major, 0, 256, name, fops); }

可知在Fs.h裡面只是再呼叫了一個__register_chrdev函式並且把所有的引數都給了這另一個函式。在跳轉到__register_chrdev函式裡面:

裡面又呼叫了__register_chrdev_region返回了一個cd,這個cd是幹嘛呢?再跳轉到__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--) {/*  chrdevs 存放字元裝置的總個數結構體 */             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;     strlcpy(cd->name, name, sizeof(cd->name));

    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); 這個函式有點長,其實主要功能就是以下部分:

如果傳入的major為0,則經過一個for迴圈遍歷所有的chrdevs結構體陣列:

static struct char_device_struct {     struct char_device_struct *next;     unsigned int major;     unsigned int baseminor;     int minorct;     char name[64];     struct cdev *cdev;        /* will die */ } *chrdevs[CHRDEV_MAJOR_HASH_SIZE]; 每一位成員為一個裝置的節點資訊,組成一個連結串列通過這個for迴圈遍歷這個連結串列來找到沒有使用的主裝置號,那很明顯,當傳入進來的major為0,會自動分配主裝置號,這個major也就是register_chrdev的第一個引數。並且當沒找到時會報錯。接著這個函式將主裝置號裝載在struct char_device_struct結構體內部進行返回,繼續在__register_chrdev函式裡面操作:

再次判斷得到的結構體,並且再次封裝結構體,通過cdev_add新增到核心。通過上面的分析,register_chrdev主要是用於生成一個主裝置號,由雜項裝置可知雜項裝置的主裝置號為10,那是否也是通過register_chrdev函式註冊的主裝置號,在misc.c檔案裡面我們發現了一個雜項裝置初始化的函式:misc_init其函式體為:

static int __init misc_init(void) {     int err;

#ifdef CONFIG_PROC_FS     proc_create("misc", 0, NULL, &misc_proc_fops);/*    互動式檔案系統  */ #endif     misc_class = class_create(THIS_MODULE, "misc");/* 建立一個裝置類  */     err = PTR_ERR(misc_class);     if (IS_ERR(misc_class))         goto fail_remove;

    err = -EIO;     if (register_chrdev(MISC_MAJOR,"misc",&misc_fops))/* 註冊字元裝置 在/sys/class/建立子目錄*/         goto fail_printk;     misc_class->devnode = misc_devnode;     return 0;

fail_printk:     printk("unable to get major %d for misc devices\n", MISC_MAJOR);     class_destroy(misc_class); fail_remove:     remove_proc_entry("misc", NULL);     return err; }

很顯然,裡面呼叫了register_chrdev在major.h資料夾裡面定義#define MISC_MAJOR        10,這就是雜項裝置的主裝置號為10的原因了,在前面我們介紹過早期字元裝置register_chrdev註冊他並沒有像雜項裝置一樣呼叫device_create來產生一個裝置節點,所以,如何使用這個函式產生的主裝置號呢?

這個問題想要達成的效果就是在/dev/下生成裝置節點,剛好在linux下有一個建立裝置結點的命令:mknod,用法:

mknod Name {b|c} major minor    :Name裝置名稱   b|c塊裝置還是字元裝置 major主裝置號 minor次裝置號

那麼,問題又來了裝置號是個坑,但是我們可以通過列印函式列印建立的主裝置號,然後通過mknod建立節點,測試程式碼如下:

#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/miscdevice.h>
#include <linux/fs.h>
#include <linux/device.h>
#include <linux/gpio.h>
#include <mach/gpio.h>
#include <plat/gpio-cfg.h>
static int open_led (struct inode *my_inode, struct file *my_file)
{
	printk("open_led呼叫成功!\n");
	return 0;	
}
static ssize_t read_led(struct file *my_file, char __user *buff, size_t cnt, loff_t *loff)
{
	printk("read_led呼叫成功!\n");
	return cnt;
}
static ssize_t write_led(struct file *my_file, const char __user *buff, size_t cnt, loff_t *loff)
{
	printk("write_led呼叫成功!\n");
	return cnt;
}
static int release_led(struct inode *my_inode, struct file *my_file)
{

    printk("release_led呼叫成功!\n");
	return 0;
}
static struct file_operations fops_led=
{
	.open=open_led,
	.write=write_led,
	.read=read_led,
	.release=release_led,
};
static unsigned int major;
static int __init tiny4412_led_init(void)  /*insmod xxx.ko*/
{
	major=register_chrdev(0,"led", &fops_led);
	printk("major:%d\n",major);
    printk("提示:驅動安裝成功!\n");
}
static void __exit tiny4412_led_exit(void)
{
	unregister_chrdev(major,"led");
    printk("提示: 驅動解除安裝成功!\n");
}
module_init(tiny4412_led_init);    /*指定驅動的入口函式*/
module_exit(tiny4412_led_exit);   /*指定驅動的出口函式*/
MODULE_LICENSE("GPL");       /*指定驅動許可證*/

將此生成.ko檔案上傳到開發板insmod安裝驅動後出行:

提示生成的主裝置號為250,現在我們用mknod生成兩個裝置節點:

通過ls /dev/檢視生成的裝置節點:

再生成一個節點,其中主裝置號一致,次裝置號不一致

生成的兩個裝置節點,現在通過使用者層去訪問驅動,使用者層程式碼為:

#include<stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int main(int argc ,char ** argv)
{
	int i=0,count,err;
	if(argc!=2)
	{
		printf("usage: ./app <device>\n");
		return 0;
	}
	err=open(argv[1],2);
	if(err<0)
	{
		printf("device open fail!\n");
		return 0;
	}
	while(1)
	{
		read(err,&count,1);
		sleep(1);
		
		write(err,&count,1);
		sleep(1);
	}
	close(err);
}

上述執行結果為可得到以下結論:

結論:register_chrdev只是單純地建立了一個可以使用到的主裝置號並且沒有建立任何裝置節點,當通過mknod產生的節點只是通過主裝置號去訪問驅動,次裝置號不一樣仍然是訪問主裝置號的驅動。

上面可以看得出,如果使用這種方式去註冊的話一個裝置佔用一個主裝置號,相比雜項裝置來說有點浪費資源,因為理論來說雜項裝置可以註冊255個並且它只佔用一個主裝置號,其實我們通過雜項裝置可以看出我們可以模仿核心雜項設備註冊方式來使得早期字元裝置更加高效性,那要模仿雜項裝置,那就在來分析下雜項裝置的函式。由上述結論可知倘若雜項裝置採用的是早期字元設備註冊方式去註冊驅動的話他是怎樣做到在相同的主裝置號的情況下去訪問不同的驅動,下面我們在misc.c資料夾裡面先分析下雜項裝置怎樣通過主裝置號10來區分不同的驅動。

首先,在misc_register函式裡面主要執行的是通過建立一個數據結構:連結串列,用連結串列來儲存各個次裝置的資訊,並且每次註冊新的裝置的時候都會遍歷連結串列去找到未被使用的裝置號,當裝置號合成之後便將這個新的連結串列節點新增到連結串列中。這便完成一次新的裝置新增,但是這裡只是體現了對次裝置號的新建並沒有如何去實現區分次裝置。

其次我們使用者層對驅動的介面操作一般是先由open函式得來的檔案描述符,也就是說當開啟裝置節點時,一定是訪問雜項裝置10的主驅動,用上面的例子來說就是都是訪問註冊時填入的檔案操作集合實現的底層介面函式,由此可以提出一個推論:雜項裝置一定實現某種函式介面,如open。到在這裡,重點來來了,通過檢視misc.c檔案發現有檔案操作集合結構體:

static const struct file_operations misc_fops = {     .owner        = THIS_MODULE,     .open        = misc_open,     .llseek        = noop_llseek, };老鐵,沒看錯,就是open函式的實現,那就看看這個open到底寫了啥。

先說明一點,註冊的雜項裝置一定會訪問這個open函式,為啥呢?因為上面的例子充分說明主裝置號都是訪問同一個驅動。那接著看open函式有什麼文章。忘了介紹一個知識點,就是open函式的形參:struct inode * inode可以通過這個來獲取當前開啟的次裝置號(使用者層open的那個裝置節點的次裝置號),獲得的方法為:minor = iminor(inode);函式傳入這個形參便可以獲得,再通過下面的for迴圈:

通過這個for迴圈遍歷整個連結串列找到這個連結串列中與當前開啟的次裝置號一致的裝置結構體指標,當存在時,將這個裝置的檔案操作集合調出來賦值給new_fops。之後做的東西更加厲害了通過open的第二個形參可以獲得當前註冊的次裝置號的fops,也就是說這個引數的成員fops為對應註冊時某個次裝置的檔案操作集合,這便是我們想要的次裝置對應的驅動程式碼,也就是說在這裡呼叫改變檔案操作集合的指向,將共有的驅動裡面通過開啟的次裝置號找到註冊時寫入的資訊裡面的檔案操作集合,然後在共有的驅動裡面改變當前的檔案集合指向對應次裝置的檔案集合便可以,具體操作如下:

此時本來是呼叫了公共的open現在通過這個open將檔案操作集合修改了,並且有通過這個open跳轉到次裝置號註冊的open,由於其餘的檔案操作集合函式形參都有一個struct file結構體,其指向都以發生改變,這邊使得指向了不同的驅動程式。以下便是形象的總結:

下面我們寫一個早期裝置框架的程式碼:

早期字元裝置底層框架程式碼:

#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/miscdevice.h>
#include <linux/fs.h>
#include <linux/device.h>
static unsigned int char_dev_major; /*存放主裝置號*/
static struct class *char_dev_class;   /*定義類*/
static LIST_HEAD(char_device_list); /*建立連結串列頭*/
/*
自定義字元裝置結構體
*/
static struct char_device
{
	int minor;
	const char *name;
	const struct file_operations *fops;
	struct list_head list;
};
static int open_char_dev (struct inode *my_inode, struct file *my_file)
{
	/*1. 獲取次裝置號*/
	int minor = iminor(my_inode); /*得到次裝置號*/
	/*2. 查詢次裝置號對應的檔案操作集合*/
	struct char_device *c;
	list_for_each_entry(c, &char_device_list, list)  
	{
		if (c->minor == minor) 
		{
			my_file->f_op=c->fops;	 /*改變檔案操作集合的指向*/
			my_file->f_op->open(my_inode,my_file); /*呼叫底層使用者寫的open函式*/
		}
	}
	return 0;	
}
static struct file_operations fops_char_dev=
{
	.open=open_char_dev
};
/*
向外提供的字元設備註冊函式
*/
int char_device_register(struct char_device * char_device)
{
	struct char_device *c;
	dev_t dev;
	/*1. 初始化連結串列頭*/
	INIT_LIST_HEAD(&char_device->list); 
	/*2. 遍歷連結串列*/
	list_for_each_entry(c, &char_device_list, list)  
	{
		if (c->minor == char_device->minor) 
		{
			printk("次裝置號%d衝突!\n",c->minor);
			return -1;
		}
	}
	/*3. 合成裝置號*/
	dev = MKDEV(char_dev_major, char_device->minor);
	printk("主裝置號:%d,次裝置號:%d\n",char_dev_major,char_device->minor);
	/*4. 在/dev下生成裝置節點檔案*/
	device_create(char_dev_class,NULL,dev,NULL, "%s", char_device->name,char_device->minor);
	/*5. 新增裝置到連結串列*/
	list_add(&char_device->list, &char_device_list);
}
/*
向外提供的字元設備註銷函式
*/
int char_device_deregister(struct char_device *char_device)
{
	/*1. 刪除連結串列節點*/
	list_del(&char_device->list);
	/*2. 刪除裝置節點檔案*/
	device_destroy(char_dev_class, MKDEV(char_dev_major, char_device->minor));
}
EXPORT_SYMBOL(char_device_register);
EXPORT_SYMBOL(char_device_deregister);
static int __init char_dev_init(void)  /*insmod xxx.ko*/
{
	int err;
	/*1.早期設備註冊方式*/
	char_dev_major=register_chrdev(0,"char_device",&fops_char_dev); 
	/*2. 建立類*/
	char_dev_class = class_create(THIS_MODULE, "tiny4412_char_dev");
	printk("char_dev_major=%d\n",char_dev_major);
    printk("提示: 字元裝置驅動框架安裝成功!\n");
    return err;
}
static void __exit char_dev_exit(void)
{
	int err;
	/*1. 設備註銷*/
	unregister_chrdev(char_dev_major,"char_dev");
	/*2. 登出類*/
	class_destroy(char_dev_class);
    printk("提示: 字元裝置驅動框架解除安裝成功!\n");
}
module_init(char_dev_init);    /*指定驅動的入口函式*/
module_exit(char_dev_exit);   /*指定驅動的出口函式*/
MODULE_LICENSE("GPL");       /*指定驅動許可證*/

兩個使用早期字元設備註冊的驅動程式碼,兩個獨立的驅動函式:

device1.c:

#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/miscdevice.h>
#include <linux/fs.h>
#include <linux/device.h>
#include <linux/cdev.h>
#include <linux/slab.h> 
#include <linux/gpio.h>
#include <mach/gpio.h>
#include <plat/gpio-cfg.h>
extern int char_device_register(struct char_device * char_device);
extern int char_device_deregister(struct char_device *char_device);
static struct char_device
{
	int minor;
	const char *name;
	const struct file_operations *fops;
	struct list_head list;
};
static int device1_open(struct inode *my_inode, struct file *my_file)
{
	printk("device1_open呼叫成功1!\n");
	return 0;	
}

static ssize_t device1_read(struct file *my_file, char __user *buff, size_t cnt, loff_t *loff)
{
	printk("device1_read呼叫成功!1\n");
	return cnt;
}

static ssize_t device1_write(struct file *my_file, const char __user *buff, size_t cnt, loff_t *loff)
{
	printk("device1_write呼叫成功!\n");
	return cnt;
}

static int device1_release(struct inode *my_inode, struct file *my_file)
{
    printk("device1_release呼叫成功1!\n");
	return 0;
}
static struct file_operations fops=
{
	.open=device1_open,
	.write=device1_write,
	.read=device1_read,
	.release=device1_release,
};
static struct char_device misc_beep=
{
	.minor=10,
	.name="device1",
	.fops=&fops,
};
static int __init device1_init(void) 
{
	char_device_register(&misc_beep);
 	printk("提示:device1驅動安裝成功!\n");
    return 0;
}
static void __exit device1_exit(void)
{
	char_device_deregister(&misc_beep);
    printk("提示: device1驅動解除安裝成功!\n");
}
module_init(device1_init);    /*指定驅動的入口函式*/
module_exit(device1_exit);   /*指定驅動的出口函式*/
MODULE_LICENSE("GPL");       /*指定驅動許可證*/

device2.c:

#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/miscdevice.h>
#include <linux/fs.h>
#include <linux/device.h>
#include <linux/cdev.h>
#include <linux/slab.h> 
#include <linux/gpio.h>
#include <mach/gpio.h>
#include <plat/gpio-cfg.h>
extern int char_device_register(struct char_device * char_device);
extern int char_device_deregister(struct char_device *char_device);
static struct char_device
{
	int minor;
	const char *name;
	const struct file_operations *fops;
	struct list_head list;
};
static int device2_open(struct inode *my_inode, struct file *my_file)
{
	printk("device2_open呼叫成功1!\n");
	return 0;	
}

static ssize_t device2_read(struct file *my_file, char __user *buff, size_t cnt, loff_t *loff)
{
	printk("device2_read呼叫成功!1\n");
	return cnt;
}

static ssize_t device2_write(struct file *my_file, const char __user *buff, size_t cnt, loff_t *loff)
{
	printk("device2_write呼叫成功!\n");
	return cnt;
}

static int device2_release(struct inode *my_inode, struct file *my_file)
{
    printk("device2_release呼叫成功1!\n");
	return 0;
}
static struct file_operations fops=
{
	.open=device2_open,
	.write=device2_write,
	.read=device2_read,
	.release=device2_release,
};
static struct char_device misc_beep=
{
	.minor=11,
	.name="device2",
	.fops=&fops,
};
static int __init device2_init(void) 
{
	char_device_register(&misc_beep);
 	printk("提示:device2驅動安裝成功!\n");
    return 0;
}
static void __exit device2_exit(void)
{
	char_device_deregister(&misc_beep);
    printk("提示: device2驅動解除安裝成功!\n");
}
module_init(device2_init);    /*指定驅動的入口函式*/
module_exit(device2_exit);   /*指定驅動的出口函式*/
MODULE_LICENSE("GPL");       /*指定驅動許可證*/

使用者層函式跟上面的一樣,現在開始執行程式了:

首先第一步:對驅動框架進行安裝:

安裝框架成功,系統給的主裝置號為250,並且檢視dev下的裝置節點並沒我們想要建立的裝置節點,這是因為我們還沒有呼叫早期字元裝置的註冊函式,這個函式在device1.c和device2.c裡面呼叫。接著安裝device1.ko和device2.ko:

可以看到成功按照程式新增的資訊去註冊,並且獲得次裝置號產生對應的設別節點device1和device2,這裡能夠體現雜項裝置是通過註冊函式產生的節點。現在最具有對比性的操作來了,上面有個例子是用mknod來建立裝置節點的,忘記了可以翻上去看一下,那個例子正好建立的裝置號一致,但是他用同樣的使用者層程式碼開啟不同的裝置節點訪問的都是同樣的一個驅動,那這個呢?那就看結果:

這結果就很nice了,他訪問的是兩個不一樣的驅動程式,實現的次裝置號的獨立程式訪問,這樣節省了早期裝置的資源,其實這個程式還是不能夠驗證是先呼叫公共的open函式來達到切換fops(檔案操作集合),我們char_device.c的open進行修改:

然後重新編譯安裝驅動:

使用者層app執行開啟相同主裝置號的裝置節點前都是訪問同一個公共的驅動open,再進行分支切換。

結論:雜項裝置的驅動其實是完善的早期字元裝置的一個特例,固定主裝置號為10,也是通過早期字元設備註冊的,對早期字元裝置封裝後再核心載入時初始化,其實上面的框架也可以新增在核心,每一套都是自己的“雜項裝置”。

上述分析中利用__register_chrdev_region函式來得到主裝置號,在char_dev.c中還設有幾個函式也是通過呼叫__register_chrdev_region函式來實現註冊的:

那這兩個API函式是幹嘛用的呢,這就是標準字元裝置的註冊函式。

三、標準字元設備註冊

在早期字元設備註冊框架裡面有涉及到cdev結構體,在__register_chrdev裡面,__register_chrdev_region獲得主裝置號後通過cdev_alloc函式建立一個struct cdev結構,將所有的註冊資訊儲存在這個結構體裡面再通過cdev_add新增向核心註冊字元裝置,在建立cdev結構體後對結構體賦值有一個初始化函式:cdev_init通過這個函式新增到cdev連結串列裡面去,也就是說,標準字元裝置是通過cdev連結串列來儲存所有的註冊資訊,在char_dev.c檔案裡面:

通過這個函式將cdev結構初始化,就是對結構體成員賦值,其中一個成員是檔案操作集合,所以這裡麵包括了對ops成員的賦值,新增連結串列之後,可以通過alloc_chrdev_region來動態申請裝置號,第二個傳參:baseminor 次裝置號的起始地址,第三個傳參:count是連續申請的個數。也就是說他可以連續申請幾個裝置並且可以通過device_create連續建立節點:示例程式碼:

#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/miscdevice.h>
#include <linux/fs.h>
#include <linux/device.h>
#include <linux/cdev.h>
#include <linux/slab.h>  
#include <linux/gpio.h>
#include <mach/gpio.h>
#include <plat/gpio-cfg.h>
static struct class *dev_class; 
static dev_t dev_num;    /*裝置號*/
static struct cdev *cdev;
static int device1_open(struct inode *my_inode, struct file *my_file)
{
	printk("device1_open呼叫成功1!\n");
	return 0;	
}

static ssize_t device1_read(struct file *my_file, char __user *buff, size_t cnt, loff_t *loff)
{
	printk("device1_read呼叫成功!1\n");
	return cnt;
}

static ssize_t device1_write(struct file *my_file, const char __user *buff, size_t cnt, loff_t *loff)
{
	printk("device1_write呼叫成功!\n");
	return cnt;
}

static int device1_release(struct inode *my_inode, struct file *my_file)
{
    printk("device1_release呼叫成功1!\n");
	return 0;
}

static struct file_operations device1_fops=
{
	.open=device1_open,
	.read=device1_read,
	.write=device1_write,
	.release=device1_release,
};
/*static struct file_operations device2_fops=
{
	.open=device2_open,
	.read=device2_read,
	.write=device2_write,
	.release=device2_release,
};*/

static int __init std_device_init(void)  
{
	int i; 
    /*1. 動態分配cdev結構*/
	cdev=cdev_alloc();
	/*2. cdev初始化*/
	cdev_init(cdev,&device1_fops);
	/*3. 動態分配裝置號*/
	alloc_chrdev_region(&dev_num,10,2,"led_dev");
	printk("dev_num:%d\n",dev_num);
	/*4. 新增字元裝置到核心*/
	cdev_add(cdev,dev_num,2);  //新增裝置到核心	
	/*5. 建立裝置類,建立成功會在: /sys/class  目錄下建立子目錄*/
	dev_class = class_create(THIS_MODULE, "exynos4412");
	for(i=0;i<2;i++)
	{
		/*6. 在/dev下生成裝置節點檔案*/
		device_create(dev_class,NULL,dev_num+i,NULL, "device%d",i+1);
	}
	if(IS_ERR(dev_class))
	{
		printk("dev_class error!\n");
		goto Class_Error;
	}
	printk("提示: 裝置號分配成功!\n");
Class_Error:
    return 0;
}
static void __exit std_device_exit(void)
{
	int i;
	for(i=0;i<2;i++)
	{
		device_destroy(dev_class,dev_num+i);//刪除裝置節點
	}
	class_destroy(dev_class);//登出類
	unregister_chrdev_region(dev_num,2);//登出裝置
	kfree(cdev);//釋放空間
    printk("提示: 裝置號驅動解除安裝成功!\n");
}

module_init(std_device_init);    /*指定驅動的入口函式*/
module_exit(std_device_exit);    /*指定驅動的出口函式*/
MODULE_LICENSE("GPL");       /*指定驅動許可證*/

使用者層程式碼不變,結果:

沒有安裝驅動之前沒有裝置節點,現在通過insmod安裝驅動:

安裝後在/dev/下生成兩個裝置節點:device1和device2,現在通過使用者層app開啟裝置節點:

這個結果有在程式上理解很簡單,因為在cdev結構體初始化的時候只使用了一個fops檔案操作集合,所以兩個都是訪問同一個檔案操作集合的函式,可以在對應檔案從操作集合裡面建立分支或者和早期字元裝置一樣實現優化,這裡不再重複。

結論:標準字元設備註冊相對前面幾個註冊方式而言底層內容豐富的多,基本上都是呼叫相對比較底層的函式,相比於雜項裝置,他能夠自己分配主裝置號,比雜項裝置靈活,並且能連續註冊好幾個裝置也可以註冊一個裝置;相對早期字元裝置,沒有個繁瑣的框架,簡單單一,但是使用還是要靈活一點才可以。