嵌入式Linux驅(qū)動開發(fā)基礎(chǔ)總結(jié)(下篇)
14, 字符設(shè)備驅(qū)動程序設(shè)計基礎(chǔ)
主設(shè)備號和次設(shè)備號(二者一起為設(shè)備號): 一個字符設(shè)備或塊設(shè)備都有一個主設(shè)備號和一個次設(shè)備號。主設(shè)備號用來標識與設(shè)備文件相連的驅(qū)動程序,用來反映設(shè)備類型。次設(shè)備號被驅(qū)動程序用來辨別操作的是哪個設(shè)備,用來區(qū)分同類型的設(shè)備。 linux內(nèi)核中,設(shè)備號用dev_t來描述,2.6.28中定義如下:
typedef u_long dev_t;
在32位機中是4個字節(jié),高12位表示主設(shè)備號,低12位表示次設(shè)備號。
可以使用下列宏從dev_t中獲得主次設(shè)備號:也可以使用下列宏通過主次設(shè)備號生成dev_t:
MAJOR(dev_tdev);MKDEV(intmajor,intminor);MINOR(dev_tdev);
分配設(shè)備號(兩種方法): (1)靜態(tài)申請:
int register_chrdev_region(dev_t from,unsigned count,const char *name);
(2)動態(tài)分配:
int alloc_chrdev_region(dev_t *dev,unsigned baseminor,unsigned count,const char *name);
注銷設(shè)備號:
void unregister_chrdev_region(dev_t from,unsigned count);
創(chuàng)建設(shè)備文件: 利用cat/proc/devices查看申請到的設(shè)備名,設(shè)備號。 (1)使用mknod手工創(chuàng)建:mknod filename type major minor (2)自動創(chuàng)建; 利用udev(mdev)來實現(xiàn)設(shè)備文件的自動創(chuàng)建,首先應(yīng)保證支持udev(mdev),由busybox配置。在驅(qū)動初始化代碼里調(diào)用class_create為該設(shè)備創(chuàng)建一個class,再為每個設(shè)備調(diào)用device_create創(chuàng)建對應(yīng)的設(shè)備。
15, 字符設(shè)備驅(qū)動程序設(shè)計
設(shè)備注冊: 字符設(shè)備的注冊分為三個步驟: (1)分配
cdev:struct cdev *cdev_alloc(void);
(2)初始化
cdev:void cdev_init(struct cdev *cdev,const struct file_operations *fops);
(3)添加
cdev:int cdev_add(struct cdev *p,dev_t dev,unsigned count)
設(shè)備操作的實現(xiàn): file_operations函數(shù)集的實現(xiàn)。
struct file_operations xxx_ops={.owner=THIS_MODULE,.llseek=xxx_llseek,.read=xxx_read,.write=xxx_write,.ioctl=xxx_ioctl,.open=xxx_open,.release=xxx_release,
…
};
特別注意:驅(qū)動程序應(yīng)用程序的數(shù)據(jù)交換: 驅(qū)動程序和應(yīng)用程序的數(shù)據(jù)交換是非常重要的。file_operations中的read()和write()函數(shù),就是用來在驅(qū)動程序和應(yīng)用程序間交換數(shù)據(jù)的。通過數(shù)據(jù)交換,驅(qū)動程序和應(yīng)用程序可以彼此了解對方的情況。但是驅(qū)動程序和應(yīng)用程序?qū)儆诓煌牡刂房臻g。驅(qū)動程序不能直接訪問應(yīng)用程序的地址空間;同樣應(yīng)用程序也不能直接訪問驅(qū)動程序的地址空間,否則會破壞彼此空間中的數(shù)據(jù),從而造成系統(tǒng)崩潰,或者數(shù)據(jù)損壞。安全的方法是使用內(nèi)核提供的專用函數(shù),完成數(shù)據(jù)在應(yīng)用程序空間和驅(qū)動程序空間的交換。這些函數(shù)對用戶程序傳過來的指針進行了嚴格的檢查和必要的轉(zhuǎn)換,從而保證用戶程序與驅(qū)動程序交換數(shù)據(jù)的安全性。這些函數(shù)有:
unsigned long copy_to_user(void__user *to,const void *from,unsigned long n);unsigned long copy_from_user(void *to,constvoid __user *from,unsigned long n);
put_user(local,user);
get_user(local,user);
設(shè)備注銷:
void cdev_del(struct cdev *p);
16,ioctl函數(shù)說明
ioctl是設(shè)備驅(qū)動程序中對設(shè)備的I/O通道進行管理的函數(shù)。所謂對I/O通道進行管理,就是對設(shè)備的一些特性進行控制,例如串口的傳輸波特率、馬達的轉(zhuǎn)速等等。它的調(diào)用個數(shù)如下:
int ioctl(int fd,ind cmd,…);
其中fd就是用戶程序打開設(shè)備時使用open函數(shù)返回的文件標示符,cmd就是用戶程序?qū)υO(shè)備的控制命令,后面的省略號是一些補充參數(shù),有或沒有是和cmd的意義相關(guān)的。
ioctl函數(shù)是文件結(jié)構(gòu)中的一個屬性分量,就是說如果你的驅(qū)動程序提供了對ioctl的支持,用戶就可以在用戶程序中使用ioctl函數(shù)控制設(shè)備的I/O通道。
命令的組織是有一些講究的,因為我們一定要做到命令和設(shè)備是一一對應(yīng)的,這樣才不會將正確的命令發(fā)給錯誤的設(shè)備,或者是把錯誤的命令發(fā)給正確的設(shè)備,或者是把錯誤的命令發(fā)給錯誤的設(shè)備。
所以在Linux核心中是這樣定義一個命令碼的:
這樣一來,一個命令就變成了一個整數(shù)形式的命令碼。但是命令碼非常的不直觀,所以LinuxKernel中提供了一些宏,這些宏可根據(jù)便于理解的字符串生成命令碼,或者是從命令碼得到一些用戶可以理解的字符串以標明這個命令對應(yīng)的設(shè)備類型、設(shè)備序列號、數(shù)據(jù)傳送方向和數(shù)據(jù)傳輸尺寸。 點擊(此處)折疊或打開
/*used to create numbers*/
#define _IO(type,nr) _IOC(_IOC_NONE,(type),(nr),0)
#define _IOR(type,nr,size) _IOC(_IOC_READ,(type),(nr),(_IOC_TYPECHECK(size)))
#define _IOW(type,nr,size) _IOC(_IOC_WRITE,(type),(nr),(_IOC_TYPECHECK(size)))
#define _IOWR(type,nr,size) _IOC(_IOC_READ|_IOC_WRITE,(type),(nr),(_IOC_TYPECHECK(size)))
#defin e_IOR_BAD(type,nr,size) _IOC(_IOC_READ,(type),(nr),sizeof(size))
#define _IOW_BAD(type,nr,size) _IOC(_IOC_WRITE,(type),(nr),sizeof(size))
#define _IOWR_BAD(type,nr,size)_IOC(_IOC_READ|_IOC_WRITE,(type),(nr),sizeof(size))
#define _IOC(dir,type,nr,size)
(((dir)<<_IOC_DIRSHIFT)|
((type)<<_IOC_TYPESHIFT)|
((nr)<<_IOC_NRSHIFT)|
((size)<<_IOC_SIZESHIFT))
17,文件私有數(shù)據(jù)
大多數(shù)linux的驅(qū)動工程師都將文件私有數(shù)據(jù)private_data指向設(shè)備結(jié)構(gòu)體,read等個函數(shù)通過調(diào)用private_data來訪問設(shè)備結(jié)構(gòu)體。這樣做的目的是為了區(qū)分子設(shè)備,如果一個驅(qū)動有兩個子設(shè)備(次設(shè)備號分別為0和1),那么使用private_data就很方便。
這里有一個函數(shù)要提出來:
[!--empirenews.page--]container_of(ptr,type,member)//通過結(jié)構(gòu)體成員的指針找到對應(yīng)結(jié)構(gòu)體的的指針
其定義如下:
/**
*container_of-castamemberofastructureouttothecontainingstructure
*@ptr: thepointertothemember.
*@type: thetypeofthecontainerstructthisisembeddedin.
*@member: thenameofthememberwithinthestruct.
*
*/
#define container_of(ptr,type,member)({
const typeof(((type*)0)->member)*__mptr=(ptr);
(type*)((char*)__mptr-offsetof(type,member));})
18,字符設(shè)備驅(qū)動的結(jié)構(gòu)
可以概括如下圖: 字符設(shè)備是3大類設(shè)備(字符設(shè)備、塊設(shè)備、網(wǎng)絡(luò)設(shè)備)中較簡單的一類設(shè)備,其驅(qū)動程序中完成的主要工作是初始化、添加和刪除cdev結(jié)構(gòu)體,申請和釋放設(shè)備號,以及填充file_operation結(jié)構(gòu)體中操作函數(shù),并實現(xiàn)file_operations結(jié)構(gòu)體中的read()、write()、ioctl()等重要函數(shù)。如圖所示為cdev結(jié)構(gòu)體、file_operations和用戶空間調(diào)用驅(qū)動的關(guān)系。
19, 自旋鎖與信號量
為了避免并發(fā),防止競爭。內(nèi)核提供了一組同步方法來提供對共享數(shù)據(jù)的保護。我們的重點不是介紹這些方法的詳細用法,而是強調(diào)為什么使用這些方法和它們之間的差別。
Linux使用的同步機制可以說從2.0到2.6以來不斷發(fā)展完善。從最初的原子操作,到后來的信號量,從大內(nèi)核鎖到今天的自旋鎖。這些同步機制的發(fā)展伴隨Linux從單處理器到對稱多處理器的過度;伴隨著從非搶占內(nèi)核到搶占內(nèi)核的過度。鎖機制越來越有效,也越來越復(fù)雜。目前來說內(nèi)核中原子操作多用來做計數(shù)使用,其它情況最常用的是兩種鎖以及它們的變種:一個是自旋鎖,另一個是信號量。
自旋鎖 自旋鎖是專為防止多處理器并發(fā)而引入的一種鎖,它在內(nèi)核中大量應(yīng)用于中斷處理等部分(對于單處理器來說,防止中斷處理中的并發(fā)可簡單采用關(guān)閉中斷的方式,不需要自旋鎖)。
自旋鎖最多只能被一個內(nèi)核任務(wù)持有,如果一個內(nèi)核任務(wù)試圖請求一個已被爭用(已經(jīng)被持有)的自旋鎖,那么這個任務(wù)就會一直進行忙循環(huán)——旋轉(zhuǎn)——等待鎖重新可用。要是鎖未被爭用,請求它的內(nèi)核任務(wù)便能立刻得到它并且繼續(xù)進行。自旋鎖可以在任何時刻防止多于一個的內(nèi)核任務(wù)同時進入臨界區(qū),因此這種鎖可有效地避免多處理器上并發(fā)運行的內(nèi)核任務(wù)競爭共享資源。
自旋鎖的基本形式如下:
spin_lock(&mr_lock);//臨界區(qū)spin_unlock(&mr_lock);
·
信號量 Linux中的信號量是一種睡眠鎖。如果有一個任務(wù)試圖獲得一個已被持有的信號量時,信號量會將其推入等待隊列,然后讓其睡眠。這時處理器獲得自由去執(zhí)行其它代碼。當持有信號量的進程將信號量釋放后,在等待隊列中的一個任務(wù)將被喚醒,從而便可以獲得這個信號量。
信號量的睡眠特性,使得信號量適用于鎖會被長時間持有的情況;只能在進程上下文中使用,因為中斷上下文中是不能被調(diào)度的;另外當代碼持有信號量時,不可以再持有自旋鎖。
信號量基本使用形式為:
static DECLARE_MUTEX(mr_sem);//聲明互斥信號量if(down_interruptible(&mr_sem))//可被中斷的睡眠,當信號來到,睡眠的任務(wù)被喚醒//臨界區(qū)up(&mr_sem);
信號量和自旋鎖區(qū)別 從嚴格意義上說,信號量和自旋鎖屬于不同層次的互斥手段,前者的實現(xiàn)有賴于后者,在信號量本身的實現(xiàn)上,為了保證信號量結(jié)構(gòu)存取的原子性,在多CPU中需要自旋鎖來互斥。 信號量是進程級的。用于多個進程之間對資源的互斥,雖然也是在內(nèi)核中,但是該內(nèi)核執(zhí)行路徑是以進程的身份,代表進程來爭奪進程。鑒于進程上下文切換的開銷也很大,因此,只有當進程占用資源時間比較長時,用信號量才是較好的選擇。
當所要保護的臨界區(qū)訪問時間比較短時,用自旋鎖是非常方便的,因為它節(jié)省上下文切換的時間,但是CPU得不到自旋鎖會在那里空轉(zhuǎn)直到執(zhí)行單元鎖為止,所以要求鎖不能在臨界區(qū)里長時間停留,否則會降低系統(tǒng)的效率
由此,可以總結(jié)出自旋鎖和信號量選用的3個原則:
1:當鎖不能獲取到時,使用信號量的開銷就是進程上線文切換的時間Tc,使用自旋鎖的開銷就是等待自旋鎖(由臨界區(qū)執(zhí)行的時間決定)Ts,如果Ts比較小時,應(yīng)使用自旋鎖比較好,如果Ts比較大,應(yīng)使用信號量。
2:信號量所保護的臨界區(qū)可包含可能引起阻塞的代碼,而自旋鎖絕對要避免用來保護包含這樣的代碼的臨界區(qū),因為阻塞意味著要進行進程間的切換,如果進程被切換出去后,另一個進程企圖獲取本自旋鎖,死鎖就會發(fā)生。
3:信號量存在于進程上下文,因此,如果被保護的共享資源需要在中斷或軟中斷情況下使用,則在信號量和自旋鎖之間只能選擇自旋鎖,當然,如果一定要是要那個信號量,則只能通過down_trylock()方式進行,不能獲得就立即返回以避免阻塞
自旋鎖VS信號量 需求建議的加鎖方法 低開銷加鎖優(yōu)先使用自旋鎖 短期鎖定優(yōu)先使用自旋鎖 長期加鎖優(yōu)先使用信號量 中斷上下文中加鎖使用自旋鎖 持有鎖是需要睡眠、調(diào)度使用信號量
20, 阻塞與非阻塞I/O
一個驅(qū)動當它無法立刻滿足請求應(yīng)當如何響應(yīng)?一個對 read 的調(diào)用可能當沒有數(shù)據(jù)時到來,而以后會期待更多的數(shù)據(jù);或者一個進程可能試圖寫,但是你的設(shè)備沒有準備好接受數(shù)據(jù),因為你的輸出緩沖滿了。調(diào)用進程往往不關(guān)心這種問題,程序員只希望調(diào)用 read 或 write 并且使調(diào)用返回,在必要的工作已完成后,你的驅(qū)動應(yīng)當(缺省地)阻塞進程,使它進入睡眠直到請求可繼續(xù)。
阻塞操作是指在執(zhí)行設(shè)備操作時若不能獲得資源則掛起進程,直到滿足可操作的條件后再進行操作。
一個典型的能同時處理阻塞與非阻塞的globalfifo讀函數(shù)如下:
/*globalfifo讀函數(shù)*/
static ssize_t globalfifo_read(struct file *filp, char __user *buf, size_t count,
loff_t *ppos)
{
int ret;
struct globalfifo_dev *dev = filp->private_data;
DECLARE_WAITQUEUE(wait, current);
down(&dev->sem); /* 獲得信號量 */[!--empirenews.page--]
add_wait_queue(&dev->r_wait, &wait); /* 進入讀等待隊列頭 */
/* 等待FIFO非空 */
if (dev->current_len == 0) {
if (filp->f_flags &O_NONBLOCK) {
ret = - EAGAIN;
goto out;
}
__set_current_state(TASK_INTERRUPTIBLE); /* 改變進程狀態(tài)為睡眠 */
up(&dev->sem);
schedule(); /* 調(diào)度其他進程執(zhí)行 */
if (signal_pending(current)) {
/* 如果是因為信號喚醒 */
ret = - ERESTARTSYS;
goto out2;
}
down(&dev->sem);
}
/* 拷貝到用戶空間 */
if (count > dev->current_len)
count = dev->current_len;
if (copy_to_user(buf, dev->mem, count)) {
ret = - EFAULT;
goto out;
} else {
memcpy(dev->mem, dev->mem + count, dev->current_len - count); /* fifo數(shù)據(jù)前移 */
dev->current_len -= count; /* 有效數(shù)據(jù)長度減少 */
printk(KERN_INFO "read %d bytes(s),current_len:%dn", count, dev->current_len);
wake_up_interruptible(&dev->w_wait); /* 喚醒寫等待隊列 */
ret = count;
}
out:
up(&dev->sem); /* 釋放信號量 */
out2:
remove_wait_queue(&dev->w_wait, &wait); /* 從附屬的等待隊列頭移除 */
set_current_state(TASK_RUNNING);
return ret;
}
21, poll方法
使用非阻塞I/O的應(yīng)用程序通常會使用select()和poll()系統(tǒng)調(diào)用查詢是否可對設(shè)備進行無阻塞的訪問。select()和poll()系統(tǒng)調(diào)用最終會引發(fā)設(shè)備驅(qū)動中的poll()函數(shù)被執(zhí)行。 這個方法由下列的原型:
unsigned int (*poll) (struct file *filp, poll_table *wait);
這個驅(qū)動方法被調(diào)用, 無論何時用戶空間程序進行一個 poll, select, 或者 epoll 系統(tǒng)調(diào)用, 涉及一個和驅(qū)動相關(guān)的文件描述符. 這個設(shè)備方法負責(zé)這 2 步:
1. 對可能引起設(shè)備文件狀態(tài)變化的等待隊列,調(diào)用poll_wait()函數(shù),將對應(yīng)的等待隊列頭添加到poll_table.
2. 返回一個位掩碼, 描述可能不必阻塞就立刻進行的操作.
poll_table結(jié)構(gòu), 給 poll 方法的第 2 個參數(shù), 在內(nèi)核中用來實現(xiàn) poll, select, 和 epoll 調(diào)用; 它在 中聲明, 這個文件必須被驅(qū)動源碼包含. 驅(qū)動編寫者不必要知道所有它內(nèi)容并且必須作為一個不透明的對象使用它; 它被傳遞給驅(qū)動方法以便驅(qū)動可用每個能喚醒進程的等待隊列來加載它, 并且可改變 poll 操作狀態(tài). 驅(qū)動增加一個等待隊列到poll_table結(jié)構(gòu)通過調(diào)用函數(shù) poll_wait:
void poll_wait (struct file *, wait_queue_head_t *, poll_table *);
poll 方法的第 2 個任務(wù)是返回位掩碼, 它描述哪個操作可馬上被實現(xiàn); 這也是直接的. 例如, 如果設(shè)備有數(shù)據(jù)可用, 一個讀可能不必睡眠而完成; poll 方法應(yīng)當指示這個時間狀態(tài). 幾個標志(通過 定義)用來指示可能的操作: POLLIN:如果設(shè)備可被不阻塞地讀, 這個位必須設(shè)置. POLLRDNORM:這個位必須設(shè)置, 如果”正常”數(shù)據(jù)可用來讀. 一個可讀的設(shè)備返回( POLLIN|POLLRDNORM ). POLLOUT:這個位在返回值中設(shè)置, 如果設(shè)備可被寫入而不阻塞. …… poll的一個典型模板如下:
static unsigned int globalfifo_poll(struct file *filp, poll_table *wait)
{
unsigned int mask = 0;
struct globalfifo_dev *dev = filp->private_data; /*獲得設(shè)備結(jié)構(gòu)體指針*/
down(&dev->sem);
poll_wait(filp, &dev->r_wait, wait);
poll_wait(filp, &dev->w_wait, wait);
/*fifo非空*/
if (dev->current_len != 0) {
mask |= POLLIN | POLLRDNORM; /*標示數(shù)據(jù)可獲得*/
}
/*fifo非滿*/
if (dev->current_len != GLOBALFIFO_SIZE) {
mask |= POLLOUT | POLLWRNORM; /*標示數(shù)據(jù)可寫入*/
}
up(&dev->sem);
return mask;
}
應(yīng)用程序如何去使用這個poll呢?一般用select()來實現(xiàn),其原型為:
int select(int numfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
其中,readfds, writefds, exceptfds,分別是被select()監(jiān)視的讀、寫和異常處理的文件描述符集合。numfds是需要檢查的號碼最高的文件描述符加1。
以下是一個具體的例子:
/*======================================================================
A test program in userspace
This example is to introduce the ways to use "select"
and driver poll
The initial developer of the original code is Baohua Song
. All Rights Reserved.
======================================================================*/#include #include #include #include #include #include
#define FIFO_CLEAR 0x1#define BUFFER_LEN 20
main()
{
int fd, num;
char rd_ch[BUFFER_LEN];
fd_set rfds,wfds;
/*以非阻塞方式打開/dev/globalmem設(shè)備文件*/
fd = open("/dev/globalfifo", O_RDONLY | O_NONBLOCK);
if (fd != - 1)
{
/*FIFO清0*/
if (ioctl(fd, FIFO_CLEAR, 0) < 0)
{
printf("ioctl command failedn");
}
while (1)
{
FD_ZERO(&rfds);// 清除一個文件描述符集rfds
FD_ZERO(&wfds);
FD_SET(fd, &rfds);// 將一個文件描述符fd,加入到文件描述符集rfds中
FD_SET(fd, &wfds);[!--empirenews.page--]
select(fd + 1, &rfds, &wfds, NULL, NULL);
/*數(shù)據(jù)可獲得*/
if (FD_ISSET(fd, &rfds)) //判斷文件描述符fd是否被置位
{
printf("Poll monitor:can be readn");
}
/*數(shù)據(jù)可寫入*/
if (FD_ISSET(fd, &wfds))
{
printf("Poll monitor:can be writtenn");
}
}
}
else
{
printf("Device open failuren");
}
}
其中: FD_ZERO(fd_set *set); //清除一個文件描述符集set FD_SET(int fd, fd_set *set); //將一個文件描述符fd,加入到文件描述符集set中 FD_CLEAR(int fd, fd_set *set); //將一個文件描述符fd,從文件描述符集set中清除 FD_ISSET(int fd, fd_set *set); //判斷文件描述符fd是否被置位。
22,并發(fā)與競態(tài)介紹
Linux設(shè)備驅(qū)動中必須解決一個問題是多個進程對共享資源的并發(fā)訪問,并發(fā)的訪問會導(dǎo)致競態(tài),在當今的Linux內(nèi)核中,支持SMP與內(nèi)核搶占的環(huán)境下,更是充滿了并發(fā)與競態(tài)。幸運的是,Linux 提供了多鐘解決競態(tài)問題的方式,這些方式適合不同的應(yīng)用場景。例如:中斷屏蔽、原子操作、自旋鎖、信號量等等并發(fā)控制機制。
并發(fā)與競態(tài)的概念 并發(fā)是指多個執(zhí)行單元同時、并發(fā)被執(zhí)行,而并發(fā)的執(zhí)行單元對共享資源(硬件資源和軟件上的全局變量、靜態(tài)變量等)的訪問則很容易導(dǎo)致競態(tài)。
臨界區(qū)概念是為解決競態(tài)條件問題而產(chǎn)生的,一個臨界區(qū)是一個不允許多路訪問的受保護的代碼,這段代碼可以操縱共享數(shù)據(jù)或共享服務(wù)。臨界區(qū)操縱堅持互斥鎖原則(當一個線程處于臨界區(qū)中,其他所有線程都不能進入臨界區(qū))。然而,臨界區(qū)中需要解決的一個問題是死鎖。
23, 中斷屏蔽
在單CPU 范圍內(nèi)避免競態(tài)的一種簡單而省事的方法是進入臨界區(qū)之前屏蔽系統(tǒng)的中斷。CPU 一般都具有屏蔽中斷和打開中斷的功能,這個功能可以保證正在執(zhí)行的內(nèi)核執(zhí)行路徑不被中斷處理程序所搶占,有效的防止了某些競態(tài)條件的發(fā)送,總之,中斷屏蔽將使得中斷與進程之間的并發(fā)不再發(fā)生。
中斷屏蔽的使用方法:
local_irq_disable() /屏蔽本地CPU 中斷/
…..
critical section /臨界區(qū)受保護的數(shù)據(jù)/
…..
local_irq_enable() /打開本地CPU 中斷/
由于Linux 的異步I/O、進程調(diào)度等很多重要操作都依賴于中斷,中斷對內(nèi)核的運行非常重要,在屏蔽中斷期間的所有中斷都無法得到處理,因此長時間屏蔽中斷是非常危險的,有可能造成數(shù)據(jù)的丟失,甚至系統(tǒng)崩潰的后果。這就要求在屏蔽了中斷后,當前的內(nèi)核執(zhí)行路徑要盡快地執(zhí)行完臨界區(qū)代碼。
與local_irq_disable()不同的是,local_irq_save(flags)除了進行禁止中斷的操作外,還保存當前CPU 的中斷狀態(tài)位信息;與local_irq_enable()不同的是,local_irq_restore(flags) 除了打開中斷的操作外,還恢復(fù)了CPU 被打斷前的中斷狀態(tài)位信息。
24, 原子操作
原子操作指的是在執(zhí)行過程中不會被別的代碼路徑所中斷的操作,Linux 內(nèi)核提供了兩類原子操作——位原子操作和整型原子操作。它們的共同點是在任何情況下都是原子的,內(nèi)核代碼可以安全地調(diào)用它們而不被打斷。然而,位和整型變量原子操作都依賴于底層CPU 的原子操作來實現(xiàn),因此這些函數(shù)的實現(xiàn)都與 CPU 架構(gòu)密切相關(guān)。
1 整型原子操作 1)、設(shè)置原子變量的值
void atomic_set(atomic v,int i); /設(shè)置原子變量的值為 i */
atomic_t v = ATOMIC_INIT(0); /定義原子變量 v 并初始化為 0 /
2)、獲取原子變量的值
int atomic_read(atomic_t v) /返回原子變量 v 的當前值*/
3)、原子變量加/減
void atomic_add(int i,atomic_t v) /原子變量增加 i */
void atomic_sub(int i,atomic_t v) /原子變量減少 i */
4)、原子變量自增/自減
void atomic_inc(atomic_t v) /原子變量增加 1 */
void atomic_dec(atomic_t v) /原子變量減少 1 */
5)、操作并測試
int atomic_inc_and_test(atomic_t *v);
int atomic_dec_and_test(atomic_t *v);
int atomic_sub_and_test(int i, atomic_t *v);
上述操作對原子變量執(zhí)行自增、自減和減操作后測試其是否為 0 ,若為 0 返回true,否則返回false。注意:沒有atomic_add_and_test(int i, atomic_t *v)。
6)、操作并返回
int atomic_add_return(int i, atomic_t *v);
int atomic_sub_return(int i, atomic_t *v);
int atomic_inc_return(atomic_t *v);
int atomic_dec_return(atomic_t *v);
上述操作對原子變量進行加/減和自增/自減操作,并返回新的值。
2 位原子操作 1)、設(shè)置位
void set_bit(nr,void addr);/設(shè)置addr 指向的數(shù)據(jù)項的第 nr 位為1 */
2)、清除位
void clear_bit(nr,void addr)/設(shè)置addr 指向的數(shù)據(jù)項的第 nr 位為0 */
3)、取反位
void change_bit(nr,void addr); /對addr 指向的數(shù)據(jù)項的第 nr 位取反操作*/
4)、測試位
test_bit(nr,void addr);/返回addr 指向的數(shù)據(jù)項的第 nr位*/
5)、測試并操作位
int test_and_set_bit(nr, void *addr);
int test_and_clear_bit(nr,void *addr);
int test_amd_change_bit(nr,void *addr);
25, 自旋鎖
自旋鎖(spin lock)是一種典型的對臨界資源進行互斥訪問的手段。為了獲得一個自旋鎖,在某CPU 上運行的代碼需先執(zhí)行一個原子操作,該操作測試并設(shè)置某個內(nèi)存變量,由于它是原子操作,所以在該操作完成之前其他執(zhí)行單元不能訪問這個內(nèi)存變量。如果測試結(jié)果表明鎖已經(jīng)空閑,則程序獲得這個自旋鎖并繼續(xù)執(zhí)行;如果測試結(jié)果表明鎖仍被占用,則程序?qū)⒃谝粋€小的循環(huán)里面重復(fù)這個“測試并設(shè)置” 操作,即進行所謂的“自旋”。
理解自旋鎖最簡單的方法是把它當做一個變量看待,該變量把一個臨界區(qū)標記為“我在這運行了,你們都稍等一會”,或者標記為“我當前不在運行,可以被使用”。[!--empirenews.page--]
Linux中與自旋鎖相關(guān)操作有: 1)、定義自旋鎖
spinlock_t my_lock;
2)、初始化自旋鎖
spinlock_t my_lock = SPIN_LOCK_UNLOCKED; /靜態(tài)初始化自旋鎖/
void spin_lock_init(spinlock_t lock); /動態(tài)初始化自旋鎖*/
3)、獲取自旋鎖
/若獲得鎖立刻返回真,否則自旋在那里直到該鎖保持者釋放/
void spin_lock(spinlock_t *lock);
/若獲得鎖立刻返回真,否則立刻返回假,并不會自旋等待/
void spin_trylock(spinlock_t *lock)
4)、釋放自旋鎖
void spin_unlock(spinlock_t *lock)
自旋鎖的一般用法:
spinlock_t lock; /定義一個自旋鎖/
spin_lock_init(&lock); /動態(tài)初始化一個自旋鎖/
……
spin_lock(&lock); /獲取自旋鎖,保護臨界區(qū)/
……./臨界區(qū)/
spin_unlock(&lock); /解鎖/
自旋鎖主要針對SMP 或單CPU 但內(nèi)核可搶占的情況,對于單CPU 且內(nèi)核不支持搶占的系統(tǒng),自旋鎖退化為空操作。盡管用了自旋鎖可以保證臨界區(qū)不受別的CPU和本地CPU內(nèi)的搶占進程打擾,但是得到鎖的代碼路徑在執(zhí)行臨界區(qū)的時候,還可能受到中斷和底半部(BH)的影響,為了防止這種影響,就需要用到自旋鎖的衍生。
獲取自旋鎖的衍生函數(shù):
void spin_lock_irq(spinlock_t lock); /獲取自旋鎖之前禁止中斷*/ void spin_lock_irqsave(spinlock_t lock, unsigned long flags);/獲取自旋鎖之前禁止中斷,并且將先前的中斷狀態(tài)保存在flags 中*/ void spin_lock_bh(spinlock_t lock); /在獲取鎖之前禁止軟中斷,但不禁止硬件中斷*/
釋放自旋鎖的衍生函數(shù):
void spin_unlock_irq(spinlock_t *lock)
void spin_unlock_irqrestore(spinlock_t *lock,unsigned long flags);
void spin_unlock_bh(spinlock_t *lock);
解鎖的時候注意要一一對應(yīng)去解鎖。 自旋鎖注意點: (1)自旋鎖實際上是忙等待,因此,只有占用鎖的時間極短的情況下,使用自旋鎖才是合理的。 (2)自旋鎖可能導(dǎo)致系統(tǒng)死鎖。 (3)自旋鎖鎖定期間不能調(diào)用可能引起調(diào)度的函數(shù)。如:copy_from_user()、copy_to_user()、kmalloc()、msleep()等函數(shù)。 (4)擁有自旋鎖的代碼是不能休眠的。
26, 讀寫自旋鎖
它允許多個讀進程并發(fā)執(zhí)行,但是只允許一個寫進程執(zhí)行臨界區(qū)代碼,而且讀寫也是不能同時進行的。 1)、定義和初始化讀寫自旋鎖
rwlock_t my_rwlock = RW_LOCK_UNLOCKED; /* 靜態(tài)初始化 */
rwlock_t my_rwlock;
rwlock_init(&my_rwlock); /* 動態(tài)初始化 */
2)、讀鎖定
void read_lock(rwlock_t *lock); void read_lock_irqsave(rwlock_t *lock, unsigned long flags); void read_lock_irq(rwlock_t *lock); void read_lock_bh(rwlock_t *lock);
3)、讀解鎖
void read_unlock(rwlock_t *lock); void read_unlock_irqrestore(rwlock_t *lock, unsigned long flags); void read_unlock_irq(rwlock_t *lock); void read_unlock_bh(rwlock_t *lock);
在對共享資源進行讀取之前,應(yīng)該先調(diào)用讀鎖定函數(shù),完成之后調(diào)用讀解鎖函數(shù)。
4)、寫鎖定
void write_lock(rwlock_t *lock); void write_lock_irqsave(rwlock_t *lock, unsigned long flags); void write_lock_irq(rwlock_t *lock); void write_lock_bh(rwlock_t *lock); void write_trylock(rwlock_t *lock);
5)、寫解鎖
void write_unlock(rwlock_t *lock); void write_unlock_irqrestore(rwlock_t *lock, unsigned long flags); void write_unlock_irq(rwlock_t *lock); void write_unlock_bh(rwlock_t *lock);
在對共享資源進行寫之前,應(yīng)該先調(diào)用寫鎖定函數(shù),完成之后應(yīng)調(diào)用寫解鎖函數(shù)。
讀寫自旋鎖的一般用法:
rwlock_t lock; /定義一個讀寫自旋鎖 rwlock/
rwlock_init(&lock); /初始化/
read_lock(&lock); /讀取前先獲取鎖/
…../臨界區(qū)資源/
read_unlock(&lock); /讀完后解鎖/
write_lock_irqsave(&lock, flags); /寫前先獲取鎖/
…../臨界區(qū)資源/
write_unlock_irqrestore(&lock,flags); /寫完后解鎖/
27, 順序鎖(sequence lock)
順序鎖是對讀寫鎖的一種優(yōu)化,讀執(zhí)行單元在寫執(zhí)行單元對被順序鎖保護的資源進行寫操作時仍然可以繼續(xù)讀,而不必等地寫執(zhí)行單元完成寫操作,寫執(zhí)行單元也不必等待所有讀執(zhí)行單元完成讀操作才進去寫操作。但是,寫執(zhí)行單元與寫執(zhí)行單元依然是互斥的。并且,在讀執(zhí)行單元讀操作期間,寫執(zhí)行單元已經(jīng)發(fā)生了寫操作,那么讀執(zhí)行單元必須進行重讀操作,以便確保讀取的數(shù)據(jù)是完整的,這種鎖對于讀寫同時進行概率比較小的情況,性能是非常好的。
順序鎖有個限制,它必須要求被保護的共享資源不包含有指針,因為寫執(zhí)行單元可能使得指針失效,但讀執(zhí)行單元如果正要訪問該指針,就會導(dǎo)致oops。
1)、初始化順序鎖
seqlock_t lock1 = SEQLOCK_UNLOCKED; /靜態(tài)初始化/
seqlock lock2; /動態(tài)初始化/
seqlock_init(&lock2)
2)、獲取順序鎖
void write_seqlock(seqlock_t *s1);
void write_seqlock_irqsave(seqlock_t *lock, unsigned long flags)
void write_seqlock_irq(seqlock_t *lock);
void write_seqlock_bh(seqlock_t *lock); int write_tryseqlock(seqlock_t *s1);
3)、釋放順序鎖
void write_sequnlock(seqlock_t *s1);
void write_sequnlock_irqsave(seqlock_t *lock, unsigned long flags)
void write_sequnlock_irq(seqlock_t *lock);
void write_sequnlock_bh(seqlock_t *lock);
寫執(zhí)行單元使用順序鎖的模式如下:
write_seqlock(&seqlock_a); /寫操作代碼/
……..[!--empirenews.page--]
write_sequnlock(&seqlock_a);
4)、讀開始
unsigned read_seqbegin(const seqlock_t *s1); unsigned read_seqbegin_irqsave(seqlock_t *lock, unsigned long flags);
5)、重讀
int read_seqretry(const seqlock_t *s1, unsigned iv); int read_seqretry_irqrestore(seqlock_t *lock,unsigned int seq,unsigned long flags);
讀執(zhí)行單元使用順序鎖的模式如下:
unsigned int seq; do{
seq = read_seqbegin(&seqlock_a);
/讀操作代碼/
…….
}while (read_seqretry(&seqlock_a, seq));
28, 信號量
信號量的使用 信號量(semaphore)是用于保護臨界區(qū)的一種最常用的辦法,它的使用方法與自旋鎖是類似的,但是,與自旋鎖不同的是,當獲取不到信號量的時候,進程不會自旋而是進入睡眠的等待狀態(tài)。 1)、定義信號量
struct semaphore sem;
2)、初始化信號量
void sema_init(struct semaphore sem, int val); /初始化信號量的值為 val */
更常用的是下面這二個宏:
#define init_MUTEX(sem) sema_init(sem, 1) #define init_MUTEX_LOCKED(sem) sem_init(sem, 0)
然而,下面這兩個宏是定義并初始化信號量的“快捷方式”
DECLARE_MUTEX(name) /一個稱為name信號量變量被初始化為 1 /
DECLARE_MUTEX_LOCKED(name) /一個稱為name信號量變量被初始化為 0 /
3)、獲得信號量
/該函數(shù)用于獲取信號量,若獲取不成功則進入不可中斷的睡眠狀態(tài)/ void down(struct semaphore *sem);
/該函數(shù)用于獲取信號量,若獲取不成功則進入可中斷的睡眠狀態(tài)/ void down_interruptible(struct semaphore *sem);
/該函數(shù)用于獲取信號量,若獲取不成功立刻返回 -EBUSY/ int down_trylock(struct sempahore *sem);
4)、釋放信號量
void up(struct semaphore sem); /釋放信號量 sem ,并喚醒等待者*/
信號量的一般用法:
DECLARE_MUTEX(mount_sem); /定義一個信號量mount_sem,并初始化為 1 /
down(&mount_sem); /* 獲取信號量,保護臨界區(qū)*/
…..
critical section /臨界區(qū)/
…..
up(&mount_sem); /釋放信號量/
29, 讀寫信號量
讀寫信號量可能引起進程阻塞,但是它允許多個讀執(zhí)行單元同時訪問共享資源,但最多只能有一個寫執(zhí)行單元。 1)、定義和初始化讀寫信號量
struct rw_semaphore my_rws; /定義讀寫信號量/
void init_rwsem(struct rw_semaphore sem); /初始化讀寫信號量*/
2)、讀信號量獲取
void down_read(struct rw_semaphore *sem);
int down_read_trylock(struct rw_semaphore *sem);
3)、讀信號量釋放
void up_read(struct rw_semaphore *sem);
4)、寫信號量獲取
void down_write(struct rw_semaphore *sem);
int down_write_trylock(struct rw_semaphore *sem);
5)、寫信號量釋放
void up_write(struct rw_semaphore *sem);
30, completion
完成量(completion)用于一個執(zhí)行單元等待另外一個執(zhí)行單元執(zhí)行完某事。 1)、定義完成量
struct completion my_completion;
2)、初始化完成量
init_completion(&my_completion);
3)、定義并初始化的“快捷方式”
DECLARE_COMPLETION(my_completion)
4)、等待完成量
void wait_for_completion(struct completion c); /等待一個 completion 被喚醒*/
5)、喚醒完成量
void complete(struct completion c); /只喚醒一個等待執(zhí)行單元*/
void complete(struct completion c); /喚醒全部等待執(zhí)行單元*/
31, 自旋鎖VS信號量
信號量是進程級的,用于多個進程之間對資源的互斥,雖然也是在內(nèi)核中,但是該內(nèi)核執(zhí)行路徑是以進程的身份,代表進程來爭奪資源的。如果競爭失敗,會發(fā)送進程上下文切換,當前進程進入睡眠狀態(tài),CPU 將運行其他進程。鑒于開銷比較大,只有當進程資源時間較長時,選用信號量才是比較合適的選擇。然而,當所要保護的臨界區(qū)訪問時間比較短時,用自旋鎖是比較方便的。
總結(jié): 解決并發(fā)與競態(tài)的方法有(按本文順序):
(1)中斷屏蔽 (2)原子操作(包括位和整型原子) (3)自旋鎖 (4)讀寫自旋鎖 (5)順序鎖(讀寫自旋鎖的進化) (6)信號量 (7)讀寫信號量 (8)完成量
其中,中斷屏蔽很少單獨被使用,原子操作只能針對整數(shù)進行,因此自旋鎖和信號量應(yīng)用最為廣泛。自旋鎖會導(dǎo)致死循環(huán),鎖定期間內(nèi)不允許阻塞,因此要求鎖定的臨界區(qū)小;信號量允許臨界區(qū)阻塞,可以適用于臨界區(qū)大的情況。讀寫自旋鎖和讀寫信號量分別是放寬了條件的自旋鎖 信號量,它們允許多個執(zhí)行單元對共享資源的并發(fā)讀。