seq_file 介面

Copyright 2003 Jonathan Corbet <corbet@lwn.net>

此檔案最初來自 LWN.net 驅動移植系列,網址為 https://lwn.net/Articles/driver-porting/

裝置驅動程式(或其他核心元件)可以透過多種方式向用戶或系統管理員提供資訊。一種有用的技術是建立虛擬檔案,例如在 debugfs、/proc 或其他位置。虛擬檔案可以提供易於訪問的人類可讀輸出,而無需任何特殊的實用程式;它們還可以簡化指令碼編寫者的工作。多年來,虛擬檔案的使用不斷增長,這並不奇怪。

但是,正確建立這些檔案一直是一項挑戰。建立一個返回字串的虛擬檔案並不難。但是,如果輸出很長,情況就會變得棘手 - 任何大於應用程式可能在單個操作中讀取的內容。處理多次讀取(和查詢)需要仔細注意讀者在虛擬檔案中的位置 - 該位置很可能是在輸出行的中間。傳統上,核心有許多錯誤的實現。

2.6 核心包含一組函式(由 Alexander Viro 實現),旨在使虛擬檔案建立者可以輕鬆地正確完成這項工作。

seq_file 介面可透過 <linux/seq_file.h> 獲得。seq_file 有三個方面:

  • 一個迭代器介面,允許虛擬檔案實現逐步執行其呈現的物件。

  • 一些實用程式函式,用於格式化物件以進行輸出,而無需擔心輸出緩衝區之類的事情。

  • 一組 canned file_operations,實現了虛擬檔案上的大多數操作。

我們將透過一個非常簡單的例子來了解 seq_file 介面:一個可載入的模組,它建立一個名為 /proc/sequence 的檔案。讀取該檔案時,只會生成一組遞增的整數值,每行一個。該序列將繼續,直到使用者失去耐心並找到更好的事情做。該檔案是可查詢的,因此可以執行以下操作:

dd if=/proc/sequence of=out1 count=1
dd if=/proc/sequence skip=1 of=out2 count=1

然後連線輸出檔案 out1 和 out2 並獲得正確的結果。是的,這是一個完全無用的模組,但重點是展示該機制如何工作,而不會迷失在其他細節中。(想要檢視此模組的完整原始碼的人可以在 https://lwn.net/Articles/22359/ 找到它)。

已棄用的 create_proc_entry

請注意,上面的文章使用了 create_proc_entry,該函式已在核心 3.10 中刪除。當前版本需要以下更新:

-   entry = create_proc_entry("sequence", 0, NULL);
-   if (entry)
-           entry->proc_fops = &ct_file_ops;
+   entry = proc_create("sequence", 0, NULL, &ct_file_ops);

迭代器介面

使用 seq_file 實現虛擬檔案的模組必須實現一個迭代器物件,該物件允許在“會話”期間(大致是一個 read() 系統呼叫)逐步執行感興趣的資料。如果迭代器能夠移動到特定位置 - 就像它們實現的檔案一樣,但可以自由地以任何方便的方式將位置號對映到序列位置 - 則迭代器只需要在會話期間短暫存在。如果迭代器不容易找到數字位置,但可以很好地與 first/next 介面一起使用,則可以將迭代器儲存在私有資料區域中,並從一個會話繼續到下一個會話。

例如,一個 seq_file 實現正在格式化表中的防火牆規則,可以提供一個簡單的迭代器,該迭代器將位置 N 解釋為鏈中的第 N 個規則。一個 seq_file 實現正在呈現可能易失的連結串列的 內容,可以記錄指向該列表的指標,前提是可以這樣做而沒有刪除當前位置的風險。

因此,定位可以以對資料生成器最有意義的任何方式完成,資料生成器無需知道位置如何轉換為虛擬檔案中的偏移量。一個明顯的例外是,零位置應指示檔案的開頭。

/proc/sequence 迭代器僅使用它將輸出的下一個數字的計數作為其位置。

必須實現四個函式才能使迭代器工作。第一個稱為 start(),它啟動一個會話並接受一個位置作為引數,返回一個將在該位置開始讀取的迭代器。傳遞給 start() 的 pos 將始終為零或先前會話中使用的最新 pos。

對於我們的簡單序列示例,start() 函式如下所示:

static void *ct_seq_start(struct seq_file *s, loff_t *pos)
{
        loff_t *spos = kmalloc(sizeof(loff_t), GFP_KERNEL);
        if (! spos)
                return NULL;
        *spos = *pos;
        return spos;
}

此迭代器的整個資料結構是一個包含當前位置的單個 loff_t 值。序列迭代器沒有上限,但對於大多數其他 seq_file 實現而言,情況並非如此;在大多數情況下,start() 函式應檢查“檔案結束”條件,如果需要,則返回 NULL。

對於更復雜的應用程式,seq_file 結構的私有欄位可用於儲存會話到會話的狀態。還有一個特殊值可以由 start() 函式返回,稱為 SEQ_START_TOKEN;如果要指示 show() 函式(如下所述)在輸出頂部列印標題,可以使用它。但是,SEQ_START_TOKEN 只能在偏移量為零時使用。SEQ_START_TOKEN 對核心 seq_file 程式碼沒有特殊含義。它作為 start() 函式與 next() 和 show() 函式通訊的便捷方式提供。

要實現的下一個函式稱為 next(),令人驚訝的是;它的工作是將迭代器前進到序列中的下一個位置。示例模組可以簡單地將位置遞增 1;更有用的模組將執行所需的操作以逐步執行某些資料結構。next() 函式返回一個新的迭代器,如果序列已完成,則返回 NULL。這是示例版本:

static void *ct_seq_next(struct seq_file *s, void *v, loff_t *pos)
{
        loff_t *spos = v;
        *pos = ++*spos;
        return spos;
}

next() 函式應將 *pos 設定為 start() 可以用來查詢序列中新位置的值。當迭代器儲存在私有資料區域中,而不是在每次 start() 上重新初始化時,似乎只需將 *pos 設定為任何非零值即可(零始終告訴 start() 重新啟動序列)。由於歷史問題,這不足以滿足要求。

從歷史上看,許多 next() 函式在檔案結束時沒有更新 *pos。如果該值隨後被 start() 用於初始化迭代器,則可能會導致序列中的最後一個條目在檔案中報告兩次的極端情況。為了阻止這種錯誤再次出現,如果 next() 函式沒有更改 *pos 的值,則核心 seq_file 程式碼現在會生成警告。因此,next() 函式必須更改 *pos 的值,當然必須將其設定為非零值。

stop() 函式關閉一個會話;當然,它的工作是清理。如果為迭代器分配了動態記憶體,則 stop() 是釋放它的位置;如果 start() 獲取了鎖,則 stop() 必須釋放該鎖。在 stop() 之前,上次 next() 呼叫設定為 *pos 的值將被記住,並用於下一個會話的第一次 start() 呼叫,除非已在檔案上呼叫 lseek();在這種情況下,下一個 start() 將被要求從零位置開始。

static void ct_seq_stop(struct seq_file *s, void *v)
{
        kfree(v);
}

最後,show() 函式應格式化迭代器當前指向的物件以進行輸出。示例模組的 show() 函式是:

static int ct_seq_show(struct seq_file *s, void *v)
{
        loff_t *spos = v;
        seq_printf(s, "%lld\n", (long long)*spos);
        return 0;
}

如果一切正常,show() 函式應返回零。通常方式中的負錯誤程式碼表示發生了錯誤;它將被傳遞迴使用者空間。此函式還可以返回 SEQ_SKIP,這將導致跳過當前項;如果在返回 SEQ_SKIP 之前,show() 函式已生成輸出,則該輸出將被刪除。

我們稍後會看到 seq_printf()。但首先,透過建立一個包含我們剛剛定義的四個函式的 seq_operations 結構,完成 seq_file 迭代器的定義:

static const struct seq_operations ct_seq_ops = {
        .start = ct_seq_start,
        .next  = ct_seq_next,
        .stop  = ct_seq_stop,
        .show  = ct_seq_show
};

稍後將需要此結構來將我們的迭代器繫結到 /proc 檔案。

值得注意的是,start() 返回並由其他函式操作的迭代器值被 seq_file 程式碼視為完全不透明。因此,它可以是任何有助於逐步執行要輸出的資料的東西。計數器可能有用,但它也可能是直接指向陣列或連結串列的指標。一切都可以,只要程式設計師知道在呼叫迭代器函式之間可能會發生一些事情。但是,seq_file 程式碼(透過設計)不會在呼叫 start()stop() 之間休眠,因此在此期間保持鎖定是合理的事情。seq_file 程式碼還將在迭代器處於活動狀態時避免獲取任何其他鎖。

保證由 start() 或 next() 返回的迭代器值將傳遞到後續的 next() 或 stop() 呼叫。這允許可靠地釋放已獲取的資源,例如鎖。不能保證迭代器將傳遞給 show(),儘管在實踐中它經常會傳遞。

格式化輸出

seq_file 程式碼管理迭代器建立的輸出中的定位,並將其放入使用者的緩衝區中。但是,要使它工作,必須將該輸出傳遞給 seq_file 程式碼。已定義了一些實用程式函式,使此任務變得容易。

大多數程式碼將只使用 seq_printf(),它的工作方式與 printk() 非常相似,但需要 seq_file 指標作為引數。

對於直接字元輸出,可以使用以下函式:

seq_putc(struct seq_file *m, char c);
seq_puts(struct seq_file *m, const char *s);
seq_escape(struct seq_file *m, const char *s, const char *esc);

前兩個函式分別輸出一個字元和一個字串,就像人們期望的那樣。seq_escape() 類似於 seq_puts(),除了 s 中的任何字元如果在字串 esc 中,都將以八進位制形式表示在輸出中。

還有一對用於列印檔名的函式:

int seq_path(struct seq_file *m, const struct path *path,
             const char *esc);
int seq_path_root(struct seq_file *m, const struct path *path,
                  const struct path *root, const char *esc)

此處,path 指示感興趣的檔案,而 esc 是一組應在輸出中轉義的字元。呼叫 seq_path() 將輸出相對於當前程序的檔案系統根的路徑。如果需要不同的根,則可以將其與 seq_path_root() 一起使用。如果發現無法從根目錄訪問 path,則 seq_path_root() 返回 SEQ_SKIP。

生成複雜輸出的函式可能需要檢查:

bool seq_has_overflowed(struct seq_file *m);

如果返回 true,則避免進一步的 seq_<output> 呼叫。

從 seq_has_overflowed 返回 true 意味著 seq_file 緩衝區將被丟棄,並且 seq_show 函式將嘗試分配更大的緩衝區並重試列印。

使其全部工作

到目前為止,我們有一組不錯的函式,可以在 seq_file 系統中生成輸出,但我們尚未將它們轉換為使用者可以看到的檔案。當然,在核心中建立檔案需要建立一組 file_operations,以實現對該檔案的操作。seq_file 介面提供了一組 canned 操作,這些操作完成了大部分工作。但是,虛擬檔案作者仍然必須實現 open() 方法才能連線所有內容。open 函式通常只有一行,如示例模組中所示:

static int ct_open(struct inode *inode, struct file *file)
{
        return seq_open(file, &ct_seq_ops);
}

此處,對 seq_open() 的呼叫採用我們之前建立的 seq_operations 結構,並設定為迭代虛擬檔案。

成功開啟後,seq_open() 將 struct seq_file 指標儲存在 file->private_data 中。如果您的應用程式中,同一個迭代器可以用於多個檔案,則可以將任意指標儲存在 seq_file 結構的私有欄位中;該值隨後可以由迭代器函式檢索。

還有一個包裝函式 seq_open(),稱為 seq_open_private()。它 kmallocs 一個零填充的記憶體塊,並將指向該記憶體塊的指標儲存在 seq_file 結構的私有欄位中,成功時返回 0。塊大小在函式的第三個引數中指定,例如:

static int ct_open(struct inode *inode, struct file *file)
{
        return seq_open_private(file, &ct_seq_ops,
                                sizeof(struct mystruct));
}

還有一個變體函式 __seq_open_private(),它的功能完全相同,不同之處在於,如果成功,它會返回指向已分配記憶體塊的指標,從而允許進一步初始化,例如:

static int ct_open(struct inode *inode, struct file *file)
{
        struct mystruct *p =
                __seq_open_private(file, &ct_seq_ops, sizeof(*p));

        if (!p)
                return -ENOMEM;

        p->foo = bar; /* initialize my stuff */
                ...
        p->baz = true;

        return 0;
}

相應的 close 函式 seq_release_private() 可用於釋放在相應的 open 中分配的記憶體。

其他感興趣的操作 - read()、llseek() 和 release() - 都由 seq_file 程式碼本身實現。因此,虛擬檔案的 file_operations 結構將如下所示:

static const struct file_operations ct_file_ops = {
        .owner   = THIS_MODULE,
        .open    = ct_open,
        .read    = seq_read,
        .llseek  = seq_lseek,
        .release = seq_release
};

還有一個 seq_release_private(),它將 seq_file 私有欄位的內容傳遞給 kfree(),然後再釋放該結構。

最後一步是建立 /proc 檔案本身。在示例程式碼中,這是以通常的方式在初始化程式碼中完成的:

static int ct_init(void)
{
        struct proc_dir_entry *entry;

        proc_create("sequence", 0, NULL, &ct_file_ops);
        return 0;
}

module_init(ct_init);

差不多就是這樣了。

seq_list

如果您的檔案將迭代連結串列,您可能會發現這些例程很有用:

struct list_head *seq_list_start(struct list_head *head,
                                 loff_t pos);
struct list_head *seq_list_start_head(struct list_head *head,
                                      loff_t pos);
struct list_head *seq_list_next(void *v, struct list_head *head,
                                loff_t *ppos);

這些輔助函式會將 pos 解釋為列表中的位置,並相應地進行迭代。您的 start() 和 next() 函式只需要使用指向適當 list_head 結構的指標來呼叫 seq_list_* 輔助函式。

超簡單版本

對於極其簡單的虛擬檔案,有一個更簡單的介面。一個模組只能定義 show() 函式,該函式應建立虛擬檔案將包含的所有輸出。然後,檔案的 open() 方法呼叫:

int single_open(struct file *file,
                int (*show)(struct seq_file *m, void *p),
                void *data);

當輸出時間到來時,將呼叫 show() 函式一次。提供給 single_open() 的資料值可以在 seq_file 結構的私有欄位中找到。使用 single_open() 時,程式設計師應使用 single_release() 代替 file_operations 結構中的 seq_release(),以避免記憶體洩漏。