版權(quán)說明:本文檔由用戶提供并上傳,收益歸屬內(nèi)容提供方,若內(nèi)容存在侵權(quán),請進行舉報或認領(lǐng)
文檔簡介
1、1,Linux 設(shè)備驅(qū)動設(shè)計 梁紅波kelvinema-,設(shè)備驅(qū)動概述,設(shè)備由兩部分組成,一個是被稱為控制器的電器部分,另一個是機械部分。 一組寄存器組被賦予到各個控制器。I/O端口包含4組寄存器,即狀態(tài)寄存器,控制寄存器,數(shù)據(jù)輸入寄存器,數(shù)據(jù)輸出寄存器。 狀態(tài)寄存器擁有可以被CPU讀取的(狀態(tài))位,用來 指示當(dāng)前命令是否執(zhí)行完畢,或者字節(jié)是否可以被讀出或?qū)懭耄约叭魏五e誤提示。 控制寄存器則用于啟動一條命令(指令)或者改變設(shè)備的(工作)模式。 數(shù)據(jù)輸入寄存器用于獲取輸入的數(shù)據(jù)。 數(shù)據(jù)輸出寄存器則向CPU發(fā)送結(jié)果。,設(shè)備驅(qū)動概述,操作系統(tǒng)是通過各種驅(qū)動程序來駕馭硬件設(shè)
2、備,它為用戶屏蔽了各種各樣的設(shè)備。 設(shè)備驅(qū)動程序是操作系統(tǒng)內(nèi)核和機器硬件之間的接口,系統(tǒng)調(diào)用是操作系統(tǒng)內(nèi)核和應(yīng)用程序之間的接口。 在應(yīng)用程序看來,硬件設(shè)備只是一個設(shè)備文件, 應(yīng)用程序可以象操作普通文件一樣對硬件設(shè)備進行操作.,4,設(shè)備驅(qū)動概述,驅(qū)動完成以下的功能: 對設(shè)備初始化和釋放. 把數(shù)據(jù)從內(nèi)核傳送到硬件和從硬件讀取數(shù)據(jù). 讀取應(yīng)用程序傳送給設(shè)備文件的數(shù)據(jù)和回送應(yīng)用程序請求的數(shù)據(jù). 檢測和處理設(shè)備出現(xiàn)的錯誤.,5,設(shè)備驅(qū)動概述,無操作系統(tǒng)的設(shè)備驅(qū)動 有操作系統(tǒng)的設(shè)備驅(qū)動,Embedded OS,Hardware,不帶操作系統(tǒng)軟件結(jié)構(gòu) 帶操作系統(tǒng)軟件結(jié)構(gòu),Driver,6,Linux設(shè)備驅(qū)動
3、,7,Linux設(shè)備驅(qū)動,用戶級的程序使用內(nèi)核提供的標(biāo)準系統(tǒng)調(diào)用來與內(nèi)核通訊,這些系統(tǒng)調(diào)用有:open(), read(), write(), ioctl(), close() 等等。 Linux的內(nèi)核是映射到每一個進程的高1G空間。每一個用戶進程運行時都好像有一份內(nèi)核的拷貝,每當(dāng)用戶進程使用系統(tǒng)調(diào)用時,都自動地將運行模式從用戶級轉(zhuǎn)為內(nèi)核級,此時進程在內(nèi)核的地址空間中運行。,8,Linux設(shè)備驅(qū)動,Linux內(nèi)核使用“設(shè)備無關(guān)”的I/O子系統(tǒng)來為所有的設(shè)備服務(wù)。 每個設(shè)備都提供標(biāo)準接口給內(nèi)核,盡可能地隱藏了自己的特性。 用戶程序使用一些基本的系統(tǒng)調(diào)用從設(shè)備讀取數(shù)據(jù)并且將它們存入緩沖的例子。我們
4、可以看到,每當(dāng)一個系統(tǒng)調(diào)用被使用時,內(nèi)核就轉(zhuǎn)到相應(yīng)的設(shè)備驅(qū)動例程來操縱硬件。,Linux設(shè)備驅(qū)動,Linux操作系統(tǒng)把設(shè)備納入文件系統(tǒng)的范疇來管理。 每個設(shè)備在Linux系統(tǒng)上看起來都像一個文件,它們存放在/dev目錄中,稱為設(shè)備節(jié)點。 對文件操作的系統(tǒng)調(diào)用大都適用于設(shè)備文件。,10,Linux設(shè)備驅(qū)動,Linux下設(shè)備的屬性 設(shè)備的類型:字符設(shè)備、塊設(shè)備、網(wǎng)絡(luò)設(shè)備 主設(shè)備號:標(biāo)識設(shè)備對應(yīng)的驅(qū)動程序。一般“一個主設(shè)備號對應(yīng)一個驅(qū)動程序” 次設(shè)備號:每個驅(qū)動程序負責(zé)管理它所驅(qū)動的幾個硬件實例,這些硬件實例則由次設(shè)備號來表示。同一驅(qū)動下的實例編號,用于確定設(shè)備文件所指的設(shè)備。 可通過ls l “設(shè)
5、備文件名”命令查看設(shè)備的主次設(shè)備號,以及設(shè)備的類型。,11,Linux設(shè)備驅(qū)動,Linux設(shè)備驅(qū)動程序是一組由內(nèi)核中的相關(guān)子例程和數(shù)據(jù)組成的I/O設(shè)備軟件接口。 每當(dāng)用戶程序要訪問某個設(shè)備時,它就通過系統(tǒng)調(diào)用,讓內(nèi)核代替它調(diào)用相應(yīng)的驅(qū)動例程。這就使得控制從用戶進程轉(zhuǎn)移到了驅(qū)動例程,當(dāng)驅(qū)動例程完成后,控制又被返回至用戶進程。,12,一些重要的數(shù)據(jù)結(jié)構(gòu),大部分驅(qū)動程序涉及三個重要的內(nèi)核數(shù)據(jù)結(jié)構(gòu): 文件操作file_operations結(jié)構(gòu)體 文件對象file結(jié)構(gòu)體 索引節(jié)點inode結(jié)構(gòu)體,13,一些重要的數(shù)據(jù)結(jié)構(gòu),文件操作結(jié)構(gòu)體file_operations 結(jié)構(gòu)體file_operations
6、在頭文件 linux/fs.h中定義,用來存儲驅(qū)動內(nèi)核模塊提供的對設(shè)備進行各種操作的函數(shù)的指針。 結(jié)構(gòu)體的每個域都對應(yīng)著驅(qū)動模塊用來處理某個被請求的事務(wù)的函數(shù)的地址。 struct file_operations struct module *owner; ssize_t(*read) (struct file *, char _user *, size_t, loff_t *); ssize_t(*write) (struct file *, const char _user *, size_t, loff_t *); 。 ,14,一些重要的數(shù)據(jù)結(jié)構(gòu),file_operations重要的成員
7、 Struct module *owner ,指向擁有該結(jié)構(gòu)體的模塊的指針。內(nèi)核使用該指針維護模塊使用計數(shù)。 方法llseek用來修改文件的當(dāng)前讀寫位置,把新位置作為返回值返回。loff_t是在LINUX中定義的長偏移量 方法read用來從設(shè)備中讀取數(shù)據(jù)。非負返回值表示成功讀取的直接數(shù)。 方法write向設(shè)備發(fā)送數(shù)據(jù)。 方法ioctl提供一種執(zhí)行設(shè)備特定命令的方法。,15,一些重要的數(shù)據(jù)結(jié)構(gòu),file_operations重要的成員 unsigned int (*poll) (struct file *, struct poll_table_struct *); 系統(tǒng)調(diào)用select和poll
8、的后端實現(xiàn),用這兩個系統(tǒng)調(diào)用來查詢 設(shè)備是否可讀寫,或是否處于某種狀態(tài)。如果poll為空,則驅(qū)動設(shè)備會被認為即可讀又可寫,返回值是一個狀態(tài)掩碼。 int (*mmap) (struct file *, struct vm_area_struct *);將設(shè)備內(nèi)存映射到進程地址空間,16,一些重要的數(shù)據(jù)結(jié)構(gòu),file_operations重要的成員 驅(qū)動內(nèi)核模塊是不需要實現(xiàn)每個函數(shù)的。相對應(yīng)的file_operations的項就為 NULL。 Gcc的語法擴展,使得可以定義該結(jié)構(gòu)體: struct file_operations fops = read: device_read, write:
9、device_write, open: device_open, release: device_release ; 沒有顯示聲明的結(jié)構(gòu)體成員都被gcc初始化為NULL。,17,一些重要的數(shù)據(jù)結(jié)構(gòu),file_operations重要的成員 標(biāo)準C的標(biāo)記化結(jié)構(gòu)體的初始化方法: struct file_operations fops = .read = device_read, .write = device_write, .open = device_open, .release = device_release ; 推薦使用該方法,提高移植性,方法允許對結(jié)構(gòu)體成員進行重新排列。沒有顯示聲明的結(jié)
10、構(gòu)體成員同樣都被gcc初始化為NULL。 指向結(jié)構(gòu)體file_operations的指針通常命名為fops。,18,一些重要的數(shù)據(jù)結(jié)構(gòu),文件對象file結(jié)構(gòu)體 文件對象file代表著一個打開的文件。進程通過文件描述符fd與已打開文件的file結(jié)構(gòu)相聯(lián)系。進程通過它對文件的線性邏輯空間進行操作。例如:file-f_op-read(); Struct file 在中定義。 指向結(jié)構(gòu)體struct file的指針通常命名為filp,或者file。建議使用文件指針filp。,19,一些重要的數(shù)據(jù)結(jié)構(gòu),文件對象file結(jié)構(gòu)體的成員 Struct file_operations *f_op; 與文件相關(guān)的
11、操作結(jié)構(gòu)體指針。與文件相關(guān)的操作是在打開文件的時候確定下來的,也就是確定該指針的值。可在需要的時候,改變指針?biāo)赶虻奈募僮鹘Y(jié)構(gòu)體。用C語言實現(xiàn)面向?qū)ο缶幊痰姆椒ㄖ剌d。 其他成員可先忽略,后面具體實例分析。因為設(shè)備驅(qū)動模塊并不自己直接填充結(jié)構(gòu)體 file,只是使用file中的數(shù)據(jù)。,20,一些重要的數(shù)據(jù)結(jié)構(gòu),索引節(jié)點inode結(jié)構(gòu) 文件打開,在內(nèi)存建立副本后,由唯一的索引節(jié)點inode描述。 與file結(jié)構(gòu)不同。 file結(jié)構(gòu)是進程使用的結(jié)構(gòu),進程每打開一個文件,就建立一個file結(jié)構(gòu)。不同的進程打開同一個文件,建立不同的file結(jié)構(gòu)。 Inode結(jié)構(gòu)是內(nèi)核使用的結(jié)構(gòu),文件在內(nèi)存建立副本,就建
12、立一個inode結(jié)構(gòu)來描述。一個文件在內(nèi)存里面只有一個inode結(jié)構(gòu)對應(yīng)。,21,一些重要的數(shù)據(jù)結(jié)構(gòu),索引節(jié)點inode結(jié)構(gòu) Inode結(jié)構(gòu)包含大量描述文件信息的成員變量。 但是對于描述設(shè)備文件的inode,跟設(shè)備驅(qū)動有關(guān)的成員只有兩個。 Dev_t i_rdev; 包含真正的設(shè)備編號。 Struct cdev *i_cdev; 指向cdev結(jié)構(gòu)體的指針。cdev是表示字符設(shè)備的內(nèi)核數(shù)據(jù)結(jié)構(gòu)。 從inode中獲得主設(shè)備號和次設(shè)備號的宏: Unsigned int iminor(struct inode *inode); Unsigned int imajor(struct inode *ino
13、de);,22,Linux設(shè)備驅(qū)動,主設(shè)備號和次設(shè)備號的內(nèi)部表達: Dev_t類型用于保存設(shè)備號,稱為設(shè)備編號。/linux/types.h文件中定義。 目前設(shè)備編號dev_t是一個32位的整數(shù),其中12位表示主設(shè)備號,20位表示次設(shè)備號。 通過設(shè)備編號獲取主次設(shè)備號: MAJOR(dev_t dev); MINOR(dev_t dev); 通過主次設(shè)備號合成設(shè)備編號: MKDEV(int major, int minor); Dev_t格式以后可能會發(fā)生變化,但只要使用這些宏,就可保證設(shè)備驅(qū)動程序的正確性。,23,分配和釋放字符設(shè)備號,編寫驅(qū)動程序要做的第一件事,為字符設(shè)備獲取一個設(shè)備號。
14、事先知道所需要的設(shè)備編號(主設(shè)備號)的情況: int register_chrdev_region(dev_t first, unsigned count, const char *name) first是要分配的起始設(shè)備編號值。 first的次設(shè)備號通常設(shè)置為0。 Count 所請求的連續(xù)設(shè)備編號的個數(shù)。 Name設(shè)備名稱,指和該編號范圍建立關(guān)系的設(shè)備。 分配成功返回0。,24,分配和釋放字符設(shè)備號,動態(tài)分配設(shè)備編號(主要是主設(shè)備號) int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,const ch
15、ar *name) dev 是一個僅用于輸出的參數(shù), 它在函數(shù)成功完成時保存已分配范圍的第一個編號。 baseminor 應(yīng)當(dāng)是請求的第一個要用的次設(shè)備號,它常常是 0. count 和 name 參數(shù)跟request_chrdev_region 的一樣.,25,分配和釋放字符設(shè)備號,不再使用時,釋放這些設(shè)備編號。使用以下函數(shù): void unregister_chrdev_region(dev_t from, unsigned count) 在模塊的卸載函數(shù)中調(diào)用該函數(shù)。,26,分配和釋放字符設(shè)備號,新驅(qū)動程序,建議使用動態(tài)分配機制獲取主設(shè)備號,也就是使用alloc_chrdev_regio
16、n()。 動態(tài)分配導(dǎo)致無法預(yù)先創(chuàng)建設(shè)備節(jié)點。 可在分配設(shè)備號后,從/proc/devices文件中獲取。 為了加載后自動創(chuàng)建設(shè)備文件,可以通過編寫內(nèi)核模塊加載腳本實現(xiàn)。,27,字符設(shè)備的注冊,內(nèi)核內(nèi)部使用struct cdev結(jié)構(gòu)表示字符設(shè)備。編寫設(shè)備驅(qū)動的第二步就是注冊該設(shè)備。 包含頭文件。 獲取一個獨立的cdev結(jié)構(gòu): struct cdev *my_cdev = cdev_alloc(); 調(diào)用cdev_init初始化cdev結(jié)構(gòu)體 void cdev_init(struct cdev *cdev, struct file_operations *fops); 初始化該設(shè)備的所有者字段:
17、 dev-cdev.owner = THIS_MODULE; 初始化該設(shè)備的可用操作集: dev-cdev.ops = ,28,字符設(shè)備的注冊,編寫設(shè)備驅(qū)動的第二步就是注冊該設(shè)備。 cdev 結(jié)構(gòu)已建立和初始化, 最后通過cdev_add函數(shù)把它告訴內(nèi)核: int cdev_add(struct cdev *dev, dev_t num, unsigned int count); dev 是要添加的設(shè)備的 cdev 結(jié)構(gòu), num 是這個設(shè)備對應(yīng)的第一個設(shè)備編號, count 是應(yīng)當(dāng)關(guān)聯(lián)到設(shè)備的設(shè)備號的數(shù)目. 卸載字符設(shè)備時,調(diào)用相反的動作函數(shù): void cdev_del(struct cd
18、ev *dev);,29,設(shè)備的注冊,早期方法: 內(nèi)核中仍有許多字符驅(qū)動不使用剛剛描述過的cdev 接口。沒有更新到 2.6 內(nèi)核接口的老代碼。 注冊一個字符設(shè)備的早期方法: int register_chrdev(unsigned int major, const char *name, struct file_operations *fops); major 是給定的主設(shè)備號。為0代表什么? name 是驅(qū)動的名字(將出現(xiàn)在 /proc/devices), fops 是設(shè)備驅(qū)動的file_operations 結(jié)構(gòu)。 register_chrdev 將給設(shè)備分配 0 - 255 的次設(shè)備號
19、, 并且為每一個建立一個缺省的 cdev 結(jié)構(gòu)。 從系統(tǒng)中卸載字符設(shè)備的函數(shù): int unregister_chrdev(unsigned int major, const char *name);,30,Open方法,編寫字符設(shè)備驅(qū)動的第三步:定義設(shè)備驅(qū)動與文件系統(tǒng)的接口,file_operation結(jié)構(gòu)體的函數(shù)定義。 open 方法 int (*open)(struct inode *inode, struct file *filp); 驅(qū)動程序提供open 方法,讓用戶進程使用設(shè)備之前,進行一些初始化的工作。 檢查設(shè)備特定的錯誤。 如果第一次打開設(shè)備, 則初始化設(shè)備。 如果需要, 更新
20、 f_op 指針,更換操作方法集。 分配并填充要放進 filp-private_data 的任何數(shù)據(jù)結(jié)構(gòu)。,31,Open方法,對于設(shè)備文件,inode 參數(shù)只有兩個參數(shù)對設(shè)備驅(qū)動有用的。 Dev_t i_rdev; 包含真正的設(shè)備編號。 Struct cdev *i_cdev; 指向cdev結(jié)構(gòu)體的指針。 i_cdev里面包含我們之前建立的 cdev 結(jié)構(gòu)。但是有時候,我們需要的是包含 cdev 結(jié)構(gòu)的描述設(shè)備的結(jié)構(gòu)。 使用通過成員地址獲取結(jié)構(gòu)體地址的宏container_of,在 中定義: container_of(pointer, container_type, container_fi
21、eld); 這個宏使用一個指向 container_field 類型的成員的指針, 它在一個 container_type 類型的結(jié)構(gòu)中,宏通過分析他們關(guān)系,返回指向包含該成員的結(jié)構(gòu)體指針.,32,Open方法,在 myscull_open, 這個宏用來找到適當(dāng)?shù)脑O(shè)備結(jié)構(gòu): dev = container_of(inode-i_cdev, struct scull_dev, cdev); 找到 myscull_dev 結(jié)構(gòu)后, scull 在filp-private_data 中存儲其指針, 為以后存取使用. filp-private_data = dev;,33,release 方法,rel
22、ease 方法做open相反的工作 釋放 open 分配給filp-private_data的內(nèi)存空間。 在最后一次的關(guān)閉操作時,關(guān)閉設(shè)備。 不是每個 close 系統(tǒng)調(diào)用引起調(diào)用 release 方法。,34,Read和Write方法,Read的任務(wù), 就是從設(shè)備拷貝數(shù)據(jù)到用戶空間。 Write的任務(wù),則從用戶空間拷貝數(shù)據(jù)到設(shè)備。 ssize_t read(struct file *filp, char _user *buff, size_t count, loff_t *offp); ssize_t write(struct file *filp, const char _user *bu
23、ff, size_t count, loff_t *offp); filp 是文件對象指針, count 是請求的傳輸數(shù)據(jù)大小. buff 參數(shù)對write來說是指向持有被寫入數(shù)據(jù)的緩存, 對read則是放入新數(shù)據(jù)的空緩存. offp 是指向一個“l(fā)ong offset type”的指針, 它指出用戶正在存取的文件位置. 返回值是“signed size type”類型;,35,Read和Write方法,read 和 write 方法的 buff 參數(shù)是用戶空間指針,不能被內(nèi)核代碼直接解引用。_user字符串只是形式上的說明,表明是用戶空間地址。 驅(qū)動必須能夠存取用戶空間緩存以完成它的工作。內(nèi)
24、核如何解決這個問題? 為安全起見,內(nèi)核提供專用的函數(shù)來完成對用戶空間的存取。這些專用函數(shù)在中聲明。 unsigned long copy_to_user(void _user *to,const void *from,unsigned long count); unsigned long copy_from_user(void *to,const void _user *from,unsigned long count); 大多數(shù)讀寫函數(shù)都會調(diào)用這兩個函數(shù),用于跟應(yīng)用程序空間交流信息。,36,Read和Write方法,典型的Read函數(shù)對參數(shù)的使用。,37,llseek函數(shù),llseek函數(shù)用
25、于對設(shè)備文件訪問定位。 驅(qū)動接口loff_t (*llseek) (struct file *, loff_t, int); 庫函數(shù)off_t lseek(int filedes, off_t offset, int whence);參數(shù) offset 的含義取決于參數(shù) whence: 如果 whence 是 SEEK_SET,文件偏移量將被設(shè)置為 offset。 如果 whence 是 SEEK_CUR,文件偏移量將被設(shè)置為 cfo 加上 offset,offset 可以為正也可以為負。 如果 whence 是 SEEK_END,文件偏移量將被設(shè)置為文件長度加上 offset, offset
26、 可以為正也可以為負。 SEEK_SET、SEEK_CUR 和 SEEK_END 是 System V 引入的,是 0、1 和 2。,38,ioctl,進行超出簡單的數(shù)據(jù)傳輸之外的操作,進行各種硬件控制操作. ioctl 方法和用戶空間版本不同的原型: int (*ioctl) (struct inode *inode, struct file *filp,unsigned int cmd, unsigned long arg) 不管可選的參數(shù)arg是否由用戶給定為一個整數(shù)或一個指針,它都以一個unsigned long的形式傳遞。 返回值 POSIX 標(biāo)準規(guī)定:如果使用了不合適的 ioctl
27、 命令號,應(yīng)當(dāng)返回-ENOTTY 。這個錯誤碼被 C 庫解釋為“不合適的設(shè)備 ioctl。 -EINVAL也是相當(dāng)普遍的。,39,結(jié)構(gòu)化設(shè)備驅(qū)動程序,設(shè)備結(jié)構(gòu)體 把與某設(shè)備相關(guān)的所有內(nèi)容定義為一個設(shè)備結(jié)構(gòu)體 其中包括設(shè)備驅(qū)動涉及的硬件資源、全局軟件資源、控制(自旋鎖、互斥鎖、等待隊列、定時器等) 在涉及設(shè)備的操作時,就僅僅操作這個結(jié)構(gòu)體,40,Linux設(shè)備驅(qū)動的并發(fā)控制,41,設(shè)備驅(qū)動的并發(fā)控制,在驅(qū)動程序中,當(dāng)多個線程同時訪問相同的資源時,可能會引發(fā)“競態(tài)”,必須對共享資源進行并發(fā)控制。 并發(fā)和競態(tài)廣泛存在。 并發(fā)控制的目的: 使得線程訪問共享資源的操作是原子操作。 原子操作: 在執(zhí)行過程
28、中不會被別的代碼路徑所中斷的操作。 驅(qū)動程序中的全局變量是一種典型的共享資源。,42,考慮一個非常簡單的共享資源的例子:一個全局整型變量和一個簡單的臨界區(qū),其中的操作僅僅是將整型變量的值增加1: i+ 該操作可以轉(zhuǎn)化成下面三條機器指令序列: 得到當(dāng)前變量i的值并拷貝到一個寄存器中 將寄存器中的值加1 把i的新值寫回到內(nèi)存中,原子操作,43,原子操作,內(nèi)核任務(wù)1 內(nèi)核任務(wù)2 獲得i(1) - 增加 i(1-2) - 寫回 i(2) - 獲得 i(2) 增加 i(2-3) 寫回 i(3),內(nèi)核任務(wù)1 內(nèi)核任務(wù)2 獲得 i(1) - 增加 i(1-2) - - 獲得 i(1) - 增加 i(1-2)
29、 - 寫回 i(2) 寫回 i(2) -,可能的實際執(zhí)行結(jié)果:,期望的結(jié)果,44,Linux內(nèi)核的并發(fā)控制,在內(nèi)核空間的內(nèi)核任務(wù)需要考慮同步 內(nèi)核空間中的共享數(shù)據(jù)對內(nèi)核中的所有任務(wù)可見,所以當(dāng)在內(nèi)核中訪問數(shù)據(jù)時,就必須考慮是否會有其他內(nèi)核任務(wù)并發(fā)訪問的可能、是否會產(chǎn)生競爭條件、是否需要對數(shù)據(jù)同步。,45,確定保護對象 找出哪些數(shù)據(jù)需要保護是關(guān)鍵所在 內(nèi)核任務(wù)的局部數(shù)據(jù)僅僅被它本身訪問,顯然不需要保護。 如果數(shù)據(jù)只會被特定的進程訪問,也不需加鎖 大多數(shù)內(nèi)核數(shù)據(jù)結(jié)構(gòu)都需要加鎖:若有其它內(nèi)核任務(wù)可以訪問這些數(shù)據(jù),那么就給這些數(shù)據(jù)加上某種形式的鎖;若任何其它東西能看到它,那么就要鎖住它。,Linux內(nèi)
30、核的并發(fā)控制,46,Linux內(nèi)核的并發(fā)控制,并發(fā)控制的機制 中斷屏蔽,原子數(shù)操作,自旋鎖和信號量都是解決并發(fā)問題的機制。 中斷屏蔽很少被單獨使用,原子操作只能針對整數(shù)來進行。因此自旋鎖和信號量應(yīng)用最為廣泛。,47,中斷屏蔽,單CPU系統(tǒng)中,避免竟態(tài)的一種簡單方式 保證正在執(zhí)行的內(nèi)核執(zhí)行路徑不被中斷處理程序所搶占,防止竟態(tài)條件的發(fā)生。 Local_irq_disable()/關(guān)中斷 Critical section /臨界區(qū) Local_irq_enable() /開中斷 中斷對內(nèi)核非常重要,長時間屏蔽中斷非常危險! 只適合短時間的關(guān)閉 對SMP多CPU引發(fā)的竟態(tài)無效,48,鎖機制可以避免競爭
31、狀態(tài)正如門鎖和門一樣,門后的房間可想象成一個臨界區(qū)。 在一段時間內(nèi),房間里只能有一個內(nèi)核任務(wù)存在,當(dāng)一個任務(wù)進入房間后,它會鎖住身后的房門;當(dāng)它結(jié)束對共享數(shù)據(jù)的操作后,就會走出房間,打開門鎖。如果另一個任務(wù)在房門上鎖時來了,那么它就必須等待房間內(nèi)的任務(wù)出來并打開門鎖后,才能進入房間。,加鎖機制,49,任何要訪問臨界資源的代碼首先都需要占住相應(yīng)的鎖,這樣該鎖就能阻止來自其它內(nèi)核任務(wù)的并發(fā)訪問:,加鎖機制,50,原子數(shù)操作,整型原子數(shù)操作 原子變量初始化 atomic_t test = ATOMIC_INIT(i); 設(shè)置原子變量的值 void atomic_set(atomic_t *v, in
32、t i) 獲得原子變量的值 atomic_read(v) 原子變量加 void atomic_add(int i, atomic_t *v) 原子變量減 void atomic_sub(int i, atomic_t *v),51,原子數(shù)操作,整型原子數(shù)操作 原子變量的自增操作 void atomic_inc(atomic_t *v) 原子變量的自減操作 void atomic_dec(atomic_t *v) 操作并測試 (測試其是否為0,0為true,否為false) atomic_inc_and_test(atomic_t *v) atomic_dec_and_test(atomic_t
33、 *v) int atomic_sub_and_test(int i, atomic_t *v) 操作并返回 (返回新值) int atomic_add_return(int i, atomic_t *v) int atomic_sub_return(int i, atomic_t *v),52,原子數(shù)操作,原子位操作 設(shè)置位 void set_bit(int nr, volatile unsigned long * addr) 清除位 void clear_bit(int nr, volatile unsigned long * addr) 改變位 change_bit(nr,p) 測試位
34、test_bit(int nr, const volatile unsigned long * p) 測試并操作位 test_and_set_bit(nr,p),53,自旋鎖,自旋鎖是專為防止多處理器并發(fā)而引入的一種鎖,它在內(nèi)核中大量應(yīng)用于中斷處理等部分。而對于單處理器來說,防止中斷處理中的并發(fā)可簡單采用關(guān)閉中斷的方式,不需要自旋鎖。 自旋鎖最多只能被一個內(nèi)核任務(wù)持有,若一個內(nèi)核任務(wù)試圖請求一個已被持有的自旋鎖,那么這個任務(wù)就會一直進行忙循環(huán),也就是旋轉(zhuǎn),等待鎖重新可用。 自旋鎖可以在任何時刻防止多于一個的內(nèi)核任務(wù)同時進入臨界區(qū),因此這種鎖可有效地避免多處理器上并發(fā)運行的內(nèi)核任務(wù)競爭共享資源。
35、,54,自旋鎖,自旋鎖的初衷就是: 在短期間內(nèi)進行輕量級的鎖定。一個被爭用的自旋鎖使得請求它的線程在等待鎖重新可用的期間進行自旋(特別浪費處理器時間),所以自旋鎖不應(yīng)該被持有時間過長。如果需要長時間鎖定的話, 最好使用信號量。,55,自旋鎖,自旋鎖防止在不同CPU上的執(zhí)行單元對共享資源的同時訪問,以及不同進程上下文互相搶占導(dǎo)致的對共享資源的非同步訪問。 在單CPU且不可搶占的內(nèi)核下,自旋鎖的所有操作都是空操作。 自旋鎖不允許任務(wù)睡眠。,56,自旋鎖,自旋鎖的基本形式如下: spin_lock(,57,自旋鎖,自旋鎖原語要求包含文件是 . 鎖的類型是 spinlock_t. 鎖的兩種初始化方法:
36、 spinlock_t my_lock = SPIN_LOCK_UNLOCKED; void spin_lock_init(spinlock_t *lock); 進入一個臨界區(qū)前, 必須獲得需要的 lock。 void spin_lock(spinlock_t *lock); 自旋鎖等待是不可中斷的。一旦你調(diào)用spin_lock, 將自旋直到鎖變?yōu)榭捎谩?釋放一個鎖: void spin_unlock(spinlock_t *lock);,58,自旋鎖,關(guān)中斷的自旋鎖 Spin_lock_irq( ) Spin_unlock_irq( ) Spin_lock_irqsave ( ) Spin_
37、unlock_irqrestore ( ),59,信號量,Linux中的信號量是一種睡眠鎖。 如果有一個任務(wù)試圖獲得一個已被持有的信號量時,信號量會將其推入等待隊列,然后讓其睡眠。 當(dāng)持有信號量的進程將信號量釋放后,在等待隊列中的一個任務(wù)將被喚醒,從而便可以獲得這個信號量。 信號量的睡眠特性,使得信號量適用于鎖會被長時間持有的情況; 信號量的操作 信號量支持兩個原子操作P()和V(),前者做測試操作,后者叫做增加操作。 Linux中分別叫做down()和up()。,60,信號量,down()和up()。 down()操作通過對信號量計數(shù)減1來請求獲得一個信號量。 如果結(jié)果是0或大于0,信號量鎖
38、被獲得,任務(wù)就可以進入臨界區(qū)了。 如果結(jié)果是負數(shù),任務(wù)會被放入等待隊列,處理器執(zhí)行其它任務(wù)。 相反,當(dāng)臨界區(qū)中的操作完成后,up()操作用來釋放信號量,增加信號量的計數(shù)值。 如果在該信號量上的等待隊列不為空,處于隊列中等待的任務(wù)在被喚醒的同時會獲得該信號量。,61,信號量,62,信號量,63,Linux信號量的實現(xiàn),內(nèi)核代碼必須包含 ,才能使用信號量。 相關(guān)的類型是 struct semaphore,信號量的定義,struct semaphore atomic_t count; int sleepers; wait_queue_head_t wait; ,64,Linux信號量的實現(xiàn),信號量的
39、聲明和初始化 直接創(chuàng)建一個信號量 struct semaphore * sem; 接著使用 sema_init 來初始化這個信號量: void sema_init(struct semaphore *sem, int val); 互斥模式的信號量聲明,內(nèi)核提供宏定義. DECLARE_MUTEX(name); 信號量初始化為 1 DECLARE_MUTEX_LOCKED(name); 信號量初始化為0,65,Linux信號量的實現(xiàn),動態(tài)分配的互斥信號量聲明 void init_MUTEX(struct semaphore *sem); 信號量初始化為 1 void init_MUTEX_LOC
40、KED(struct semaphore *sem); 信號量初始化為0,66,Linux信號量的實現(xiàn),信號量的P操作 void down(struct semaphore *sem); down減小信號量的值,并根據(jù)信號量的值決定是否等待。不可中斷的等待。 int down_interruptible(struct semaphore *sem); 操作是可中斷的。 int down_trylock(struct semaphore *sem); 信號量在調(diào)用時不可用, down_trylock 立刻返回一個非零值.,67,Linux信號量的實現(xiàn),信號量的V操作 void up(struct
41、 semaphore *sem); 通過down操作進入臨界區(qū)的進程,再退出的時候都需要調(diào)用一個up操作,釋放信號量。,68,Linux信號量的實現(xiàn),信號量基本使用形式為: static DECLARE_MUTEX(mr_sem);/聲明互斥信號量 if(down_interruptible( 操作配套使用,69,Linux 設(shè)備驅(qū)動調(diào)試,70,內(nèi)核調(diào)試選項,內(nèi)核開發(fā)者在內(nèi)核自身中構(gòu)建了多個調(diào)試特性。這些特性會產(chǎn)生額外的輸出并降低性能,Linux發(fā)行版的內(nèi)核為了提高性能,去除這些調(diào)試特性。 用來開發(fā)的內(nèi)核應(yīng)當(dāng)激活的調(diào)試配置選項,是在“kernel hacking” 菜單中。,71,通過打印調(diào)試
42、,Printk printk 通過附加不同的消息優(yōu)先級在消息上,對消息的嚴重程度進行分類。在 定義了8個loglevel。 DEFAULT_MESSAGE_LOGLEVEL為默認級別(printk.c ) 當(dāng)消息優(yōu)先級小于console_loglevel,信息才能顯示出來。而console_loglevel的初值為DEFAULT_CONSOLE_LOGLEVEL。 通過對/proc/sys/kernel/printk的訪問來改變console_loglevel的值。該文件包含四個數(shù)字:當(dāng)前的loglevel、默認loglevel、最小允許的loglevel、引導(dǎo)時的默認loglevel。 ec
43、ho 1 /proc/sys/kernel/printk echo 8 /proc/sys/kernel/printk,72,通過打印調(diào)試,打開和關(guān)閉消息 通過封裝printk函數(shù),快速打開調(diào)試信息或者關(guān)閉調(diào)試信息。 # define PDEBUG(fmt, args.) printk( KERN_DEBUG “myscull: fmt, # args) 通過在Makefile里面定義調(diào)試開關(guān)變量去決定調(diào)試信息是否打開。,73,通過查詢調(diào)試,獲取相關(guān)信息的最好方法:在需要的時候才去查詢系統(tǒng)信息,而不是持續(xù)不斷地產(chǎn)生數(shù)據(jù)。 /proc文件系統(tǒng)是一種特殊的、由軟件創(chuàng)建的文件系統(tǒng),內(nèi)核使用他向外界導(dǎo)
44、出信息。 /proc下面的每個文件都綁定于一個內(nèi)核函數(shù),用戶讀取其中的文件時,該函數(shù)動態(tài)的生成文件的內(nèi)容。 例如/proc/devices,74,通過查詢調(diào)試,包含 在驅(qū)動中定義跟proc文件綁定的內(nèi)核函數(shù)read_proc,在函數(shù)里面定義要輸出的信息。 在初始化函數(shù)中調(diào)用creat_proc_read_entry函數(shù)將/proc入口文件和read_proc函數(shù)聯(lián)系起來。 卸載模塊時調(diào)用remove_proc_entry撤銷proc入口。,75,通過查詢調(diào)試,read_proc函數(shù) int (*read_proc)(char *page, char *start, off_t offset,
45、int count, int *eof, void *data); page 是輸出數(shù)據(jù)的緩存內(nèi)存頁。進程讀取/proc文件時,內(nèi)核會分配一個內(nèi)存頁,read_proc將數(shù)據(jù)通過這個內(nèi)存頁返回到用戶空間。 start 是這個函數(shù)用來說有關(guān)的數(shù)據(jù)寫在頁中哪里 eof,當(dāng)沒有數(shù)據(jù)可返回時,驅(qū)動設(shè)置這個參數(shù)。 Data是提供給驅(qū)動的專用數(shù)據(jù)指針。,76,通過查詢調(diào)試,int sprintf (char *buf, const char *fmt, .) 將數(shù)據(jù)打包成字符流的形式。 內(nèi)核很多象printk函數(shù)一樣,通過庫函數(shù)的形式提供給內(nèi)核開發(fā)者的函數(shù),以滿足內(nèi)核開發(fā)中的一些簡單的需要。 void *
46、memset (void *s, char c, size_t count); void *memcpy (void *dest, const void *src, size_t count);,77,通過查詢調(diào)試,creat_proc_read_entry函數(shù) struct proc_dir_entry *create_proc_read_entry(const char *name,mode_t mode, struct proc_dir_entry *base, read_proc_t *read_proc, void *data); name 是要創(chuàng)建的proc文件名, mod 是文件
47、的訪問掩碼(缺省0 ), base 指出要創(chuàng)建的文件的目錄;如果 base 是 NULL, 文件在 /proc 根下創(chuàng)建 。 read_proc 是實現(xiàn)文件內(nèi)容的 read_proc 函數(shù), data 被內(nèi)核忽略(傳遞給 read_proc).,78,查看Oops信息,大多數(shù)bug通常是因為廢棄了一個NULL指針或者使用了錯誤的指針值。這類bug導(dǎo)致的結(jié)果通常是一條oops消息。 一條oops消息能夠顯示發(fā)生故障時CPU的狀態(tài),以及CPU寄存器的內(nèi)容和其他看似難以理解的信息。,79,查看Oops信息,例如訪問一個NULL指針。因為NULL不是一個可訪問的指針值,所以會引發(fā)一個錯誤,內(nèi)核會簡單地
48、將其轉(zhuǎn)換為oops消息并顯示。然后其調(diào)用進程會被殺死。 Unable to handle kernel NULL pointer dereference at virtual address 00000000 printing eip: d083a064 Oops: 0002 #1 SMP CPU: 0 EIP: 0060: Not tainted EFLAGS: 00010246 (2.6.6) EIP is at oops_example _write+0 x4/0 x10 oops_example eax: 00000000 ebx: 00000000 ecx: 00000000 edx
49、: 00000000 ,80,通過監(jiān)視調(diào)試,strace命令可以顯示由用戶空間程序所發(fā)出的所有系統(tǒng)調(diào)用。 還以符號形式顯示調(diào)用的參數(shù)和返回值。當(dāng)一個系統(tǒng)調(diào)用失敗, 錯誤的符號值(例如, ENOMEM)和對應(yīng)的字串(Out of memory) 都顯示. strace 有很多命令行選項; -t 來顯示每個調(diào)用執(zhí)行的時間, -T 來顯示調(diào)用中花費的時間, -e 來限制被跟蹤調(diào)用的類型, -o 來重定向輸出到一個文件. 缺省地, strace 打印調(diào)用信息到 stderr.,81,Linux 的內(nèi)存分配,82,kmalloc函數(shù),void *kmalloc(size_t size,int flags
50、); 所分配到的內(nèi)存在物理內(nèi)存中連續(xù)且保持原有的數(shù)據(jù)(不清零)。 size是要分配的塊的大小。Linux 創(chuàng)建一系列內(nèi)存對象slab,每個slab內(nèi)的內(nèi)存塊大小是固定。處理分配請求時,就直接在包含有足夠大內(nèi)存塊的slab中分配一個整塊給請求者。 內(nèi)核只能分配一些預(yù)定義的、固定大小的字節(jié)數(shù)組。kmalloc 能夠處理的最小內(nèi)存塊是 32 或 64 字節(jié)(體系結(jié)構(gòu)依賴),而內(nèi)存塊大小的上限隨著體系和內(nèi)核配置而變化。 不應(yīng)分配大于 128 KB的內(nèi)存。若需多于幾個 KB的內(nèi)存塊,最好使用其他方法。,83,kmalloc函數(shù),void *kmalloc(size_t size,int flags);
51、Flags 分配標(biāo)志,表示如何分配空間 。所有標(biāo)志都定義在 GFP_KERNEL 內(nèi)存分配是代表運行在內(nèi)核空間的進程執(zhí)行的,并且在空閑內(nèi)存較少時把當(dāng)前進程轉(zhuǎn)入休眠以等待一個頁面。 GFP_ATOMIC 內(nèi)核通常會為原子性的分配預(yù)留一些空閑頁。當(dāng)當(dāng)前進程不能被置為睡眠時,應(yīng)使用 GFP_ATOMIC,這樣kmalloc 甚至能夠使用最后一個空閑頁。如果連這最后一個空閑頁也不存在,則分配返回失敗。常用來從中斷處理和進程上下文之外的其他代碼中分配內(nèi)存,從不睡眠。 GFP_DMA 要求分配可用于DMA的內(nèi)存。,84,后備高速緩存,內(nèi)核為驅(qū)動程序常常需要反復(fù)分配許多相同大小內(nèi)存塊的情況,增加了一些特殊的
52、內(nèi)存池,稱為后備高速緩存(lookaside cache)。 設(shè)備驅(qū)動程序通常不會涉及后備高速緩存,但是也有例外:在 Linux 2.6 中 USB 和 SCSI 驅(qū)動。,85,后備高速緩存,創(chuàng)建一個新的后備高速緩存 kmem_cache_create name: name和這個后備高速緩存相關(guān)聯(lián);通常設(shè)置為被緩存的結(jié)構(gòu)類型的名字,不能包含空格。 參數(shù)size:每個內(nèi)存區(qū)域的大小。 offset:頁內(nèi)第一個對象的偏移量;用來確保被分配對象的特殊對齊,0 表示缺省值。 flags:控制分配方式的位掩碼: SLAB_NO_REAP 保護緩存在系統(tǒng)查找內(nèi)存時不被削減,不推薦。 SLAB_HWCACH
53、E_ALIGN 所有數(shù)據(jù)對象跟高速緩存行對齊,平臺依賴,可能浪費內(nèi)存。 SLAB_CACHE_DMA 每個數(shù)據(jù)對象在 DMA 內(nèi)存區(qū)段分配.,86,后備高速緩存,創(chuàng)建一個新的后備高速緩存 kmem_cache_create 參數(shù)constructor 和 destructor 是可選函數(shù),用來初始化新分配的對象和在內(nèi)存被作為整體釋放給系統(tǒng)之前“清理”對象。,87,后備高速緩存,kmem_cache_alloc 從已創(chuàng)建的后備高速緩存中分配對象: void *kmem_cache_alloc(kmem_cache_t *cache, int flags); flags 和kmalloc 的fla
54、gs相同 使用 kmem_cache_free釋放一個對象: void kmem_cache_free(kmem_cache_t *cache, const void *obj); 當(dāng)驅(qū)動用完這個后備高速緩存(通常在當(dāng)模塊被卸載時),釋放緩存: int kmem_cache_destroy(kmem_cache_t *cache);,88,get_free_page相關(guān)函數(shù),如果一個模塊需要分配大塊的內(nèi)存,最好使用面向頁的分配技術(shù)。 _get_free_page(unsigned int flags); 返回一個指向新頁的指針, 未清零該頁 get_zeroed_page(unsigned i
55、nt flags); 類似于_get_free_page,但用零填充該頁 _get_free_pages(unsigned int flags, unsigned int order); 分配是若干(物理連續(xù)的)頁面并返回指向該內(nèi)存區(qū)域的第一個字節(jié)的指針,該內(nèi)存區(qū)域未清零 flags 與 kmalloc 的用法相同 order 是請求或釋放的頁數(shù)以 2 為底的對數(shù)。若其值過大(沒有這么大的連續(xù)區(qū)可用), 則分配失敗,89,get_free_page相關(guān)函數(shù),當(dāng)程序不需要頁面時,用下列函數(shù)之一來釋放 void free_page(unsigned long addr);void free_pag
56、es(unsigned long addr, unsigned long order);,90,Linux 的中斷處理,91,為什么會有中斷,中斷最初是為克服對I/O接口控制采用程序查詢所帶來的處理器低效率而產(chǎn)生的。 處理器速度一般比外設(shè)快很多 用輪詢的方式來查詢設(shè)備的狀態(tài),CPU效率不高,CPU和外設(shè)不能并行工作。 中斷機制讓CPU啟動設(shè)備后,就去處理其他任務(wù),只有當(dāng)外設(shè)真正完成數(shù)據(jù)傳輸?shù)臏蕚?,請求CPU服務(wù)的時候,CPU才轉(zhuǎn)過來處理外設(shè)的請求。,92,中斷和異常,外部中斷: 外部設(shè)備所發(fā)出的I/O請求。 隨著計算機系統(tǒng)結(jié)構(gòu)的不斷改進以及應(yīng)用技術(shù)的日益提高,中斷的適用范圍也隨之?dāng)U大,出現(xiàn)了所
57、謂的內(nèi)部中斷(或叫異常)。 異常: 為解決機器運行時所出現(xiàn)的某些隨機事件及編程方便而出現(xiàn)的。,93,I/O中斷處理,為了保證系統(tǒng)對外部的響應(yīng),一個中斷處理程序必須被盡快的完成。因此,把所有的操作都放在中斷處理程序中并不合適 Linux中把緊隨中斷要執(zhí)行的操作分為三類 緊急的(critical)一般關(guān)中斷運行。諸如對PIC應(yīng)答中斷,對PIC或是硬件控制器重新編程,或者修改由設(shè)備和處理器同時訪問的數(shù)據(jù) 非緊急的(noncritical)如修改那些只有處理器才會訪問的數(shù)據(jù)結(jié)構(gòu)(例如按下一個鍵后讀掃描碼),這些也要很快完成,因此由中斷處理程序立即執(zhí)行,不過一般在開中斷的情況下,94,I/O中斷處理,L
58、inux中把緊隨中斷要執(zhí)行的操作分為三類 非緊急可延遲的(noncritical deferrable) 這些操作可以被延遲較長的時間間隔而不影響內(nèi)核操作,有興趣的進程將會等待數(shù)據(jù)。內(nèi)核用下半部分這樣一個機制來在一個更為合適的時機用獨立的函數(shù)來執(zhí)行這些操作。 如把緩沖區(qū)內(nèi)容拷貝到某個進程的地址空間(例如把鍵盤緩沖區(qū)內(nèi)容發(fā)送到終端處理程序進程)。,95,S3c2410的中斷,中斷發(fā)生時,需要知道中斷的來源。 芯片的引線有限,很難提供很多條中斷請求引線。使用中斷控制器管理中斷請求線。 S3c2410將中斷控制器集成在CPU芯片中,“復(fù)用”GPIO端口引腳,具有作為中斷請求線的功能。 SRCPND寄
59、存器32位中的每一位對應(yīng)著一個中斷源,每一位被設(shè)置為1,則相應(yīng)的中斷源產(chǎn)生中斷請求并且等待中斷被服務(wù)。,96,注冊中斷服務(wù)例程,中斷號是一個寶貴且常常有限的資源。內(nèi)核維護一個中斷號的注冊表。 要使用中斷,就要進行中斷號的申請,也就是IRQ(Interrupt ReQuirement)。 只有當(dāng)設(shè)備需要中斷的時候才申請占用一個IRQ,或者是在申請IRQ時采用共享中斷的方式,讓更多的設(shè)備使用中斷。,97,注冊中斷服務(wù)例程,在 實現(xiàn)中斷注冊接口: int request_irq(unsigned int irq, irqreturn_t (*handler)(int, void *, struct pt_regs *), unsigned long flags, const char *dev_name, void *dev_id ); void free_irq(unsigned int irq, void *dev_id); request_irq 的返回值是 0 指示申請成功,為負值時表示錯誤碼。函數(shù)返回 -EBUSY 表示已經(jīng)有另一個驅(qū)動占用了所要申請的中斷線。,98,注冊中斷服務(wù)例程,r
溫馨提示
- 1. 本站所有資源如無特殊說明,都需要本地電腦安裝OFFICE2007和PDF閱讀器。圖紙軟件為CAD,CAXA,PROE,UG,SolidWorks等.壓縮文件請下載最新的WinRAR軟件解壓。
- 2. 本站的文檔不包含任何第三方提供的附件圖紙等,如果需要附件,請聯(lián)系上傳者。文件的所有權(quán)益歸上傳用戶所有。
- 3. 本站RAR壓縮包中若帶圖紙,網(wǎng)頁內(nèi)容里面會有圖紙預(yù)覽,若沒有圖紙預(yù)覽就沒有圖紙。
- 4. 未經(jīng)權(quán)益所有人同意不得將文件中的內(nèi)容挪作商業(yè)或盈利用途。
- 5. 人人文庫網(wǎng)僅提供信息存儲空間,僅對用戶上傳內(nèi)容的表現(xiàn)方式做保護處理,對用戶上傳分享的文檔內(nèi)容本身不做任何修改或編輯,并不能對任何下載內(nèi)容負責(zé)。
- 6. 下載文件中如有侵權(quán)或不適當(dāng)內(nèi)容,請與我們聯(lián)系,我們立即糾正。
- 7. 本站不保證下載資源的準確性、安全性和完整性, 同時也不承擔(dān)用戶因使用這些下載資源對自己和他人造成任何形式的傷害或損失。
最新文檔
- 定制鐵藝護欄施工協(xié)議(2024年)
- 2024水庫工程施工協(xié)議文本
- 高級運營官2024年度勞動協(xié)議
- 搬運服務(wù)協(xié)議樣本2024年
- 二手車買賣協(xié)議:2024年車型
- 2024年職業(yè)雇傭協(xié)議樣式
- 2024年頂級項目中介服務(wù)協(xié)議
- 防水工程勞務(wù)合作具體協(xié)議樣本
- 2024年暖通施工協(xié)議條款詳細樣本
- 高等教育學(xué)教材建設(shè)的實施路徑
- 幼兒園中班語言《啪啦啪啦-碰》微課件
- 物業(yè)公司業(yè)主手冊范本
- 醫(yī)師定期考核表格參考模板
- 英語人教版三年級上冊(教具)動物圖卡
- 民辦非企業(yè)單位(法人)登記申請表08669
- 霍蘭德人格六角形模型(共享內(nèi)容)
- 寶鋼中央研究院創(chuàng)新戰(zhàn)略與運行機制研究
- 建筑CAD測試多選題
- 支座鑄造工藝設(shè)計
- 2022年學(xué)校禁毒工作計劃
- GB-T-30512-2014-汽車禁用物質(zhì)要求
評論
0/150
提交評論