1. 程式人生 > >Linux系統下fd分配的方法

Linux系統下fd分配的方法

最近幾天在公司裡寫網路通訊的程式碼比較多,自然就會涉及到IO事件監測方法的問題。我驚奇的發現select輪訓的方法在那裡居然還大行其道。我告訴他們現在無論在Linux系統下,還是windows系統下,select都應該被廢棄不用了,其原因是在兩個平臺上select的系統呼叫都有一個可以說是致命的坑。

在windows上面單個fd_set中容納的socket handle個數不能超過FD_SETSIZE(在win32 winsock2.h裡其定義為64,以VS2010版本為準),並且fd_set結構使用一個數組來容納這些socket handle的,每次FD_SET巨集都是向這個陣列中放入一個socket handle,並且此過程中是限定了不能超過FD_SETSIZE,具體請自己檢視winsock2.h中FD_SET巨集的定義。
此處的問題是

身fd_set中的socket handle已經達到FD_SETSIZE個,那麼後續的FD_SET操作實際上是沒有效果的,對應socket handle的IO事件將被遺漏!!!

而在Linux系統下面,該問題其實也是處在fd_set的結構和FD_SET巨集上。此時fd_set結構是使用bit位序列來記錄每一個待檢測IO事件的fd。記錄的方式稍微複雜,如下

/usr/include/sys/select.h中

複製程式碼
 1 typedef long int __fd_mask;
 2 #define __NFDBITS    (8 * sizeof (__fd_mask))
 3
#define __FDELT(d) ((d) / __NFDBITS) 4 5 #define __FDMASK(d) ((__fd_mask) 1 << ((d) % __NFDBITS)) 6 7 typedef struct 8 { 9 /* XPG4.2 requires this member name. Otherwise avoid the name 10 from the global namespace. */ 11 #ifdef __USE_XOPEN 12 __fd_mask fds_bits[__FD_SETSIZE / __NFDBITS];
13 # define __FDS_BITS(set) ((set)->fds_bits) 14 #else 15 __fd_mask __fds_bits[__FD_SETSIZE / __NFDBITS]; 16 # define __FDS_BITS(set) ((set)->__fds_bits) 17 #endif 18 } fd_set; 19 20 #define FD_SET(fd, fdsetp) __FD_SET (fd, fdsetp)
複製程式碼

/usr/include/bits/select.h中 

1 # define __FD_SET(d, set)    (__FDS_BITS (set)[__FDELT (d)] |= __FDMASK (d))

可以看出,在上面的過程,實際上每個bit在fd_set的bit序列中的位置對應於fd的值。而fd_set結構中bit位個數是__FD_SETSIZE定義的,__FD_SETSIZE在/usr/include/bits/typesize.h(包含關係如下sys/socket.h -> bits/types.h -> bits/typesizes.h)中被定義為1024。

現在的問題是,當fd>=1024時,FD_SET巨集實際上會引起記憶體寫越界。而實際上在man select中對已也有明確的說明,如下 

NOTES

An fd_set is a fixed size buffer. Executing FD_CLR() or FD_SET() with a value of fd that is negative or is equal to or
larger than FD_SETSIZE will result in undefined behavior. Moreover, POSIX requires fd to be a valid file descriptor.

 這一點包括之前的我,是很多人沒有注意到的,並且雲風大神有篇博文《一起 select 引起的崩潰》也描述了這個問題。

可以看出在Linux系統select也是不安全的,若想使用,得小心翼翼的確認fd是否達到1024,但這很難做到,不然還是老老實實的用poll或epoll吧。

扯得有點遠了,但也引出了本片文章要敘述的主題,就是Linux系統下fd值是怎麼分配確定,大家都知道fd是int型別,但其值是怎麼增長的,在下面的內容中我對此進行了一點分析,以2.6.30版本的kernel為例,歡迎拍磚。

首先得知道是哪個函式進行fd分配,對此我以pipe為例,它是分配fd的一個典型的syscall,在fs/pipe.c中定義了pipe和pipe2的syscall實現,如下

複製程式碼
 1 SYSCALL_DEFINE2(pipe2, int __user *, fildes, int, flags)
 2 {
 3     int fd[2];
 4     int error;
 5 
 6     error = do_pipe_flags(fd, flags);
 7     if (!error) {
 8         if (copy_to_user(fildes, fd, sizeof(fd))) {
 9             sys_close(fd[0]);
10             sys_close(fd[1]);
11             error = -EFAULT;
12         }
13     }
14     return error;
15 }
16 
17 SYSCALL_DEFINE1(pipe, int __user *, fildes)
18 {
19     return sys_pipe2(fildes, 0);
20 }
複製程式碼

進一步分析do_pipe_flags()實現,發現其使用get_unused_fd_flags(flags)來分配fd的,它是一個巨集
#define get_unused_fd_flags(flags) alloc_fd(0, (flags)),位於include/linux/fs.h中

好了咱們找到了主角了,就是alloc_fd(),它就是核心章實際執行fd分配的函式。其位於fs/file.c,實現也很簡單,如下

複製程式碼
 1 int alloc_fd(unsigned start, unsigned flags)
 2 {
 3     struct files_struct *files = current->files;
 4     unsigned int fd;
 5     int error;
 6     struct fdtable *fdt;
 7 
 8     spin_lock(&files->file_lock);
 9 repeat:
10     fdt = files_fdtable(files);
11     fd = start;
12     if (fd < files->next_fd)
13         fd = files->next_fd;
14 
15     if (fd < fdt->max_fds)
16         fd = find_next_zero_bit(fdt->open_fds->fds_bits,
17                        fdt->max_fds, fd);
18 
19     error = expand_files(files, fd);
20     if (error < 0)
21         goto out;
22 
23     /*
24      * If we needed to expand the fs array we
25      * might have blocked - try again.
26      */
27     if (error)
28         goto repeat;
29 
30     if (start <= files->next_fd)
31         files->next_fd = fd + 1;
32 
33     FD_SET(fd, fdt->open_fds);
34     if (flags & O_CLOEXEC)
35         FD_SET(fd, fdt->close_on_exec);
36     else
37         FD_CLR(fd, fdt->close_on_exec);
38     error = fd;
39 #if 1
40     /* Sanity check */
41     if (rcu_dereference(fdt->fd[fd]) != NULL) {
42         printk(KERN_WARNING "alloc_fd: slot %d not NULL!\n", fd);
43         rcu_assign_pointer(fdt->fd[fd], NULL);
44     }
45 #endif
46 
47 out:
48     spin_unlock(&files->file_lock);
49     return error;
50 }
複製程式碼

在pipe的系統呼叫中start值始終為0,而中間比較關鍵的expand_files()函式是根據所給的fd值,判斷是否需要對程序的開啟檔案表進行擴容,其函式頭註釋如下

複製程式碼
/*
 * 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.
 */
複製程式碼

 

此處對其實現就不做深究了,回到alloc_fd(),現在可以看出,其分配fd的原則是

每次優先分配fd值最小的空閒fd,當分配不成功,即返回EMFILE的錯誤碼,這表示當前程序中fd太多。

到此也印證了在公司寫的服務端程式(kernel是2.6.18)中,每次列印client連結對應的fd值得變化規律了,假如給一個新連線分配的fd值為8,那麼其關閉之後,緊接著的新的連結分配到的fd也是8,再新的連結的fd值是逐漸加1的。

為此,我繼續找了一下socket對應fd分配方法,發現最終也是 alloc_fd(0, (flags),呼叫序列如下
socket(sys_call) -> sock_map_fd() -> sock_alloc_fd() -> get_unused_fd_flags()
open系統呼叫也是用get_unused_fd_flags(),這裡就不列舉了。

現在想回頭說說開篇的select的問題。由於Linux系統fd的分配規則,實際上是已經保證每次的fd值儘量的小,一般非IO頻繁的系統,的確一個程序中fd值達到1024的概率比較小。因而對此到底是否該棄用select,還不能完全地做絕對的結論。如果設計的系統的確有其他措施保證fd值小於1024,那麼用select無可厚非。

但在網路通訊程式這種場合是絕不應該作此假設的,所以還是儘量的不用select吧!!

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

注:Linux預設情況下程序內最大的fd個數為1024,所以沒有將其改為大於1024的情況下使用select來檢測IO事件是不會因fdset讀寫而導致的記憶體越界的問題;
但網路服務場合,若有高併發的需求,多會對這項系統配置改為更大的值,此時使用select就有問題。並且現在的Linux系統都有poll()這個呼叫,所以完全沒有使用select()的必要,完全可以用poll()代替select()