此前工做中,筆者使用perf測過CPU的CPI[1],cache miss, 內存帶寬等性能指標。另外,還移植過perf uncore[2]相關的補丁。這些讓我很好奇:perf大概是怎麼工做的? 帶着這個問題,筆者謹但願把本身的一點經驗分享出來。html
perf list列出的event有這幾類:1. hardware,如cache-misses; 2. software, 如context switches; 3. cache, 如L1-dcache-loads;4. tracepoint; 5. pmu。 可是,perf list僅僅把有符號名稱的事件列出來了,而缺了不少硬件相關的事件。這些硬件相關事件叫做Raw Hardware Event, man perf-list
有介紹。node
舉個例子,PMU是一組監控CPU各類性能的硬件,包括各類core, offcore和uncore事件。單說perf uncore, Intel處理器就提供了各類的性能監控單元,如內存控制器(IMC), 電源控制(PCU)等等,詳見《Intel® Xeon® Processor E5 and E7 v4 Product Families Uncore Performance Monitoring Reference Manual》[3]。這些uncore的PMU設備,註冊在MSR space或PCICFG space[4],能夠經過下面命令看到(抹掉同類別設備):linux
$ls /sys/devices/ | grep uncore uncore_cbox_0 uncore_ha_0 uncore_imc_0 uncore_pcu uncore_qpi_0 uncore_r2pcie uncore_r3qpi_0 uncore_ubox
可是,使用perf list
只能顯示IMC相關事件:編程
$perf list|grep uncore uncore_imc_0/cas_count_read/ [Kernel PMU event] uncore_imc_0/cas_count_write/ [Kernel PMU event] uncore_imc_0/clockticks/ [Kernel PMU event] ... uncore_imc_3/cas_count_read/ [Kernel PMU event] uncore_imc_3/cas_count_write/ [Kernel PMU event] uncore_imc_3/clockticks/ [Kernel PMU event]
爲何perf list沒有顯示其餘uncore事件呢?從代碼分析來看,perf list
會經過sysfs去讀取uncore設備所支持的event, 見linux/tools/perf/util/pmu.c:pmu_aliases()
:性能優化
/* * Reading the pmu event aliases definition, which should be located at: * /sys/bus/event_source/devices/<dev>/events as sysfs group attributes. */ static int pmu_aliases(const char *name, struct list_head *head)
再看perf uncore的驅動代碼,發現只有iMC uncore設備註冊了events相關屬性, 見arch/x86/events/intel/uncore_snbep.c:hswep_uncore_imc_events
:數據結構
static struct uncore_event_desc hswep_uncore_imc_events[] = { INTEL_UNCORE_EVENT_DESC(clockticks, "event=0x00,umask=0x00"), INTEL_UNCORE_EVENT_DESC(cas_count_read, "event=0x04,umask=0x03"), INTEL_UNCORE_EVENT_DESC(cas_count_read.scale, "6.103515625e-5"), INTEL_UNCORE_EVENT_DESC(cas_count_read.unit, "MiB"), INTEL_UNCORE_EVENT_DESC(cas_count_write, "event=0x04,umask=0x0c"), INTEL_UNCORE_EVENT_DESC(cas_count_write.scale, "6.103515625e-5"), INTEL_UNCORE_EVENT_DESC(cas_count_write.unit, "MiB"), { /* end: all zeroes */ }, };
從實用性看,在全部uncore設備中,系統工程師可能最經常使用的就是iMC提供的內存帶寬監測。其它不經常使用到的uncore PMU事件,能夠經過Raw Hardware Event的方式,查看Intel Uncore手冊[5]來指定。async
在使用過程當中,發現一個perf list
存在的bug,iMC channel的編號不正確,發了個補丁獲得了Intel工程師review,upstream尚未merge,見perf/x86/intel/uncore: allocate pmu index for pci device dynamically
[6]。這是一個很明顯的問題,剛開始我不相信上游或Intel會容許這樣明顯的問題存在,雖然問題不大,經過解決這個問題的感覺是perf可能隱藏一些問題,須要在測試中提升警戒,最好能經過其餘測量方式進行粗略的對比驗證。函數
perf-stat是最經常使用到的命令,用man手冊的原話就是Run a command and gathers performance counter statistics from it。perf-record命令可看作是perf-stat的一種包裝,核心代碼路徑與perf-stat同樣,加上週期性採樣,用一種可被perf-report解析的格式將結果輸出到文件。所以,很好奇perf-stat是如何工做的。工具
perf是由用戶態的perf tool命令和內核態perf驅動兩部分,加上一個連通用戶態和內核態的系統調用sys_perf_event_open組成。性能
perf工具是隨內核tree一塊兒維護的,構建和調試都很是方便:
$cd linux/tools/perf $make ... $./perf stat ls ... Performance counter stats for 'ls': 1.011337 task-clock:u (msec) # 0.769 CPUs utilized 0 context-switches:u # 0.000 K/sec 0 cpu-migrations:u # 0.000 K/sec 105 page-faults:u # 0.104 M/sec 1,105,427 cycles:u # 1.093 GHz 1,406,263 instructions:u # 1.27 insn per cycle 282,440 branches:u # 279.274 M/sec 9,686 branch-misses:u # 3.43% of all branches 0.001314310 seconds time elapsed
以上是一個很是簡單的perf-stat命令,運行了ls
命令,在沒有指定event的狀況下,輸出了幾種默認的性能指標。下面,咱們以這個簡單的perf-stat命令爲例分析其工做過程。
若是perf-stat命令沒有經過-e
參數指定任何event,函數add_default_attributes()
會默認添加8個events。 event
是perf工具的核心對象,各類命令都是圍繞着event工做。perf-stat命令能夠同時指定多個events,由一個核心全局變量struct perf_evlist *evsel_list
組織起來,如下僅列出幾個很重要的成員:
struct perf_evlist { struct list_head entries; bool enabled; struct { int cork_fd; pid_t pid; } workload; struct fdarray pollfd; struct thread_map *threads; struct cpu_map *cpus; struct events_stats stats; ... }
ls
命令的進程pid;-t
參數指定多個線程,僅在這些線程運行時進行計數; -C
參數指定多個cpu, 僅當程序運行在這些cpu上時纔會計數;perf_evlist::entries是一個event鏈表,連接的對象是一個個event,由struct perf_evsel
表示,其中很是重要的成員以下:
struct perf_evsel { char *name; struct perf_event_attr attr; struct perf_counts *counts; struct xyarray *fd; struct cpu_map *cpus; struct thread_map *threads; }
struct xyarray
表示的二維表格,最終的計數值被分解成cpus*threads
個小的counter,sys_perf_event_open()
請求perf驅動爲每一個份量值建立一個子counter,並分別返回一個fd;perf的性能計數器本質上是一些特殊的硬件寄存器,perf對這樣的硬件能力進行抽象,提供針對event的per-CPU和per-thread的64位虛機計數器("virtual" 64-bit counters)
。當perf-stat不指定任何thread或cpu時,這樣的一個二維表格就變成一個點,即一個event對應一個counter,對應一個fd。
簡單介紹了核心數據結構,終於能夠繼續看看perf-stat的工做流了。perf-stat的工做邏輯主要在__run_perf_stat()
中,大體是這樣: a. fork一個子進程,準備用來運行cmd,即示例中的ls
命令;b. 爲每個event事件,經過sys_perf_event_open()
系統調用,建立一個counter; c. 經過管道給子進程發消息,exec命令, 即運行示例中的ls
命令, 並當即enable計數器; d. 當程序運行結束後,disable計數器,並讀取counter。 用戶態的工做流大體以下:
__run_perf_stat() perf_evlist__prepare_workload() create_perf_stat_counter() sys_perf_event_open() enable_counters() perf_evsel__run_ioctl(evsel, ncpus, nthreads, PERF_EVENT_IOC_DISABLE) ioctl(fd, ioc, arg) wait() disable_counters() perf_evsel__run_ioctl(evsel, ncpus, nthreads, PERF_EVENT_IOC_ENABLE) read_counters() perf_evsel__read(evsel, cpu, thread, count) readn(fd, count, size)
用戶態工做流比較清晰,最終均可以很方便經過ioctl()控制計數器,經過read()讀取計數器的值。而這樣方便的條件都是perf系統調sys_perf_event_open()
用創造出來的,已經火燒眉毛想看看這個系統調用作了些什麼。
perf系統調用會爲一個虛機計數器(virtual counter)
打開一個fd,而後perf-stat就經過這個fd向perf內核驅動發請求。perf系統調用定義以下(linux/kernel/events/core.c):
/** * sys_perf_event_open - open a performance event, associate it to a task/cpu * * @attr_uptr: event_id type attributes for monitoring/sampling * @pid: target pid * @cpu: target cpu * @group_fd: group leader event fd */ SYSCALL_DEFINE5(perf_event_open, struct perf_event_attr __user *, attr_uptr, pid_t, pid, int, cpu, int, group_fd, unsigned long, flags)
特別提一下, struct perf_event_attr
是一個信息量很大的結構體,kernel中有文檔詳細介紹[7]。其它參數如何使用,man手冊有詳細的解釋,而且手冊最後還給出了用戶態編程例子,見man perf_event_open
。
sys_perf_event_open()
主要作了這幾件事情:
a. 根據struct perf_event_attr
,建立和初始化struct perf_event
, 它包含幾個重要的成員:
/** * struct perf_event - performance event kernel representation: */ struct perf_event { struct pmu *pmu; //硬件pmu抽象 local64_t count; // 64-bit virtual counter u64 total_time_enabled; u64 total_time_running; struct perf_event_context *ctx; // 與task相關 ... }
b. 爲這個event找到或建立一個struct perf_event_context
, context和event是1:N的關係,一個context會與一個進程的task_struct關聯,perf_event_count::event_list
表示全部對這個進程感興趣的事件, 它包括幾個重要成員:
struct perf_event_context { struct pmu *pmu; struct list_head event_list; struct task_struct *task; ... }
c. 把event與一個context進行關聯,見perf_install_in_context()
;
d. 最後,把fd和perf_fops
進行綁定:
static const struct file_operations perf_fops = { .llseek = no_llseek, .release = perf_release, .read = perf_read, .poll = perf_poll, .unlocked_ioctl = perf_ioctl, .compat_ioctl = perf_compat_ioctl, .mmap = perf_mmap, .fasync = perf_fasync, };
perf系統調用大體的調用鏈以下:
sys_perf_event_open() get_unused_fd_flags() perf_event_alloc() find_get_context() alloc_perf_context() anon_inode_getfile() perf_install_in_context() add_event_to_ctx() fd_install(event_fd, event_file)
perf event有兩種方式:計數(counting)和採樣(sampled)。計數方式會對發生在全部指定cpu和指定進程的事件次數進行求和,對事件數值經過read()
得到。而採樣方式會週期性地把計數結果放在由mmap()
建立的ring buffer中。回到開始的簡單perf-stat
示例,用的是計數(counting)方式。
接下來,咱們主要了解這幾個問題:
回答這些問題的入口,基本都在perf實現的文件操做集中:
static const struct file_operations perf_fops = { .read = perf_read, .unlocked_ioctl = perf_ioctl, ...
首先,咱們看一下怎樣enable計數器的,主要步驟以下:
perf_ioctl() __perf_event_enable() ctx_sched_out() IF ctx->is_active ctx_resched() perf_pmu_disable() task_ctx_sched_out() cpu_ctx_sched_out() perf_event_sched_in() event_sched_in() event->pmu->add(event, PERF_EF_START) perf_pmu_enable() pmu->pmu_enable(pmu)
這個過程有不少調度相關的處理,使整個邏輯顯得複雜,咱們暫且不關心太多調度細節。硬件的PMU資源是有限的,當event數量多於可用的PMC時,多個virtual counter
就會複用硬件PMC。所以, PMU先把event添加到激活列表(pmu->add(event, PERF_EF_START)
), 最後enable計數(pmu->pmu_enable(pmu)
)。PMU是CPU體系結構相關的,能夠想象它有一套爲event分配具體硬件PMC的邏輯,咱們暫不深究。
咱們繼續瞭解如何獲取計數器結果,大體的callchain以下:
perf_read() perf_read_one() perf_event_read_value() __perf_event_read() pmu->start_txn(pmu, PERF_PMU_TXN_READ) pmu->read(event) pmu->commit_txn(pmu)
PMU最終會經過rdpmcl(counter, val)
得到計數器的值,保存在perf_event::count
中。關於PMU各類操做說明,能夠參考include/linux/perf_event.h:struct pmu{}
。PMU操做的實現是體系結構相關的,x86上的read()的實現是arch/x86/events/core.c:x86_pmu_read()
。
event能夠設置限定條件,僅當指定的進程運行在指定的cpu上時,才能進行計數,這就是上面提到的計數時機問題。很容易想到,這樣的時機發生在進程切換的時候。當目標進程切換出目標CPU時,PMU中止計數,並將硬件寄保存在內存變量中,反之亦然,這個過程相似進程切換時對硬件上下文的保護。在kernel/sched/core.c
, 咱們能看到這些計數時機。
在進程切換前:
prepare_task_switch() perf_event_task_sched_out() __perf_event_task_sched_out() // stop each event and update the event value in event->count perf_pmu_sched_task() pmu->sched_task(cpuctx->task_ctx, sched_in)
進程切換後:
finish_task_switch() perf_event_task_sched_in() perf_event_context_sched_in() perf_event_sched_in()
經過對perf-list和perf-stat這兩個基本的perf命令進行分析,引出了一些有意思的問題,在嘗試回答這些問題的過程當中,基本上總結了目前我對perf這個工具的認識。可是,本文僅對perf的工做原理作了很粗略的梳理,也沒有展開對PMU層,perf uncore等硬件相關代碼進行分析,但願之後能補上這部份內容。
最後,能堅持看到最後的親們都是但願更深瞭解性能測試的,做爲福利給你們推薦本書: 《system performance: enterprise and the cloud》 書的做者是一位從事多年性能優化工做的一線工程師,想必你們都據說過他寫的火焰圖程序: perf Examples
Cheers!