1. 綜述
本系列文章旨在解構內核 perf 框架的實現。perf 是一個龐大的系統,所涉及的邏輯板塊非常多,因此想要把 perf 框架講清楚是不容易的。為了讓讀者能建立起清晰的脈絡,本系列文章會根據一定的內在邏輯,逐步展開對各板塊的解構。 perf 框架其本身因為考慮了很多 general 的需求,比如子任務繼承父任務的 event、perf 框架后端可對接多種 PMU、既支持 per-cpu 亦支持 per-task 的 event 監控、既支持 counting(計數)模式亦支持 sampling(采樣)模式,等等,這些會給解構 perf 框架帶來不必要的麻煩。為強干弱枝我們的解構邏輯脈絡,本系列文章以無繼承、per-task、后端只對接硬件 PMU、counting 模式的 perf 工作流程作為分析切入點。 本文乃系列文章的第一篇。簡要鋪墊 perf 的前端,并對前端所確立的 perf 體系基本模型做闡述。 本文語義下,“perf 框架”指的是實現在內核中的 perf 框架系統,“perf 前端”指的是用戶態的系統調用接口。 本文所涉及 PMU 相關的知識,請自行參閱本號《Intel SDM 之 Performance Monitoring》,或 Intel SDM。
2. perf 前端
2.1 是什么
本文所謂的“perf 前端”,并非指“perf”這個用戶態程序,而是 perf_event_open 這個系統調用的用戶側接口。實際上 perf 程序其底層就是基于 perf_event_open。 libc 并未對此系統調用做用戶態封裝,需要用戶自行封裝。有關 perf_event_open 的細節請讀者自行 "man perf_event_open",本文默認讀者熟悉 perf_event_open 的使用(如果該條件不成立,那么您可能并非本系列文章的潛在受眾),不會對該接口的眾多細節做展示,而只挑選與內核 perf 框架模型有對應關系的點進行展開。
2.2 為什么
perf 框架,前端承接用戶態的各種事件(event)的屬性配置,后端將 event 嫁接到內核的調度、文件系統等框架中,底層對接各種 PMU 硬件,所以其必然要建立一個復雜、嚴謹的模型(抽象)系統。而 perf 前端是整個模型系統對接用戶的最外層,所以搞清楚 perf 前端,是理解 perf 框架其模型系統的一個必要過程。
3. 事件(event)
3.1 是什么
所謂事件,就是用戶所關心的,在 OS 運行過程中所發生的一個 ... ... 好吧,事件。 比方說,你可能關心某個任務在運行中的 ipc(instruction per cycle)指標,那么你需要監控(采樣、計數)任務運行中的兩個事件:instructions、cycles。 比如你可以以固定間隔,定期獲取該周期內此任務的 instruction 及 cycle ?數,然后計算二者的比值即可。 如何獲取這些事件數呢?其后端需要借助 PMU。
3.2 事件類型
事件有很多類型:
PERF_TYPE_HARDWARE
PERF_TYPE_SOFTWARE
PERF_TYPE_TRACEPOINT
PERF_TYPE_HW_CACHE
PERF_TYPE_RAW
PERF_TYPE_BREAKPOINT
我們關注 PERF_TYPE_HARDWARE、PERF_TYPE_SOFTWARE、PERF_TYPE_HW_CACHE、PERF_TYPE_RAW 這四種類型。 實際上,事件類型的本質,就是其后端 PMU 的類型。
3.2.1 hardware
與硬件相關的事件。典型事件有:
PERF_COUNT_HW_CPU_CYCLES
PERF_COUNT_HW_INSTRUCTIONS
PERF_COUNT_HW_CACHE_REFERENCES
PERF_COUNT_HW_CACHE_MISSES
特征是其后端必須使用硬件 PMU 來監控。比如 PERF_COUNT_HW_INSTRUCTIONS,你要想知道運行期間所產生的 instruction 數,就必須借助硬件才能實現。
3.2.2 software
與軟件相關的事件。典型事件有:
PERF_COUNT_SW_PAGE_FAULTS
PERF_COUNT_SW_CONTEXT_SWITCHES
PERF_COUNT_SW_CPU_MIGRATIONS
特征是其后端使用的是一個軟件實現的 PMU 來監控。比如 PERF_COUNT_SW_CONTEXT_SWITCHES,你要想知道運行期間的上下文切換次數,就必須借助內核中基于調度系統實現的軟件 PMU 才能實現。
3.2.3 hw_cache
與 cache 相關的事件。該類事件與上述兩類不同。上述兩類,每一種事件,通過一個 id(事件編碼)來描述;而描述一個 cache 相關的事件,需要三個維度的信息:
cache id:具體是監控哪一級 cache(PERF_COUNT_HW_CACHE_L1D、PERF_COUNT_HW_CACHE_L1I、PERF_COUNT_HW_CACHE_LL 等)。
cache op id:監控的是對 cache 的什么操作(PERF_COUNT_HW_CACHE_OP_READ、PERF_COUNT_HW_CACHE_OP_WRITE、PERF_COUNT_HW_CACHE_OP_PREFETCH)。
cache op result id:操作的結果(PERF_COUNT_HW_CACHE_RESULT_ACCESS、PERF_COUNT_HW_CACHE_RESULT_MISS)。
具體來說,用戶期望監控 LLC 讀操作 miss 事件,則組合 PERF_COUNT_HW_CACHE_LL、PERF_COUNT_HW_CACHE_OP_READ、PERF_COUNT_HW_CACHE_RESULT_MISS 這三個參數,并傳給 perf_event_open 接口。 ? 這類事件,其后端使用的仍然是硬件 PMU(沒錯,與 hardware 類型一樣)。
3.2.4 raw
實際上,hardware 事件的編碼,如 PERF_COUNT_HW_CPU_CYCLES,是對事件編碼的簡化、抽象。 perf 系統對常用的硬件事件提供了形似 PERF_COUNT_HW_CPU_CYCLES 的簡化編碼。一個事件在 PMU 體系中,原教旨的編碼應該是 umask + event select:
圖 1:PERF_COUNT_HW_CPU_CYCLES 事件的 umask + event select 編碼方式
圖 2:IA32_PERFEVTSELx 寄存器格式 更多細節參閱本號《Intel SDM 之 Performance Monitoring》或 Intel SDM。 因為 PMU 可監控的事件非常之多,perf 框架不可能對所有事件都提供抽象編碼,所以如果要對抽象編碼范圍覆蓋之外的其他事件做監控,需要采用 umask + event select 的方式,告訴 perf(PMU)所要監控的事件。 這種采用 umask +event select 方式編碼的事件即是 raw 類型事件。 ? raw 事件的本質,就是 hardware、hw_cache 事件中無法被抽象編碼所覆蓋的那些漏網之魚事件,也是 SDM 中對事件的原教旨編碼方式,所以后端也是硬件 PMU。 在后面文章的分析中我們其實可以看到,hardware、hw_cache 類型事件,其本質就是 raw 事件。
3.2.5 事件類型與 PMU 的關系
這里要注意,PMU 不一定非得是一個硬件,也有軟件實現的 PMU。
圖 3:事件類型與 PMU 的關系
3.3 事件監控模式
事件的監控有兩種模式,一曰 counting,一曰 sampling。
counting 模式很簡單,就是簡單的獲取事件的計數,比如過去 5S 內 cpu 0 或 task 123 運行期間產生的 instruction 數等。
sampling 模式比較復雜,其利用 PMU 定時去對 CPU 當前運行的 ip 等信息進行采樣,并通過一個環形 buffer 將采樣數據給到用戶態。
如綜述所言,本系列文章重點關注 counting 模式。
4. 前端編程基本范式
這里介紹前端的編程范式,不是為了介紹其本身而介紹,主要是為了介紹 perf 框架模型系統的前端呈現。 具體參數配置、接口形式等,請自行 man。
4.1 基本 counting 模式編程
注意 perf_event_attr 中對事件類型、事件編碼的指定。
/* 用戶自己對 syscall 的封裝 */ long perf_event_open(struct perf_event_attr *hw_event, pid_t pid, int cpu, int group_fd, unsigned long flags) { int ret; ret = syscall(__NR_perf_event_open, hw_event, pid, cpu, group_fd, flags); return ret; } int main(void) { struct perf_event_attr pe; long long count; int fd; /* 這里初始化了一個 attr * 此 attr 是向 perf_event_open 刻畫事件屬性的關鍵參數 */ memset(&pe, 0, sizeof(struct perf_event_attr)); /* type:當前要監控的是一個 hardware 類型事件 * 如前文所述,hardware 類型事件,其本質就是 perf 框架提供了抽象事件編碼的硬件事件 */ pe.type = PERF_TYPE_HARDWARE; pe.size = sizeof(struct perf_event_attr); /* config:指定事件的編碼(instruction 事件) */ pe.config = PERF_COUNT_HW_INSTRUCTIONS; /* disable:該事件默認初始是 disabled 模式 */ pe.disabled = 1; /* 不監控 kernel(OS)模式下的事件 */ pe.exclude_kernel = 1; /* perf_event_open pid 入參是 0 * 表明監控當前 task 的 instruction 事件 * 一個事件在用戶態的呈現,就是一個 fd */ fd = perf_event_open(&pe, 0, -1, -1, 0); /* RESET:復位計數值為 0 * ENABLE:enable 此事件(attr 參數中初始此事件是 disabled 的) * 故而需要顯式 enable */ ioctl(fd, PERF_EVENT_IOC_RESET, 0); ioctl(fd, PERF_EVENT_IOC_ENABLE, 0); /* 此 printf 的前后對事件進行了 enable、disable * 所以,本程序的 instruction,本質上就是此 printf 語句運行期間的 instruction。 */ printf("Measuring instruction count for this printf "); ioctl(fd, PERF_EVENT_IOC_DISABLE, 0); /* 讀出事件的計數值 */ read(fd, &count, sizeof(long long)); printf("Used %lld instructions ", count); close(fd); }
4.2 事件組讀取
4.1 節中,對 instruction 單一事件進行監控,并讀取 instruction 的計數。 perf 事件讀取還支持一種 PERF_FORMAT_GROUP 讀取方式,效果是將若干個事件放進一個組內,每個組有一個 group leader。對 group leader 進行讀取,可以一次讀出組內所有事件的計數。 下面演示將一個 hardware 類型事件和一個 raw 類型事件作為一個組,一次讀取整組事件計數的基本范式:
struct read_format { u64 nr; /* The number of events */ u64 values[2]; }; int main(void) { struct perf_event_attr pe; struct read_format count; int group_leader_fd, group_member_fd; /* 創建一個 hardware 類型的事件,不贅述 */ memset(&pe, 0, sizeof(struct perf_event_attr)); pe.type = PERF_TYPE_HARDWARE; pe.size = sizeof(struct perf_event_attr); pe.config = PERF_COUNT_HW_INSTRUCTIONS; pe.disabled = 1; pe.exclude_kernel = 1; /* 這里指定采用 PERF_FORMAT_GROUP 讀取方式 * 注意,只在 group leader 時需要指定該參數 */ pe.read_format = PERF_FORMAT_GROUP; /* 創建 group leader */ group_leader_fd = perf_event_open(&pe, 0, -1, -1, 0); /* 創建一個 raw 類型的事件 * 假設要監控的事件編碼:umask = 0x00, event_select = 0x3c * 實際上此事件就是 UnHalted Core Cycles(也就是 hardware 類型的 PERF_COUNT_HW_CPU_CYCLES) */ memset(&pe, 0, sizeof(struct perf_event_attr)); pe.type = PERF_TYPE_RAW; pe.size = sizeof(struct perf_event_attr); /* 注意 raw 類型事件,config 的編碼方式 */ pe.config = (0x00 << 4) | 0x3c; pe.disabled = 1; pe.exclude_kernel = 1; /* 創建 group member,perf_event_open 的 group_fd 參數指定為 group_leader_fd */ group_member_fd = perf_event_open(&pe, 0, -1, group_leader_fd, 0); /* 組模式下,只需要操作 group leader 即可 */ ioctl(group_leader_fd, PERF_EVENT_IOC_RESET, 0); ioctl(group_leader_fd, PERF_EVENT_IOC_ENABLE, 0); printf("Measuring instruction count for this printf "); ioctl(group_leader_fd, PERF_EVENT_IOC_DISABLE, 0); /* 讀出事件組的計數值,注意這里入參 count 是一個 struct read_format */ read(group_leader_fd, &count, sizeof(count)); /* 只有 count.nr 與當前組成員數(包括 leader、member)匹配,才是合法的數據 */ if (count.nr == 2) printf("Used %llu instructions, %llu cycles ", count.values[0], count.values[1]); close(group_leader_fd); close(group_member_fd); }
4.3 模型總結
perf_event_open 支持 per-task 級別的事件監控,pid 入參傳入目標 task 的 pid 即可。如綜述所言,per-cpu 級別的事件監控本系列文章不做重點討論。
perf_event_open 支持對事件進行分組,group leader 的 attr 需要帶上 PERF_FORMAT_GROUP 參數,group member 的創建需要指定 group leader。
5. 總結
本文簡要介紹 perf 前端的編程范式,意在為后續 perf 框架中模型系統的抽象建立直觀感受。
編輯:黃飛
評論
查看更多