linux文件描述符分配實現詳解(基於ARM處理器)

1、linux內核文件相關結構體


1.1、files_struct

files_struct: 進程打開文件的表結構,next_fd表示下一個可用進程描述符,並不一定真正可用,假如0-10描述符都被使用了,中間師傅3文件描述符,再打開文件,此時將使用3作爲新的文件描述符,內核認爲next_fd爲4,next_fd只是表示可能可用的下一個文件描述符,下次查找可用描述符時從next_fd開始查找,而不需要從頭開始找。

1.2、fdtable

fdtable: 真正記錄哪些文件描述符被使用了,哪些是空閒的,實際是一個文件描述符位圖,每1bit表示了一個文件描述符,例如bit 0爲1表示描述符1被使用了,bit 3爲0表示描述符3可以使用。fd數組記錄了file信息,數組下標就是文件描述符的值。細節後面再介紹。

1.3、file

file: 文件的真正信息,文件描述符只是個數組下標,通過下標查找file結構體信息,f_op記錄的是文件讀寫及其他操作的真正函數,不同的文件系統,讀寫函數不一樣,申請文件描述後,內核會將文件描述符與文件結構體(file讀寫函數等)關聯起來。具體怎識別文件系統獲取讀寫函數不在本文介紹。

2、linux內核open系統調用

2.1、SyS_open函數調用棧

系統調用怎麼實現的,請參考之前的文章。打開文件的時候,內核去找一個未使用的文件描述符,最終調用__alloc_fd,查找前面講的fdtable文件描述符位圖。

#0  __alloc_fd (files=0x87822000, start=32, end=1024, flags=2) at fs/file.c:512
#1  0x8010b0d8 in get_unused_fd_flags (flags=<optimized out>) at fs/file.c:562
#2  0x800eeaf0 in do_sys_open (dfd=-100, filename=<optimized out>, flags=2, mode=<optimized out>) at fs/open.c:1018
#3  0x800eebcc in SYSC_open (mode=<optimized out>, flags=<optimized out>, filename=<optimized out>) at fs/open.c:1038
#4  SyS_open (filename=<optimized out>, flags=<optimized out>, mode=<optimized out>) at fs/open.c:1033

2.2、fdtable介紹

如下簡要示意了下文件描述符位圖結構


full_fds_bits每1bit代表的是一個32位的數組,也就是說代表了32位描述符;上面只畫了32位,內核中的位圖是一片連續的內存空間,最低bit表示數值0,下一比特表示1,依次類推;full_fds_bits每1bit只有0和1兩個值,0表示有該組有可用的文件描述符,1表示沒有可用的文件描述符,例如位圖bit 0代表的是0-31共32個文件描述符,bit1代表的是32-63共32個文件描述符,假如0-31文件描述符都被使用了,那麼位圖bit0則應該標記爲1,如果32-63中有一個未使用的文件描述符,則bit1被標記爲0,當32-63中的所有文件描述符都被使用的時候,才標記爲1。

open_fds是真正的文件描述符位圖,也是一片連續的內存空間,每bit代表一個文件描述符(注意full_fds_bits每bit代表的是一組文件描述符),標記爲0的bit表示該文件描述符沒用被使用,標記爲1的比特表示該文件描述符已經被使用,例如從內存其實地址開始計算,第35比特爲1,則表示文件描述符35已經被使用了。

2.3、next zero bit查找函數解釋

函數原型: int find_next_zero_bit(void *addr, unsigned int maxbit, int offset)
參數: addr位圖內存起始地址;maxbit最大bit偏移,也就是位圖最後一bit的偏移;offset起始查找地址,前面略有介紹位圖數組full_fds_bits,通過該位圖可以確定某組文件描述符裏面是否有可以使用的文件描述符,另外next_fd也記錄了下一個可能可用的文件描述符,因此查找可用文件描述符的時候,總是從可能可用的文件描述符開始查找,而不需要從頭找,next_fd在打開和關閉文件描述符的時候會計算。

以下是ARM彙編語言實現,只介紹一部分,其他部分原理是一樣的(介紹時是以文件描述符爲例的,對文件描述符組查找也是一樣的,都是查找0bit的偏移地址)。

ENTRY(_find_next_zero_bit_le)
teq r1, #0 // r1 = maxbit,如果maxbit爲0,是不需要比較的
beq 3b
ands ip, r2, #7 // 判斷offset低3位是否爲0,查找0bit位的時候是以8位對齊查找的,3位二進制,如果offset是8的整數倍,那麼低3位應該是0,跳轉到1b處查找(從byte的第0位開始查找)
beq 1b @ If new byte, goto old routine
 ARM( ldrb r3, [r0, r2, lsr #3] ) // offset不是8的整數倍,那麼先從offset % 8開始查找,假如offset = 18 = 15 + 3,0-15 bit正好是2個字節,我們只需要從第3個字節的第3位開始查找即可,因爲計算機讀的時候是以最小單位字節讀取的,所以我們不能直接讀取第18bit,而是要讀取16-23bit,相當與多讀了3bit的值而已;"lsr, #3"實際是offset/8,獲取的是bit對應的byte,例如18/8 = 2,表示18bit在內存的第2個字節裏面(字節起始索引爲0)

 THUMB( lsr r3, r2, #3 )
 THUMB( ldrb r3, [r0, r3] )
eor r3, r3, #0xff @ now looking for a 1 bit // 8bit文件描述符進行異或操作,實際效果是各位取反,就是將0變1、1變0,對0的查找變爲對1的查找,便於代碼的編寫
movs r3, r3, lsr ip @ shift off unused bits // ip是offset % 8,右移文件描述符,就是將不需要比較的位移除(該函數是從指定位置開始找0bit位,但是並不是說指定位置之前都是1)
bne .L_found // 結果不爲0,即有bit的值爲1(前面已經將0取反爲1了),找到了爲1的bit則跳轉到.L_found
orr r2, r2, #7 @ if zero, then no bits here
add r2, r2, #1 @ align bit pointer
b 2b @ loop for next bit // 沒有找到,繼續查找,後續都是8個bit的查找
ENDPROC(_find_next_zero_bit_le)

......

/*
 * One or more bits in the LSB of r3 are assumed to be set.
 */
.L_found:
#if __LINUX_ARM_ARCH__ >= 5
rsb r0, r3, #0
and r3, r3, r0
clz r3, r3
rsb r3, r3, #31
add r0, r2, r3
#else
tst r3, #0x0f // r3是前面取的8bit文件描述符,r3 & 0x0f用來判斷低4位是否有1,即可用描述符是否在r3的低4位裏面
addeq r2, r2, #4 // 上一條指令結果爲0,表示r3低4位沒有可用的文件描述符,offset = offset + 4,在第4位之後繼續查找
movne r3, r3, lsl #4 // tst指令執行結果不爲0,表示r3低4位有1(即有可用文件描述符),將r3左移4位,移位後低4位就都爲0了,注意這裏offset沒有變化,執行者條指令之後,可用描述符都集中的r3的高4位了,只要從第4位開始查找爲1的bit就可以了。

                // 從第4位開始查找
tst r3, #0x30 // 判斷第4或者5位是否爲1
addeq r2, r2, #2 // 第4、5位都不爲1,則爲1的bit位必定在第6或7位,偏移先加2,offset = offset + 2
movne r3, r3, lsl #2 // 第4或者第5位有1,則先左移2兩位,這步的offset沒有修改

                // 從第6位開始查找
tst r3, #0x40 // 判斷第6位是否爲0
addeq r2, r2, #1 // 第6位爲0,則爲1的bit位一定在第7位,offset = offset + 1
mov r0, r2 // 爲1(之前爲0的bit取反得到的)的bit偏移位置,即可用的文件描述符,r0是函數的返回值
#endif
cmp r1, r0 @ Clamp to maxbit
movlo r0, r1
ret lr


註釋:

前面的說明有些繞口,這裏舉個簡單的例子再解釋下;例如r3的第0-3bit都爲0,第7bit爲1,offset起始值爲0,需要查找到低7bit,先讓offset = offset + 4,然後從第4位找爲1的第7bit位,此時第4到第7位的偏移是3,我們只需要讓offset再加3即可offset = offset + 3 = 4 + 3 = 7,也就是我們每次查找的起始位置變了;再假如r3的第3bit爲1,offset起始值爲0,將r3左移4位,此時第3bit將變爲第7bit,但是offset還是爲0,接着我們從第4bit開始查找爲1的bit,第4到第7bit的偏移爲3,offset = offset + 3 = 0 + 3 = 3,得到的結果是正確的。

總結一句話就是,移位操作是爲了使後面代碼查找時的起點都是一樣的。

2.4、__alloc_fd

文件描述符分配,該函數僅分配了一個可用的文件描述符,文件描述符與文件操作函數的關聯不在這裏處理。



/*
 * allocate a file descriptor, mark it busy.
 */
int __alloc_fd(struct files_struct *files,
      unsigned start, unsigned end, unsigned flags)
{
unsigned int fd;
int error;
struct fdtable *fdt;


spin_lock(&files->file_lock);
repeat:
fdt = files_fdtable(files); // 獲取文件描述符表
fd = start;
if (fd < files->next_fd)
fd = files->next_fd; // 默認傳遞的起始查找文件描述不一定有效,不在有效範圍時使用next_fd作爲起始查找值


if (fd < fdt->max_fds)
fd = find_next_fd(fdt, fd); // 起始查找文件描述符小於最大文件描述符,從當前文件描述符表中查找可用的文件描述符(max_fds表示已分配的文件描述符的數量,也就是位圖總的bit數,後面會看到文件描述符表擴展的代碼,在此先介紹下)


/*
* N.B. For clone tasks sharing a files structure, this test
* will limit the total number of files that can be opened.
*/
error = -EMFILE;
if (fd >= end) // 可用文件描述符超出函數參數傳遞的最大值,返回-EMFILE,這是個標準錯誤碼errno
goto out;


error = expand_files(files, fd); // 擴展文件描述符,當fd<=max_fds時,fd在文件描述符位圖可表示的範圍,例如我們申請的文件描述符大小爲1byte,那麼文件描述符最大隻能表示7,當fd大於7的時候,我們就沒有對應的bit位可以標記了,因此需要重新擴展,申請更大的內存,申請的新的文件描述符表,讓後將舊的值拷貝到新的文件描述符表中。只有fd>max_fds纔會真正擴展。

if (error < 0)
goto out;


/*
* If we needed to expand the fs array we
* might have blocked - try again.
*/
if (error)
goto repeat;


if (start <= files->next_fd)
files->next_fd = fd + 1;


__set_open_fd(fd, fdt); // 在文件描述符表中標記fd已經被打開,對應bit位設置爲1,同時更新fd所在文件描述符組的值,因爲fd改變後,可能導致該組的文件描述符都被使用了,需要將該組標記爲1,下次查找可用文件描述符時就會跳過該組,避免不必要的查找。
if (flags & O_CLOEXEC)
__set_close_on_exec(fd, fdt); // 打開時帶有O_CLOEXEC標誌,設置close_on_exec文件描述符位打開狀態,大致意思是exec創建進程時會覆蓋父進程,但是子進程繼承了父進程的文件描述符,對於exec創建的新進程,繼承的文件描述符已經沒有任何意義了,創建之後需要關閉這些無意義的文件描述符,而這些文件描述符就記錄在close_on_exec裏面。
else
__clear_close_on_exec(fd, fdt);
error = fd;
#if 1
/* Sanity check */
if (rcu_access_pointer(fdt->fd[fd]) != NULL) {
printk(KERN_WARNING "alloc_fd: slot %d not NULL!\n", fd);
rcu_assign_pointer(fdt->fd[fd], NULL); // 文件操作函數設置爲NULL,此時只分配了文件描述符,還沒有真正關聯到具體的文件操作函數
}
#endif


out:
spin_unlock(&files->file_lock);
return error;
}

2.5、find_next_fd

查找下一個可用的文件描述符


static unsigned long find_next_fd(struct fdtable *fdt, unsigned long start)
{
unsigned long maxfd = fdt->max_fds; // 文件描述符表最大文件描述符
unsigned long maxbit = maxfd / BITS_PER_LONG; // 最大文件描述符組(一組文件描述符包含32個文件描述符,例如0-31爲一組)
unsigned long bitbit = start / BITS_PER_LONG; // 起始查找文件描述符所在組(32個文件描述符爲一組,我們要從文件描述符33開始查找,可知,33文件描述符在33/32 = 1組,因此我們從第1組開始查找即可)


bitbit = find_next_zero_bit(fdt->full_fds_bits, maxbit, bitbit) * BITS_PER_LONG; // 查找下一個可用文件描述符組,結果乘以BITS_PER_LONG,即得到該組起始文件描述符。
if (bitbit > maxfd)
return maxfd; // 可用文件描述符起始值大於最大文件描述符,直接返回最大文件描述符,表示文件描述符需要擴展。
if (bitbit > start)
start = bitbit; // 可用起始文件描述符大於參數傳遞的起始查找文件描述符,將開始查找的值從真正有效的值開始,避免做無效的查找。
return find_next_zero_bit(fdt->open_fds, maxfd, start); // 從文件描述符表中查找可用的文件描述符(之前是查找可用組,以32位爲大小查找,提高效率,這次的查找範圍縮小的組內了,即最多只需要查找32次了,這有點像找房間一樣,先找樓層,再找房間,而不需要每層樓每間房間都找一遍;查找函數在前面章節已經介紹了)。
}

2.6、expand_files

文件描述表大小是根據需要動態增加的,不會一開始就申請很大空間,這樣會浪費內存,當文件描述符表不夠大時才重新分配內存空間。
/*
 * Expand files.
 * This function will expand the file structures, if the requested size exceeds
 * the current capacity and there is room for expansion.
 * Return <0 error code on error; 0 when nothing done; 1 when files were
 * expanded and execution may have blocked.
 * The files->file_lock should be held on entry, and will be held on exit.
 */
static int expand_files(struct files_struct *files, int nr)
__releases(files->file_lock)
__acquires(files->file_lock)
{
struct fdtable *fdt;
int expanded = 0;


repeat:
fdt = files_fdtable(files); // 獲取文件描述符表


/* Do we need to expand? */
if (nr < fdt->max_fds) // 新的文件描述符<max_fds,舊的文件描述符表已經足夠表示該文件描述符了,不需要擴展。
return expanded;


/* Can we expand? */
if (nr >= sysctl_nr_open) // 大於限制的最大文件描述符,返回錯誤,不運行操作系統設置的最大文件描述符
return -EMFILE;


if (unlikely(files->resize_in_progress)) { // 同一進程下的多個線程是共用一個文件描述符的,需要互斥訪問
spin_unlock(&files->file_lock);
expanded = 1;
wait_event(files->resize_wait, !files->resize_in_progress);
spin_lock(&files->file_lock);
goto repeat;
}


/* All good, so we try */
files->resize_in_progress = true;
expanded = expand_fdtable(files, nr); // 擴展文件描述符表
files->resize_in_progress = false;


wake_up_all(&files->resize_wait);
return expanded;
}

2.7、expand_fdtable

/*
 * Expand the file descriptor table.
 * This function will allocate a new fdtable and both fd array and fdset, of
 * the given size.
 * Return <0 error code on error; 1 on successful completion.
 * The files->file_lock should be held on entry, and will be held on exit.
 */
static int expand_fdtable(struct files_struct *files, int nr)
__releases(files->file_lock)
__acquires(files->file_lock)
{
struct fdtable *new_fdt, *cur_fdt;


spin_unlock(&files->file_lock);
new_fdt = alloc_fdtable(nr); // 申請足以表示nr文件描述符的內存空間


/* make sure all __fd_install() have seen resize_in_progress
* or have finished their rcu_read_lock_sched() section.
*/
if (atomic_read(&files->count) > 1)
synchronize_sched();


spin_lock(&files->file_lock);
if (!new_fdt)
return -ENOMEM;
/*
* extremely unlikely race - sysctl_nr_open decreased between the check in
* caller and alloc_fdtable().  Cheaper to catch it here...
*/
if (unlikely(new_fdt->max_fds <= nr)) {
__free_fdtable(new_fdt);
return -EMFILE;
}
cur_fdt = files_fdtable(files);
BUG_ON(nr < cur_fdt->max_fds);
copy_fdtable(new_fdt, cur_fdt); // 文件描述符信息拷貝
rcu_assign_pointer(files->fdt, new_fdt); // 文件描述符表指針更新
if (cur_fdt != &files->fdtab)
call_rcu(&cur_fdt->rcu, free_fdtable_rcu);
/* coupled with smp_rmb() in __fd_install() */
smp_wmb();
return 1;
}

2.8、alloc_fdtable

static struct fdtable * alloc_fdtable(unsigned int nr)
{
struct fdtable *fdt;
void *data;


/*
* Figure out how many fds we actually want to support in this fdtable.
* Allocation steps are keyed to the size of the fdarray, since it
* grows far faster than any of the other dynamic data. We try to fit
* the fdarray into comfortable page-tuned chunks: starting at 1024B
* and growing in powers of two from there on.
*/
nr /= (1024 / sizeof(struct file *));
nr = roundup_pow_of_two(nr + 1);
nr *= (1024 / sizeof(struct file *)); // nr大小不確定,這之前的步驟就是爲了調整nr大小,具體含義看英文說明
/*
* Note that this can drive nr *below* what we had passed if sysctl_nr_open
* had been set lower between the check in expand_files() and here.  Deal
* with that in caller, it's cheaper that way.
*
* We make sure that nr remains a multiple of BITS_PER_LONG - otherwise
* bitmaps handling below becomes unpleasant, to put it mildly...
*/
if (unlikely(nr > sysctl_nr_open)) // nr大於系統設置的文件描述符上限,需要調整不超過系統設置的上限
nr = ((sysctl_nr_open - 1) | (BITS_PER_LONG - 1)) + 1;


fdt = kmalloc(sizeof(struct fdtable), GFP_KERNEL_ACCOUNT); // 申請內存空間
if (!fdt)
goto out;
fdt->max_fds = nr; // 最大文件描述符
data = alloc_fdmem(nr * sizeof(struct file *));
if (!data)
goto out_fdt;
fdt->fd = data;


data = alloc_fdmem(max_t(size_t,
2 * nr / BITS_PER_BYTE + BITBIT_SIZE(nr), L1_CACHE_BYTES));
if (!data)
goto out_arr;
fdt->open_fds = data; // 文件描述符位圖(每一位代表一個文件描述符)
data += nr / BITS_PER_BYTE;
fdt->close_on_exec = data; // close_on_exec文件描述符位圖(用於在exec創建替換父進程時,確定哪些文件描述符需要關閉)
data += nr / BITS_PER_BYTE;
fdt->full_fds_bits = data; // 文件描述符組位圖(每一位代表一個文件描述符組,一組文件描述符有32個文件描述符,當該組文件描述符都被使用了的時候,將該組對應的bit位設置爲1,表示該組已經沒有可用文件描述符了)


return fdt;


out_arr:
kvfree(fdt->fd);
out_fdt:
kfree(fdt);
out:
return NULL;
}

2.9、__put_unused_fd

爲了加速文件描述符查找,文件描述符分配/釋放的時候都會更新next_fd,用來標誌下一個可能可用的文件描述符,例如:我們剛申請到了文件描述符3,那麼就代表3之前的文件描述符都被使用了(分配文件描述符是從小到大分配的),下一個可能可用的文件描述符應該大於等於4,當然文件描述符4有可能已經被使用了,但是我們不必查找文件描述符4之前的文件描述符,這樣就提高了效率。另外,例如:0-9文件描述符都被使用了,next_fd=10,現在close文件描述符3,3<10,因此我們更新next_fd爲3,這樣我們就能保證next_fd始終是最小可能可用的文件描述符,不會造成查找時跳過可用文件描述符的情況。

static void __put_unused_fd(struct files_struct *files, unsigned int fd)
{
struct fdtable *fdt = files_fdtable(files);
__clear_open_fd(fd, fdt); // 清除文件描述符使用標記,同時清除該文件描述符所在組的標記,釋放了一個文件描述符,該組至少有一個文件描述符可使用
if (fd < files->next_fd)
files->next_fd = fd; // 更新下一個可用的文件描述符(打開文件時的更新請查看前面章節)
}

3、文件描述符關聯

linux系統下任何設備都是文件,任何文件操作接口都是一樣的,對於應用程序來說,用戶只獲取到文件描述符,要實現對文件操作,內核還需要知道文件描述符對應的真正設備是什麼,怎麼操作;ext2、ext4、socket、pipe各種文件的操作都不一樣,具體有vfs同一封裝了,後面有空再介紹。

long do_sys_open(int dfd, const char __user *filename, int flags, umode_t mode)
{
struct open_flags op;
int fd = build_open_flags(flags, mode, &op);
struct filename *tmp;


if (fd)
return fd;


tmp = getname(filename);
if (IS_ERR(tmp))
return PTR_ERR(tmp);


fd = get_unused_fd_flags(flags); // 獲取未使用的文件描述符
if (fd >= 0) {
struct file *f = do_filp_open(dfd, tmp, &op); // 打開文件,獲取file結構體,文件操作的函數指針等,在此暫不做解釋,可以參考之前pipe管道的文章,自己分析下pipe操作函數是怎麼獲取到的,pipe操作比物理文件系統操作簡單了很多,分析起來更容易。
if (IS_ERR(f)) {
put_unused_fd(fd);
fd = PTR_ERR(f);
} else {
fsnotify_open(f);
fd_install(fd, f); // 文件描述符與文件結構體(真正的文件操作函數等)關聯,實際就是設置fd對應的數組的值爲f,在此不做詳細解釋。
}
}
putname(tmp);
return fd;
}

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章