句柄是在 Windows 中引入的一個(gè)概念,它是和對(duì)象一一對(duì)應(yīng)的 32 位無符號(hào)整數(shù)值。句柄可以映射到唯一的對(duì)象,它是處理對(duì)象的一個(gè)接口,對(duì)于所涉及的對(duì)象,可以通過相應(yīng)的句柄來操作它。句柄的引入主要是操作系統(tǒng)為了避免應(yīng)用程序直接對(duì)某個(gè)對(duì)象的數(shù)據(jù)結(jié)構(gòu)進(jìn)行操作為目的,用操作句柄來代替操作對(duì)象。
在 Linux 環(huán)境中,任何事物都是用文件來表示,設(shè)備是文件,目錄是文件,socket 也是文件。用來表示所處理對(duì)象的接口和唯一接口就是文件。應(yīng)用程序在讀 / 寫一個(gè)文件時(shí),首先需要打開這個(gè)文件,打開的過程其實(shí)質(zhì)就是在進(jìn)程與文件之間建立起連接,句柄的作用就是唯一標(biāo)識(shí)此連接。此后對(duì)文件的讀 / 寫時(shí),目標(biāo)文件就由這個(gè)句柄作為代表。最后關(guān)閉文件其實(shí)就是釋放這個(gè)句柄的過程,使得進(jìn)程與文件之間的連接斷開。
要了解 Linux 當(dāng)中的句柄,必須先了解 Linux 的文件系統(tǒng),下圖表示了 Linux 的每個(gè)進(jìn)程 (task) 的結(jié)構(gòu),及 task 所打開的文件的結(jié)構(gòu)。
圖 1. task_struct 的結(jié)構(gòu)圖

以下主要列舉 task_struct, files_struct
的數(shù)據(jù)結(jié)構(gòu):
struct task_struct { …… /* filesystem information */ struct fs_struct *fs; …… /* open file information */ struct files_struct *files; …… /* limits */ struct rlimit rlim[RLIM_NLIMITS]; /* rlim[7] 表示 open file 的限制 / } struct files_struct { …… struct file ** fd; /* current fd array */ …… struct files * fd_array[NR_OPEN_DEFAULT]; } |
files_struct
數(shù)據(jù)結(jié)構(gòu)中最主要的就是 file 結(jié)構(gòu)指針數(shù)組 fd_array[],這個(gè)數(shù)組的大小是固定的,即 32,其下標(biāo)即為“打開文件號(hào)”。fd 最初指向 fd_array[]。一個(gè)進(jìn)程可以打開多少個(gè)文件取決于該進(jìn)程的 task_struct 結(jié)構(gòu)中關(guān)于可用資源的限制 rlim[7],在這個(gè)限制以內(nèi),如果超過了 file 結(jié)構(gòu)指針數(shù)組的容量就可以通過 expand_fd_array() 來擴(kuò)充該數(shù)組的容量,并讓 fd 指向新的數(shù)組。
當(dāng)前進(jìn)程的進(jìn)程控制塊 (task_struct) 結(jié)構(gòu)中有一個(gè) file 指針,指向已打開的文件信息結(jié)構(gòu) files_struct。當(dāng)進(jìn)程通過打開文件 ( 如 open()) 與具體的文件建立起連接,實(shí)際上系統(tǒng)會(huì)查找一個(gè)空閑的 fid 作為 fd_array 的下標(biāo),將其指向一個(gè) file 數(shù)據(jù)結(jié)構(gòu),返回給進(jìn)程的就是 fid,這個(gè) fid 就是句柄。
了解了句柄的產(chǎn)生,接下來我們來介紹 Linux 是如何對(duì)句柄進(jìn)行管理。
用戶通過 open() 來申請(qǐng)資源,在系統(tǒng)內(nèi)核中實(shí)際會(huì)調(diào)用 sys_open() 方法來實(shí)現(xiàn)真正的資源申請(qǐng)工作。sys_open() 的代碼在 fs/open.c 中:long sys_open(const char * filename, int flags, int mode) 。調(diào)用參數(shù) filename 實(shí)際上是文件的路徑名(絕對(duì)路徑或者相對(duì)路徑名);mode 表示打開的模式,如只讀等等;flag 則包含了許多標(biāo)志位,可以表示打開模式以外的一些屬性和要求。
sys_open 具體的操作過程如下:
圖 2. sys_open 操作過程圖

- 調(diào)用 getname() 從用戶空間把文件的路徑名拷貝到系統(tǒng)空間
- 調(diào)用 get_unused_fd() 從進(jìn)程的可使用的文件描述符中找出一個(gè)沒有被使用的文件描述符(確切的說是文件描述符數(shù)組的下標(biāo))。
- 調(diào)用 flip_open() 打開一個(gè)創(chuàng)建的文件,并返回一個(gè)文件描述符。
用戶向系統(tǒng)申請(qǐng)文件資源是否能夠成功,主要取決于 get_unused_fd 函數(shù)。如果函數(shù)的返回值為大于 0 的正整數(shù),則 sys_open 能夠獲得系統(tǒng)文件資源;如果函數(shù)的返回值為-1,說明申請(qǐng)失敗,該進(jìn)程所申請(qǐng)的文件數(shù)已經(jīng)超過系統(tǒng)設(shè)置的最大值。get_unused_fd 具體的操作過程如下:
圖 3. get_unused_fd 操作過程圖

- 調(diào)用 find_next_zero_bit,從進(jìn)程描述符位圖(fd)中找出一個(gè)沒有被占用(置位)的位,得到它的下標(biāo)。
- 檢驗(yàn)這個(gè)描述符下標(biāo)的合法性,即是否滿足資源限制。一個(gè)進(jìn)程可以打開多少個(gè)文件取決于該進(jìn)程的 task_struct 結(jié)構(gòu)中關(guān)于可用資源的限制 rlim,在這個(gè)限制以內(nèi),如果超過了 file 結(jié)構(gòu)指針數(shù)組的容量就可以通過 expand_fd_array() 來擴(kuò)充該數(shù)組的容量,并讓 fd 指向新的數(shù)組。
- 若操作成功,返回這個(gè)下標(biāo)。
與打開文件的方式類似,Linux 系統(tǒng)調(diào)用 close() 函數(shù)關(guān)閉文件,并由內(nèi)核中的 sys_close() 負(fù)責(zé)具體實(shí)現(xiàn)。sys_close() 函數(shù)也處于 fs/open.c 中:long sys_close(unsigned int fd) 。它的作用是釋放進(jìn)程已經(jīng)占用的某一文件資源。調(diào)用參數(shù) fd 為需要關(guān)閉的文件句柄值。
造成句柄泄露的主要原因,是進(jìn)程在調(diào)用系統(tǒng)文件之后,沒有釋放已經(jīng)打開的文件句柄。在 Linux 系統(tǒng)中,進(jìn)程與文件之間是通過“打開文件”操作建立連接,文件系統(tǒng)會(huì)返回文件句柄來唯一標(biāo)識(shí)進(jìn)程與文件的連接。每當(dāng)一個(gè)進(jìn)程執(zhí)行完畢之后,Linux 系統(tǒng)會(huì)將與進(jìn)程相關(guān)的文件句柄自動(dòng)釋放。但是,如果進(jìn)程一直處于執(zhí)行狀態(tài),文件的句柄只能通過“關(guān)閉文件”操作來自我釋放。與 Windows 系統(tǒng)的設(shè)置不同,Linux 系統(tǒng)對(duì)進(jìn)程可以調(diào)用的文件句柄數(shù)做了限制,在默認(rèn)情況下,每個(gè)進(jìn)程可以調(diào)用的最大句柄數(shù)為 1024 個(gè)。超過了這個(gè)數(shù)值,進(jìn)程則無法獲得新的句柄。因此,句柄的泄露將會(huì)對(duì)進(jìn)程的功能失效造成極大的隱患。
Linux 中,單個(gè)進(jìn)程能夠打開的最大文件句柄數(shù)量是可以配置的,系統(tǒng)默認(rèn)是 1024。當(dāng)單個(gè)進(jìn)程打開的文件句柄數(shù)量超過了系統(tǒng)定義的值,就會(huì)出現(xiàn)“Too many files open”的錯(cuò)誤提示。用戶可以通過以下命令查看系統(tǒng)定義的最大值:
ulimit – n |
對(duì)于一般的應(yīng)用程序而言 1024 已經(jīng)完全夠用了,但是有些進(jìn)程處理大量請(qǐng)求,很有可能 1024 就不夠用了,則需要調(diào)整系統(tǒng)參數(shù),來適應(yīng)應(yīng)用的變化。Linux 有硬性和軟性設(shè)置兩種,都可以通過 ulimit 來設(shè)置。例如
ulimit – HSn 2048 |
以上命令就可以設(shè)置 H(硬性),S(軟性)的值為 2048。設(shè)定完成后,一旦系統(tǒng)重啟,就又恢復(fù)成默認(rèn)值了。
以下是一個(gè)能夠造成文件句柄泄露的實(shí)例。
# define Maximum 1024 Int i = 0; while(i< Maximum) { fh = open("/home/ychengc/filehandle.c",O_CREAT | O_WRONLY ,S_IRUSR|S_IWU SR); printf("%d\n",fh); i++; } |
在這段程序中,進(jìn)程連續(xù) 1024 次向系統(tǒng)申請(qǐng)文件 /home/ychengc/filehandle.c
的句柄。理論上,每次都應(yīng)該成功。但是,我們卻發(fā)現(xiàn) fh 的最小值是 3,最大值是 1023。當(dāng) fh 超過 1023 后,每次申請(qǐng)的句柄 ID 都是等于-1。造成這種情況的原因,是每個(gè)進(jìn)程都會(huì)默認(rèn)的保留 3 個(gè)文件描述符,文件描述符 0、1、2 分別代表標(biāo)準(zhǔn)輸入、標(biāo)準(zhǔn)輸出和標(biāo)準(zhǔn)錯(cuò)誤輸出。因此,這個(gè)進(jìn)程一共向系統(tǒng)申請(qǐng)了 1027 個(gè)文件描述符,超過了系統(tǒng)最大值 1024 的限制,導(dǎo)致最后三次的申請(qǐng)都是失敗的。
在 Linux 平臺(tái)上,lsof(list open files)是一個(gè)列出當(dāng)前系統(tǒng)打開文件的工具。在 Linux 環(huán)境下,任何事物都以文件的形式存在,系統(tǒng)在后臺(tái)為應(yīng)用程序分配了一個(gè)文件描述符,無論這個(gè)文件的本質(zhì)如何,該文件描述符為應(yīng)用程序與基礎(chǔ)操作系統(tǒng)之間的交互提供了通用接口。因?yàn)閼?yīng)用程序打開文件的描述符列表提供了大量關(guān)于這個(gè)應(yīng)用程序本身的信息,因此通過 lsof 工具能夠查看這個(gè)列表對(duì)系統(tǒng)監(jiān)測(cè)以及排錯(cuò)將是很有幫助的。
在終端下輸入 lsof 即可顯示系統(tǒng)打開的文件,因?yàn)?lsof 需要訪問核心內(nèi)存和各種文件,所以必須以 root 用戶的身份運(yùn)行它才能夠充分地發(fā)揮其功能。屏幕顯示如下:
COMMAND PID USER FD TYPE DEVICE SIZE NODE NAME init 1 root cwd DIR 3,2 4096 2 / init 1 root rtd DIR 3,2 4096 2 / init 1 root txt REG 3,2 32684 1200637 /sbin/init …… |
lsof 輸出各列信息的意義如下:
COMMAND:進(jìn)程的名稱
PID:進(jìn)程標(biāo)識(shí)符
USER:進(jìn)程所有者
FD:文件描述符,應(yīng)用程序通過文件描述符識(shí)別該文件。如 cwd、txt 等
TYPE:文件類型,如 DIR、REG 等
DEVICE:指定磁盤的名稱
SIZE:文件的大小
NODE:索引節(jié)點(diǎn)(文件在磁盤上的標(biāo)識(shí))
NAME:打開文件的確切名稱
在 Linux 系統(tǒng)中可以用 man lsof 查看詳細(xì)的介紹和參數(shù)使用方法,在這里不作過多介紹。在偵測(cè)程序句柄泄露的應(yīng)用中,我們主要用到 lsof 的如下使用方法:
lsof – p PID |
PID 是指我們要偵測(cè)程序的進(jìn)程號(hào),可以用命令 ps – ef 來得到。我們以進(jìn)程號(hào) 14946 為例:
# lsof -p 14946 COMMAND PID USER FD TYPE DEVICE SIZE NODE NAME rpc.rquot 14946 root cwd DIR 3,2 4096 2 / rpc.rquot 14946 root rtd DIR 3,2 4096 2 / rpc.rquot 14946 root txt REG 3,2 65292 267543 /usr/sbin/rpc.rquotad rpc.rquot 14946 root mem REG 3,2 45889 535442 /lib/libnss_files-2.3.4.so rpc.rquot 14946 root mem REG 3,2 1454802 541622 /lib/tls/ libc-2.3.4.so …… |
每一行就代表該進(jìn)程正在使用的一個(gè)文件,即句柄。統(tǒng)計(jì)行數(shù)總和就是該進(jìn)程打開的所有句柄數(shù)量,這為我們用統(tǒng)計(jì)方法偵測(cè)句柄泄露提供的依據(jù)。
通過運(yùn)行 lsof 工具我們可以得到一個(gè)程序打開的所有句柄數(shù)量。我們基于統(tǒng)計(jì)方法的偵測(cè)句柄泄露的基本思想就是:在該程序連續(xù)運(yùn)行的相當(dāng)長(zhǎng)時(shí)間內(nèi),對(duì)它打開的所有句柄數(shù)量進(jìn)行固定周期采樣,再利用作圖工具對(duì)采樣數(shù)據(jù)繪圖,通過圖形我們基本可以判斷該程序是否存在句柄泄露。在程序運(yùn)行的同時(shí),我們可以運(yùn)行大量測(cè)試用例,盡可能的覆蓋程序的所有功能。
下面腳本對(duì)某進(jìn)程采樣 3000 個(gè)數(shù)據(jù),每 10 秒采樣一次,依此數(shù)據(jù)繪制句柄統(tǒng)計(jì)趨勢(shì)圖。
#!/bin/sh set -x echo "">total_handler psid=`ps -ef|grep $1|head -1|awk '{print $2}'` count=0 while [ $count -lt 3000 ] do lsof -p $psid|wc -l >> total_handler sleep 10 count=`expr $count + 1` done |
根據(jù)我們項(xiàng)目的測(cè)試經(jīng)驗(yàn),通常統(tǒng)計(jì)出來的句柄圖形如下列三種:
圖 4. 平穩(wěn)圖

在程序運(yùn)行當(dāng)中,句柄被不斷地打開關(guān)閉,因此統(tǒng)計(jì)圖形呈現(xiàn)平穩(wěn)的鋸齒形。在程序運(yùn)行后期,很多臨時(shí)打開的句柄被逐漸關(guān)閉,總的句柄數(shù)量沒有隨著時(shí)間的推移而增加,因此該程序不存在句柄泄露。
圖 5. 峰值穩(wěn)定圖

在該程序運(yùn)行初期,程序打開的句柄數(shù)量會(huì)隨著時(shí)間的推移而逐步增加。但是當(dāng)運(yùn)行一段時(shí)間后,句柄數(shù)量會(huì)達(dá)到一個(gè)相對(duì)平穩(wěn)的狀態(tài),大概 3500 左右。這個(gè)時(shí)候表明程序打開了很多臨時(shí)句柄,但是句柄數(shù)量相對(duì)穩(wěn)定,也不存在句柄泄露問題。
圖 6. 遞增圖

程序在運(yùn)行當(dāng)中,某一操作引起了程序打開句柄數(shù)量逐步增加,而且沒有出現(xiàn)相對(duì)平穩(wěn)的跡象,說明該程序可能存在句柄泄露,需要進(jìn)一步分析是哪一部分的句柄存在泄漏,以及什么操作會(huì)引起程序句柄的泄露。
通過對(duì)程序句柄數(shù)量進(jìn)行采樣統(tǒng)計(jì),并且繪制出相應(yīng)的統(tǒng)計(jì)圖形,能夠以比較直觀的方式判斷在程序中是否存在句柄泄露。該方法基于程序要運(yùn)行大量的測(cè)試用例,增加測(cè)試用例的覆蓋率,盡可能多的用測(cè)試用例觸發(fā)程序打開和關(guān)閉句柄的操作,這樣才能發(fā)現(xiàn)潛在的句柄泄露 bug。對(duì)于如何能夠快速的發(fā)現(xiàn)句柄泄露代碼,我們將做進(jìn)一步研究。
- 《 Linux 內(nèi)核源代碼情景分析》 毛德操 浙江大學(xué)出版社 2001 年 9 月
- 在 developerWorks Linux 專區(qū) 尋找為 Linux 開發(fā)人員(包括 Linux 新手入門)準(zhǔn)備的更多參考資料,查閱我們 最受歡迎的文章和教程。
- 在 developerWorks 上查閱所有 Linux 技巧 和 Linux 教程。