linux中一些常見函式的含義
一.1.container_of : 根據一個結構體變數中的一個域成員變數的指標來獲取指向整個結構體變數的指標
例如:
有一個結構體變數,其定義如下:
struct demo_struct {
type1 member1;
type2 member2;
type3 member3;
type4 member4;
};
struct demo_struct demo;
同時,在另一個地方,獲得了變數demo中的某一個域成員變數的指標,比如:
type3 *memp = get_member_pointer_from_somewhere();
此時,如果需要獲取指向整個結構體變數的指標,而不僅僅只是其某一個域成員變數的指標,我們就可以這麼做:
struct demo_struct *demop = container_of(memp, struct demo_struct, member3);
這樣,我們就通過一個結構體變數的一個域成員變數的指標獲得了整個結構體變數的指標。
2.int down_interruptible(struct semaphore *sem)
這個函式的功能就是獲得訊號量,如果得不到訊號量就睡眠,此時沒有訊號打斷,那麼進入睡眠。但是在睡眠過程中可能被訊號打斷,打斷之後返回-EINTR,主要用來程序間的互斥同步。
下面是該函式的註釋:
/**
* down_interruptible - acquire the semaphore unless interrupted
* @sem: the semaphore to be acquired
*
* Attempts to acquire the semaphore. If no more tasks are allowed to
* acquire the semaphore, calling this function will put the task to sleep.
* If the sleep is interrupted by a signal, this function will return -EINTR.
* If the semaphore is successfully acquired, this function returns 0.
*/
一個程序在呼叫down_interruptible()之後,如果sem<0,那麼就進入到可中斷的睡眠狀態並排程其它程序執行, 但是一旦該程序收到訊號,那麼就會從down_interruptible函式中返回。並標記錯誤號為:-EINTR。一個形象的比喻:傳入的訊號量為1好比天亮,如果當前訊號量為0,程序睡眠,直到(訊號量為1)天亮才醒,但是可能中途有個鬧鈴(訊號)把你鬧醒。又如:小強下午放學回家,回家了就要開始吃飯嘛,這時就會有兩種情況:情況一:飯做好了,可以開始吃;情況二:當他到廚房去的時候發現媽媽還在做,媽媽就對他說:“你先去睡會,待會做好了叫你。”小強就答應去睡會,不過又說了一句:“睡的這段時間要是小紅來找我玩,你可以叫醒我。”小強就是down_interruptible,想吃飯就是獲取訊號量,睡覺對應這裡的休眠,而小紅來找我玩就是中斷休眠。
使用可被中斷的訊號量版本的意思是,萬一出現了semaphore的死鎖,還有機會用ctrl+c發出軟中斷,讓等待這個核心驅動返回的使用者態程序退出。而不是把整個系統都鎖住了。在休眠時,能被中斷訊號終止,這個程序是可以接受中斷訊號的!比如你在命令列中輸入# sleep 10000,按下ctrl + c,就給上面的程序傳送了程序終止訊號。訊號傳送給使用者空間,然後通過系統呼叫,會把這個訊號傳給遞給驅動。訊號只能傳送給使用者空間,無權直接傳送給核心的,那1G的核心空間,我們是無法直接去操作的。
3.filp->private_data
struct file是字元裝置驅動相關重要結構。struct file代表一個開啟的檔案描述符,它不是專門給驅動程式使用的,系統中每一個開啟的檔案在核心中都有一個關聯的 struct file。 它由核心在 open時建立,並傳遞給在檔案上操
作的任何函式,知道最後關閉。當檔案的所有例項都關閉之後,核心釋放這個資料結構。
在 struct filed有個成員void *private_data;文件上說明該成員是系統呼叫時儲存狀態資訊非常有用的資源。起初一直不明白這個private_data在驅動 open函式中的作用,後來發現private_data 這個成員在open函式被呼叫的時候 linux 系統就已經將其幅值為NULL,之後可供使用者使用,或者比較悲劇的被使用者忽略改域。
在詳細的閱讀原始碼後,發現 這個private_data 其實是用來儲存自定義裝置結構體的地址的。自定義結構體的地址被儲存在private_data後,可以在read ,write 等驅動函式中被傳遞和呼叫自定義裝置結構體中的成員。
例如 可以在open函式中這麼做
struct scull_dev *dev;
dev = container_of(inode->i_cdev,struct scull_dev,cdev);
filp->private_data = dev; /for other methods
(container_of這個巨集返回的是地址,即結構體的地址)
也可以使用C語言中的一些技巧實現地址的賦值
struct s3c2440_camif *dev =&camif;
file->private_data = dev;
4.PAGE_ALIGN:將實體地址addr修整為頁邊界地址(頁的上邊界)
define PAGE_ALIGN(addr) (((addr)+PAGE_SIZE-1)&PAGE_MASK)
|————|<– PAGE_ALGN(addr)
| |
| |
| |
| |<– addr
| |
| |
| |
| |
|————|
one page(4K)
define PAGE_SHIFT 12
define PAGE_SIZE (1UL << PAGE_SHIFT)
define PAGE_MASK (~(PAGE_SIZE-1))
PAGE_MASK = ~(1 0000 0000 0000 - 1) = ~(1111 1111 1111) = 0000 0000 0000
PAGE_SIZE = 1 0000 0000 0000 = 2^12 = 4K
如addr為0x22000001 。。PAGE_ALIGN(addr)=(0x22000001+4096-1)&0xfffff000
=(0x22000001+0xfff)&0xfffff000=0x22001000&0xfffff000
=0x22001000;
同樣,比如addr為0x22000003,PAGE_ALIGN(addr)後仍然是0x22001000;
就是和下一個頁對齊,一個頁為4k。起始地址為0x xxxxx 000;
5.int remap_pfn_range(struct vm_area_struct *vma, unsigned long virt_addr, unsigned long pfn, unsigned long size, pgprot_t prot);
該函式的功能是建立頁表。其中引數vma是核心根據使用者的請求自己填寫的,而引數addr表示記憶體對映開始處的虛擬地址,因此,該函式為addr~addr+size之間的虛擬地址構造頁表。
另外,pfn(Page Fram Number)是虛擬地址應該對映到的實體地址的頁面號,實際上就是實體地址右移PAGE_SHIFT位。如果PAGE_SHIFT為4kb,則PAGE_SHIFT為12,因為PAGE_SHIFT等於1<< PAGE_SHIFT。最後一個引數prot是新頁所要求的保護屬性。在驅動程式中,一般能使用remap_pfn_range()對映記憶體中的保留頁(如X86系統中的640KB~1MB區域)和裝置I/O記憶體。因此,如果想把kmalloc()申請的記憶體對映到使用者空間,則可以通過mem_map_reserve()把相應的記憶體設定為保留後就可以。
下面是struct vm_area_struct結構體的定義:
QUOTE:
/*
* This struct defines a memory VMM memory area. There is color: black; background-color: #a0ffff;”>vm_area_struct {
struct mm_struct * vm_mm; /* VM area parameters */
unsigned long vm_start;
unsigned long vm_end;
/* linked list of VM areas per task, sorted by address */
struct vm_area_struct *vm_next;
pgprot_t vm_page_prot;
unsigned long vm_flags;
/* AVL tree of VM areas per task, sorted by address */
short vm_avl_height;
struct vm_area_struct * vm_avl_left;
struct vm_area_struct * vm_avl_right;
/* For areas with an address space and backing store,
* font-size: 10px;”>vm_area_struct *vm_next_share;
struct vm_area_struct **vm_pprev_share;
struct vm_operations_struct * vm_ops;
unsigned long vm_pgoff; /* offset in PAGE_SIZE units, not PAGE_CACHE_SIZE */
struct file * vm_file;
unsigned long vm_raend;
void * vm_private_data; /* was vm_pte (shared mem) */
};
vm_area_struct結構所描述的虛存空間以vm_start、vm_end成員表示,它們分別儲存了該虛存空間的首地址和末地址後第一個位元組的地址,以位元組為單位,所以虛存空間範圍可以用[vm_start, vm_end)表示。
通常,程序所使用到的虛存空間不連續,且各部分虛存空間的訪問屬性也可能不同。所以一個程序的虛存空間需要多個vm_area_struct結構來描述。在vm_area_struct結構的數目較少的時候,各個vm_area_struct按照升序排序,以單鏈表的形式組織資料(通過vm_next指標指向下一個vm_area_struct結構)。但是當vm_area_struct結構的資料較多的時候,仍然採用連結串列組織的化,勢必會影響到它的搜尋速度。針對這個問題,vm_area_struct還添加了vm_avl_hight(樹高)、vm_avl_left(左子節點)、vm_avl_right(右子節點)三個成員來實現AVL樹,以提高vm_area_struct的搜尋速度。
假如該vm_area_struct描述的是一個檔案對映的虛存空間,成員vm_file便指向被對映的檔案的file結構,vm_pgoff是該虛存空間起始地址在vm_file檔案裡面的檔案偏移,單位為物理頁面。
一個程式可以選擇MAP_SHARED或MAP_PRIVATE共享模式將一個檔案的某部分資料對映到自己的虛存空間裡面。這兩種對映方式的區別在於:MAP_SHARED對映後在記憶體中對該虛存空間的資料進行修改會影響到其他以同樣方式對映該部分資料的程序,並且該修改還會被寫回檔案裡面去,也就是這些程序實際上是在共用這些資料。而MAP_PRIVATE對映後對該虛存空間的資料進行修改不會影響到其他程序,也不會被寫入檔案中。
來自不同程序,所有對映同一個檔案的vm_area_struct結構都會根據其共享模式分別組織成兩個連結串列。連結串列的鏈頭分別是:vm_file->f_dentry->d_inode->i_mapping->i_mmap_shared, vm_file->f_dentry->d_inode->i_mapping->i_mmap。而vm_area_struct結構中的vm_next_share指向連結串列中的下一個節點;vm_pprev_share是一個指標的指標,它的值是連結串列中上一個節點(頭節點)結構的vm_next_share(i_mmap_shared或i_mmap)的地址。
程序建立vm_area_struct結構後,只是說明程序可以訪問這個虛存空間,但有可能還沒有分配相應的物理頁面並建立好頁面對映。在這種情況下,若是程序執行中有指令需要訪問該虛存空間中的記憶體,便會產生一次缺頁異常。這時候,就需要通過vm_area_struct結構裡面的vm_ops->nopage所指向的函式來將產生缺頁異常的地址對應的檔案資料讀取出來。
vm_flags主要儲存了程序對該虛存空間的訪問許可權,然後還有一些其他的屬性。vm_page_prot是新對映的物理頁面的頁表項pgprot的預設值。
例子:
QUOTE:
int memdev_mmap(struct file *filp, struct vm_area_struct *vma){
vma->vm_flags |= VM_IO;
vma->vm_flags |= VM_RESERVED;
if(remap_pfn_range(vma, vma->vm->start, virt_to_phys(dev->data)>>PAGE_SHIFT, size, vma->vm_page_prot))
return -EAGAIN;
return 0;
}
6.writel() 往記憶體對映的 I/O 空間上寫資料,wirtel() I/O 上寫入 32 位資料 (4位元組)。
原型:
void writel (unsigned char data , unsigned short addr )
readl() 從記憶體對映的 I/O 空間讀取資料,readl 從 I/O 讀取 32 位資料 ( 4 位元組 )。
原型:
unsigned char readl (unsigned int addr )
變數 addr 是 I/O 地址。
返回值 : 從 I/O 空間讀取的數值。
定義
**#define readb __raw_readb
**#define readw(addr) __le16_to_cpu(__raw_readw(addr))
**#define readl(addr) __le32_to_cpu(__raw_readl(addr))
**#ifndef __raw_readb
static inline u8 __raw_readb(const volatile void __iomem *addr)
{
return (const volatile u8 __force ) addr;
}
**#endif
**#ifndef __raw_readw
static inline u16 __raw_readw(const volatile void __iomem *addr)
{
return (const volatile u16 __force ) addr;
}
**#endif
**#ifndef __raw_readl
static inline u32 __raw_readl(const volatile void __iomem *addr)
{
return (const volatile u32 __force ) addr;
}
**#endif
**#define writeb __raw_writeb
**#define writew(b,addr) __raw_writew(__cpu_to_le16(b),addr)
**#define writel(b,addr) __raw_writel(__cpu_to_le32(b),addr)
8.virt_to_page
巨集 virt_to_page 從一個核心虛地址得到該頁的描述結構 struct page *。所有實體記憶體都
由一個 memmap 陣列來描述。這個巨集就是通過計算給定地址的物理頁在這個陣列中的位
置。另外這個檔案也定義了一個簡單的巨集檢查一個頁是不是合法:VALID_PAGE(page)。
如果 page 離 memmap 陣列的開始太遠以至於超過了最大物理頁面應有的距離則是不合
法的。
但頁表項的定義也放在這裡。有 pgd_t,pmd_t,pte_t 和存取它們值的巨集 pte_val等
9.ClearPageReserved
ClearPageReserved清除了該頁面flag中的reserved標誌,表示該頁面屬於動態記憶體
10.copy_to_user和copy_from_user函式的使用說明
Linux作業系統和驅動程式執行在核心空間,應用程式執行在使用者空間,兩者不能簡單地使用指標傳遞資料,
因為Linux使用的虛擬記憶體機制,使用者空間的資料可能被換出,當核心空間使用使用者空間指標時,
對應的資料可能不在記憶體中。使用者空間的記憶體對映採用段頁式,而核心空間有自己的規則;本次主要的討論
linux執行的內用空間與核心空間進行資料傳遞(主要是應用在linux的驅動程式中)常用函式copy_to_user
和copy_from_user。下面是對這兩個函式進行的詳解
1.函式copy_to_user的函式原型
unsigned long copy_to_user(void __user *to, const void *from, unsigned long n)
{
if (likely(access_ok(VERIFY_WRITE, to, n)))
n = __copy_to_user(to, from, n);
return n;
}
引數詳解:
引數1( void __user *to): 拷貝核心空間的地址指標
引數2(const void *from): 使用者空間的地址指標
引數3(unsigned long n): 核心拷貝到使用者空間的位元組數
返回值: 成功返回 0,失敗是返回還沒有拷貝的位元組數
2.copy_from_user 函式
unsigned long copy_from_user(void *to, const void __user *from, unsigned long n)
{
if (likely(access_ok(VERIFY_READ, from, n)))
n = __copy_from_user(to, from, n);
else
memset(to, 0, n);
return n;
}
引數詳解:
引數1( void __user *to): 拷貝核心空間的地址指標
引數2(const void *from): 使用者空間的地址指標
引數3(unsigned long n): 使用者空間到核心空間的位元組數
返回值: 成功返回 0,失敗是返回還沒有拷貝的位元組數
總結:在兩個函式中,都有對地址空間的有效性進行了檢測。
11.dma_alloc_coherent() – 給DMA池分配物理頁
dma_alloc_coherent() – 獲取物理頁,並將該物理頁的匯流排地址保存於dma_handle,返回該物理頁的虛擬地址
void *dma_alloc_coherent(struct device *dev, size_t size, dma_addr_t *dma_handle, gfp_t gfp){
void *ret;
if (!dev || *dev->dma_mask >= 0xffffffffUL)
gfp &= ~GFP_DMA;
ret = (void *)__get_free_pages(gfp, get_order(size)); //(1)
if (ret) {
memset(ret, 0, size);
*dma_handle = virt_to_bus(ret); //(2)
}
return ret;
}
(1) 將size轉換成order, 即2^order
(2) 將虛擬地址ret轉換成匯流排地址
mprotect: 設定記憶體訪問許可權
mmap 的第三個引數指定對記憶體區域的保護,由標記讀、寫、執行許可權的 PROT_READ、PROT_WRITE 和 PROT_EXEC 按位與操作獲得,或者是限制沒有訪問許可權的 PROT_NONE。如果程式嘗試在不允許這些許可權的本地記憶體上操作,它將被 SIGSEGV 訊號(Segmentation fault,段錯誤)終止。
在記憶體對映完成後,這些許可權仍可以被 mprotect 系統呼叫所修改。mprotect 的引數分別為記憶體區間的地址,區間的大小,新的保護標誌設定。所指定的記憶體區間必須包含整個頁:區間地址必須和整個系統頁大小對齊,而區間長度必須是頁大小的整數倍。這些頁的保護標記被這裡指定的新保護模式替換。
獲得頁面對齊的記憶體
應注意的是, malloc 返回的記憶體區域通常並不與記憶體頁面對齊,甚至在記憶體的大小是頁大小整數倍的情況下也一樣。如果您想保護從 malloc 獲得的記憶體,您不得不分配一個更大的記憶體區域並在其中找到一個與頁對齊的區間。
您可以選擇使用 mmap 系統呼叫來繞過 malloc 並直接從 Linux 核心中分配頁面對齊記憶體。
mmap通過對映 /dev/zero 來分配記憶體頁。記憶體將被初始化為可讀和可寫模式。
int fd = open (“/dev/zero”, O_RDONLY);
char* memory = mmap (NULL, page_size, PROT_READ | PROT_WRITE, MAP_PRIVATE, fd, 0);
close (fd);
然後,您的程式可以使用 mprotect 把它變成只讀:
mprotect (memory, page_size, PROT_READ);
有一種監控記憶體訪問的高階技巧,可以通過利用 mmap 和 mprotect 保護目標記憶體區間,然後當程式訪問時候接收並處理 Linux 系統傳送的 SIGSEGV 訊號。程式碼 展示了這個技巧。
程式碼使用mprotect檢測記憶體訪問
include