標籤:

linux內核中,對於字元設備/塊設備/匯流排/設備/驅動等概念,如何正確理解?

linux內核術語里,有字元/塊/網路設備分類;也有匯流排、設備、驅動分類;還有input、V4L2等等。這些術語之間是個什麼關係?

目前我看了一些源碼,對於字元設備、塊設備應該是這樣:用戶程序open()文件時,vfs會根據文件屬性,自動切換到字元(塊)設備驅動。

站在應用程序的角度,從上往下看內核:如何理解其他術語之間的關係呢?


更新一下:

自己的一點新理解(可能與題 主的 「站在應用的角度」不合,但如果真要在應用程序的角度,大多是不需要考慮設備類型的,只需要關心操作系統抽象出來的介面就可以了。除非你的應用有自己的io管理,比如oracle的asm,操作系統只需要把系統識別到的原始設備暴露給oracle,並把設備的屬主給oracle,oracle會有自己的抽象層來對原始設備進行管理自己完成和自己上層應用的交互):

@in nek 曾提到的我們 受 到國內教材的 毒害(字元設備 的順序訪問,塊設備 的隨機訪問 ),國外 好像 也是這樣的教材:

a: 先貼 一個 quora 的大家看一下:https://www.quora.com/What-is-difference-between-character-device-driver-block-device-driver-in-linux

b: 國外也強調 字元 設備 的 線性 訪問 ,見wiki上面的 說法:

Device file

順便提一下關於 block devices最後一段的內容:

「Most systems create both block and character devices to represent hardware like hard disks. FreeBSD and Linux notably do not; the former has removed support for block devices,
while the latter creates only block devices. In Linux, to get a character device for a disk one must use the "raw" driver, though one can get the same effect as opening a character device by opening the block device with the Linux-specific O_DIRECT flag.」

大體翻譯一下: 多數系統為代表性的硬體 既創建塊設備(文件)也創建字元設備(文件)比如硬碟,freebsd 和linux沒這樣做,前者移除了對塊設備的支持,而後者則僅僅為硬碟創建 塊設備(文件),在linux中,想以字元設備來使用硬碟 ,必須使用"raw" driver(記不記得安裝oracle 或RAC時,有時客戶會要求把硬碟作成raw式的字元設備 ?),這樣就可以像操作字元設備一樣打開塊設備(通過使用linux 特殊的操作 flag。最近讀到,這個O_DERECT的標誌,就是在進行讀寫操作的時候,不使用操作系統的cache)。

個人理解為 某些硬體 是可以同時 做成字元設備和塊設備 的,不知 Linux中是RAM 是否在這些硬體之列。

c: 在 另一本書中《深入理解Linux內核第三版》(此書以2.6內核為例,也提到2.4內核作比較),也看到的 提起 「字元設備的 順序訪問」: 並提到了操作系統為字元設備建立循環緩衝: 也許,利用了緩衝 應用程序才可以操作字元設備 進行lseek。

######################以下是原答案####################################

1.在操作系統的角度,塊設備/字元設備/網路設備 都是io的一種,可以從這些設備讀寫數據。

2.區別在於

a: 字元設備最小的讀寫單位是一個字元的長度-- 即兩個位元組(16位),這樣對我們造成的印象是當我們通過鍵盤輸入的時候或者是讀終端數據的時候,如果不考慮緩衝,數據可以每次以16個bit被送給字元設備。 ( 原答案中我是這樣寫的,經知友in nek指正這樣說法存在錯誤: 字元設備不能隨機讀寫,只能按順序讀寫,比如你依次在鍵盤輸入的內容,只會按你的輸入順序被系統接受。在比如操作系統向終端寫入數據,也是順序的在終端顯示。就像拿著碗喝豆漿,你只能從最上面的喝起,如果碗不漏的話。)

b:對於塊設備, 最小的讀寫單位是一個物理塊,對於磁碟來說是一個扇區(一般來說是512位元組),會給人造成「512位元組,好大一堆字元組成的一塊啊」的印象,猜想由於這樣的特性被操作系統設計時定義成塊設備 。(我的原答案:塊設備可以隨機讀寫,比如你可以讀寫系統下掛載的硬碟的任意位置的數據,就像吃一塊豆腐,你拿著刀可以吃這塊豆腐的任意部分。並且由於塊設備的訪問速度太慢,讀寫一次(硬碟)的cpu等待時間太長,系統會為塊設備在內存設立緩衝區,先把要寫入硬碟的數據寫入緩衝區,等滿足一定條件時一併寫入硬碟。)

c: 匯流排設備是連接各種設備的橋樑,比如lspci -t可以看到pci匯流排的設備連接樹,匯流排設備負責把塊設備/字元設備/網路設備 /cpu /內存 在硬體上是連通的,這樣當cpu發出指令「從硬碟的某個地方讀多少數據並把這些數據寫到終端」,「把鍵盤輸入的內容寫入到網卡」的時候,這些數據會通過匯流排被傳輸。

d:驅動是打開設備的方式和方法,比如吃豆腐,你可以拿刀切,這裡用刀切就是一種方法,喝豆漿可以用吸管。但刀切和吸管這兩種方式都要向系統註冊,系統將刀切和吃豆腐關聯,將吸管和和豆漿關聯。之後,當你需要吃豆腐時,系統會調用「刀切這個方法」。


你問的是站在應用程序的角度。

從應用程序的角度,你不需要知道什麼是匯流排。匯流排是內核用來管理設備和它的驅動的一種機制。

設備只有兩種,一種是字元設備,一種是塊設備。ls -l /dev,看到c開頭的就是字元設備,看到b開頭的就是塊設備。其他你提到的v4l,input等,都是字元設備。

字元設備通過open/read/write/close等系統調用來使用,如果它支持,也可以通過mmap等方式來直接像內存一樣訪問。每個字元設備都代表了它的那個硬體可以提供的功能,沒有固定的使用方法,這個你看看具體設備的文檔。

基本上我們可以認為字元設備是一種通用設備的代表,一般的設備都會被抽象為字元設備。塊設備則是一種專門為了和存儲IO系統介面的設備,它可以直接作為文件系統的基礎,例如,你可以通過mkfs在塊設備上創建文件系統等。

僅憑記憶回答,不保證正確,但作為基礎理解應該沒有什麼問題的。

----------------------2016-2-18補充-------------------------------------

作為「站在應用程序的角度」,題主的問題我回答完了。但看到好多其他的回答,比如 @唐浩然 的回答, @朱涵俊 都比較有嚴重的錯誤,我覺得有必要澄清一下。

(註:唐在答案中對我的說法進行了澄清:外國的教材也是他所說的回答,所以讀者可以去參考他的回答,我覺得這是一個概念演進的問題,不影響本文的邏輯,我這裡就不大改了)

首先,說唐的問題,他(不止他一位),認為字元設備是順序訪問的,塊設備是隨機訪問的。這個理解很可能是受中國教材的毒害,形成了固定的看法。但實際上,我問一句了:/dev/mem是字元設備還是塊設備?各位自己看看,這是字元設備,難道/dev/mem是要順序訪問的?這可是RAM,Random Access Memory。也許Unix最初設計的時候,是打算用字元設備來做串列設備,用塊設備來做隨機訪問設備,但這一點,可能幾十年前就已經不存在了。

字元設備,現在就是簡單的一種設備,你去看看它的回調函數,就知道它可以提供什麼功能了(基於4.5-rc1的代碼):

struct file_operations {
struct module *owner;
loff_t (*llseek) (struct file *, loff_t, int);
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);
ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);
int (*iterate) (struct file *, struct dir_context *);
unsigned int (*poll) (struct file *, struct poll_table_struct *);
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
int (*mmap) (struct file *, struct vm_area_struct *);
int (*open) (struct inode *, struct file *);
int (*flush) (struct file *, fl_owner_t id);
int (*release) (struct inode *, struct file *);
int (*fsync) (struct file *, loff_t, loff_t, int datasync);
int (*aio_fsync) (struct kiocb *, int datasync);
int (*fasync) (int, struct file *, int);
int (*lock) (struct file *, int, struct file_lock *);
ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
int (*check_flags)(int);
int (*flock) (struct file *, int, struct file_lock *);
ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
int (*setlease)(struct file *, long, struct file_lock **, void **);
long (*fallocate)(struct file *file, int mode, loff_t offset,
loff_t len);
void (*show_fdinfo)(struct seq_file *m, struct file *f);
#ifndef CONFIG_MMU
unsigned (*mmap_capabilities)(struct file *);
#endif
ssize_t (*copy_file_range)(struct file *, loff_t, struct file *,
loff_t, size_t, unsigned int);
int (*clone_file_range)(struct file *, loff_t, struct file *, loff_t,
u64);
ssize_t (*dedupe_file_range)(struct file *, u64, u64, struct file *,
u64);
};

只要那個設備實現了對應的函數,就有對應的功能,實現了mmap就能當內存來訪問,實現了llseek就可以跳躍訪問,實現fallocate就可以凌空截斷,一點壓力沒有。

塊設備呢?塊設備的回調是這樣的:

struct block_device_operations {
int (*open) (struct block_device *, fmode_t);
void (*release) (struct gendisk *, fmode_t);
int (*rw_page)(struct block_device *, sector_t, struct page *, int rw);
int (*ioctl) (struct block_device *, fmode_t, unsigned, unsigned long);
int (*compat_ioctl) (struct block_device *, fmode_t, unsigned, unsigned long);
long (*direct_access)(struct block_device *, sector_t, void __pmem **,
pfn_t *);
unsigned int (*check_events) (struct gendisk *disk,
unsigned int clearing);
/* -&>media_changed() is DEPRECATED, use -&>check_events() instead */
int (*media_changed) (struct gendisk *);
void (*unlock_native_capacity) (struct gendisk *);
int (*revalidate_disk) (struct gendisk *);
int (*getgeo)(struct block_device *, struct hd_geometry *);
/* this callback is with swap_lock and sometimes page table lock held */
void (*swap_slot_free_notify) (struct block_device *, unsigned long);
struct module *owner;
const struct pr_ops *pr_ops;
};

看懂了嗎?這個完全是為實現文件系統而做的。

接著說朱的問題,我一開始看得目瞪口呆,不知道為什麼他會有這樣的誤解,後來突然想明白了,他是把用戶態的設備和內核的Device給弄到一起了。

用戶態的device,我們一般就是指/dev目錄下的哪些設備文件,那些文件的作用是建立一個用戶程序到內核驅動的門戶。這些門戶背後,是驅動程序。

內核中,很早很早以前(我也懶得查是什麼時候),我們的驅動是這樣寫的:你寫一個動態模塊,插入到內核中,這個動態模塊就開始和要驅動的硬體進行通訊,然後註冊一個字元設備或者一個塊設備,你就可以把你的信息傳遞給用戶程序了。我印象中LDD2好像就是這樣的例子來教大家寫程序的。你們看看後面那位 @0x07c00 寫的程序,就是這種模型。

但這種模型太舊太舊了,Linux早就不是這個樣子的了。這個模型有個嚴重問題,我一個驅動註冊一個字元設備,我就知道我的硬體地址是什麼,那如果我有兩個一樣的設備,我要不要寫兩個驅動啊?

為了解決這個問題,應該是從2.5開始,Linux就引入了一個新的模型,就是所謂的device-bus-driver模型。簡單說,你有多少個硬體,初始化程序就建立多少個device,device就是個數據結構,裡面描述了硬體的信息,比如中斷號啦,IO地址啦什麼的。driver呢,才是原來那個動態模塊,它負責讀device提供過來的信息,然後用這個信息來初始化,並且調用register_chrdev()一類的函數,把設備的用戶態介面暴露到用戶態去。所以呢,內核並沒有取消字元設備,塊設備的支持,而是把這些東西分離成了device和driver,靠probe()過程去動態註冊了。

那麼bus是個什麼東西呢?bus在內核中稱為bus_type,它是用來匹配device和driver的,x86這個部分搞得比較ugly,我用arm來舉例。ARM很多設備是焊死在AMBA匯流排上的,或者說設備和CPU是直接連在一起的,沒有什麼匯流排發現功能,這種設備,Linux就在啟動的時候建立一個稱為Platform_bus的虛擬匯流排類型,BIOS是知道自己這個硬體有什麼設備的,它就在DTS里,或者通過ACPI介面,把這些設備一個個描述出來,這樣Linux的初始化代碼,通過of_xxx函數(針對DTS)或者acpi函數,讀到這些設備描述,然後創建成device,註冊給platform_bus。之後,內核初始化,一個個動態模塊就可以插入來,這些動態模塊也把自己註冊在platform_bus上,platform_bus就可以啟動它的匹配演算法(platform_bus的匹配演算法是用名稱),把device作為參數傳遞給驅動(的probe())函數,probe函數就可以註冊到各個子系統(字元設備,網路設備,塊設備,都行)了。

真正的匯流排控制器,比如pci-c,一開始也可以是個平台設備,這個匯流排控制器作為device被probe,probe中就可以對下屬的硬體進行枚舉,就可以創建更多的device,那些device和pci_bus的driver匹配,就可以實現對這些設備的驅動,這樣就會形成一個樹狀的結構,完成整個設備樹的構建。

我這裡忽略了不少細節,還有疑問就跟貼問吧。

最後鏈接一個姊妹問題:

linu?x input設備模型問題? - in nek 的回答


目前內核已經取消塊設備,字元設備。一般用匯流排概念,匯流排上面接設備,如常見的pci匯流排,pci設備。之前內核採用字元設備,塊設備,有很多弊端,如網路設備效率比較低,塊設備有緩存,容易丟失數據,性能降低。一般的內核都取消了字元設備,塊設備模式,或者因為兼容性繼續保留。改用匯流排驅動模式。網路設備作為一個設備類型存在,而不是之前的字元設備或者塊設備。


以下內容來自我個人的理解,畢竟很久沒有琢磨這些東西了,稍微把之前學習的東西整理了下下。

不能完全回答這個問題,答案中任何敘述不對的地方還望大家指出來。

我就說說字元設備吧。

原始的字元設備大概就是像磁帶一樣的設備吧,廣義上來講是一種按位元組訪問的設備,用戶程序可以通過標準文件操作來訪問設備。

以上這些說法,網上很多,接下來,我就結合代碼來說說吧。

代碼是這樣的:我們為一個存在於系統中設備寫一個驅動程序。系統是識別到了的,但是就是無法使用,為什麼系統是識別了的但是無法使用?因為對於用戶來講,是無法直接使用一個系統中的硬體的,要使用這個添加的硬體,我們就要安裝這個設備的驅動程序。通過驅動程序來操作這個設備。

一,驅動程序

#include "linux/kernel.h"
#include "linux/module.h"
#include "linux/fs.h"
#include "linux/init.h"
#include "linux/types.h"
#include "linux/errno.h"
#include "linux/uaccess.h"
#include "linux/kdev_t.h"
#define MAX_SIZE 1024

static int my_open(struct inode *inode, struct file *file);
static int my_release(struct inode *inode, struct file *file);
static ssize_t my_read(struct file *file, char __user *user, size_t t, loff_t *f);
static ssize_t my_write(struct file *file, const char __user *user, size_t t, loff_t *f);

static char message[MAX_SIZE] = "-------This is dmesg info--------!";
static int device_num = 0;
static int counter = 0;
static int mutex = 0;
static char* devName = "NewDev";//設備名

struct file_operations pStruct =
{ open:my_open, release:my_release, read:my_read, write:my_write, };

/* 初始化模塊,向系統註冊這個設備,*/
int init_module()
{
int ret;
/********************************************************
* 第一個參數是0,告訴系統此時的設備號是由系統自己選一個可用的來分配,
* 第二個參數是新設備註冊時的設備名字,
* 第三個參數是指向file_operations的指針,
*******************************************************/
ret = register_chrdev(0, devName, pStruct);
if (ret &< 0) { printk("regist failure! "); return -1; } else { printk("the device has been registered! "); device_num = ret; printk("&<1&>Devices major number %d.
", device_num);
return 0;

}
}
/* 註銷 */
void cleanup_module()
{
unregister_chrdev(device_num, devName);
printk("unregister it success!
");
}

static int my_open(struct inode *inode, struct file *file)
{
if (mutex)
return -EBUSY;
mutex = 1;
printk("&<1&>main device : %d
", MAJOR(inode-&>i_rdev));
printk("&<1&>slave device : %d
", MINOR(inode-&>i_rdev));
printk("&<1&>%d times to call the device
", ++counter);
try_module_get(THIS_MODULE);
return 0;
}

static int my_release(struct inode *inode, struct file *file)
{
printk("Device released!
");
module_put(THIS_MODULE);
mutex = 0;//開鎖
return 0;
}

static ssize_t my_read(struct file *file, char __user *user, size_t t, loff_t *f)
{
/*從內核空間拷貝到用戶空間*/
if(copy_to_user(user,message,sizeof(message)))
{
return -EFAULT;
}
return sizeof(message);
}

static ssize_t my_write(struct file *file, const char __user *user, size_t t, loff_t *f)
{
/*從用戶空間到內核空間*/
if(copy_from_user(message,user,sizeof(message)))
{
return -EFAULT;
}
return sizeof(message);
}

有了這個驅動程序後,結合下面的Makefile來編譯:

# If KERNELRELEASE is defined, weve been invoked from the
# kernel build system and can use its language.
ifeq ($(KERNELRELEASE),)
# Assume the source tree is where the running kernel was built
# You should set KERNELDIR in the environment if its elsewhere
KERNELDIR ?= /lib/modules/$(shell uname -r)/build
# The current directory is passed to sub-makes as argument
PWD := $(shell pwd)
modules:
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
modules_install:
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules_install
clean:
rm -rf *.o *~ core .depend .*.cmd *.ko *.mod.c .tmp_versions
.PHONY: modules modules_install clean
else
# called from kernel build system: just declare what our modules are
obj-m := devDrv.o
endif

使用make命令來編譯。

編譯好了後,就要將這個驅動安裝到系統中去,Linux中就是把這個驅動程序裝載到系統中。

裝載:

到系統中看看

成功了。

通過dmesg命令可以看到驅動程序已經向系統註冊了這個設備。

設備號都看的出來了。

我這裡設備號是248。

二、接下來要做的就是添加一個設備。

給系統添加一個設備,在Linux中,「一切都是文件」,那麼,設備也是文件,所以,添加的這個設備自然也就以一個文件的形式呈現出來----設備文件。

關於主設備號和從設備號。簡單的說主設備號就是標識這一類的設備,驅動程序只管這一類的設備。而從設備號才是真正標識單個設備的,即就是某一類設備中的個體。

現在開始建立這個設備,也就是給這個設備分配從設備號:

是的,已經創建好了:

--------------------------------------------------------------------

到現在,來看看我們做了什麼。

首先是寫了一個驅動程序,這個驅動程序向系統註冊了這個設備。

然後,向系統添加了這個設備。

接下來就差個應用程序了,畢竟這樣才完整。

用戶通過應用程序操作這個設備。現在,這個設備說是一個設備,但是在Linux下就是一個文件,對這個文件進行讀寫操作也就是對這個設備進行讀寫操作了。

fd = open("/dev/NewDev", O_RDWR | O_NONBLOCK);

這樣就可以打開了。

盡綿薄之力,希望可以幫到你。

以上。


任何linux user層的操作都是對文件的操作,之後陷入kernel,通過VFS訪問對應的驅動,也就是塊設備/字元設備驅動;塊設備/字元設備通過各種協議(I2C,mipi,spi等)訪問硬體。platform是一種虛擬匯流排,是為了管理設備提出來的。舉個例子,platform下有i2c、block等;i2c下面有touchpanel,sensor等。這樣在整體上就有一個系統的樹形層次結構。input是另外一種分類,touchpanel,sensor等也掛載在input下面,因為他們既屬於I2C設備,有屬於input設備,沒有衝突的。I2C設備說明使用了I2C匯流排,input設備說明使用了input子系統,例如觸摸屏(touchpanel)如下:linux user態--&>input子系統---&>設備驅動---&>I2C匯流排---&>硬體。


匯流排、設備、驅動是整體組織管理設備的框架,一個kobject對應一個/sys目錄下一個目錄,一個kobject_attribute對應一個文件,用戶層可以修改/sys文件修改驅動參數。input、V4L2等是驅動框架,對應一類設備比如platform_dev對應片內外設,V4L2對應攝像頭,frambuff對應顯卡,滑鼠 鍵盤等對應input;字元/塊/網路設備分類是按設備類型劃分;


在經過 @in nek 大神鄙視後,發憤圖強啃代碼,自己覺得能分清這些區別,跟大家分享一下。

目前呢,我更加認為應該現在APP的角度來看內核和驅動。

以字元設備為例,內核是通過設備文件以fops的介面暴露給用戶,而fops是可以直接或間接的操作到硬體。因此,按照這樣的"一脈相承",是壓根沒有匯流排/設備/驅動啥事的。

那問題來了,該如何理解匯流排/設備/驅動呢?

我是這樣理解:這些東西就是個工具,都是為了更好/更有效/更方便的完成用戶與硬體之間的一脈相承。如果狹義的理解更方便:匯流排/設備/驅動根本就不是"驅動"。


推薦閱讀:

全局變數什麼時候在內存中申請空間呢?
linux CFS調度演算法的疑問?
如何理解Linux一切皆是文件?這當中又有哪些值得後人借鑒的思想?
為什麼微內核系統在PC不如宏內核普及?
platform driver 是作為一個怎樣的概念出現的?

TAG:Linux內核 |