目錄
DPDK 的日誌系統
在版本較新的 DPDK 中引入了動態類型日誌系統。此外,除了原來支持的全局日誌輸出,也支持了針對單獨某個模塊的日誌輸出。本文以 18.05 版本進行闡述。
RTE_LOG 宏
DPDK 封裝好了 RTE_LOG 宏供開發 App 使用,如下:
// x86_64-native-linuxapp-gcc/include/rte_log.h
/**
* Generates a log message.
*
* The RTE_LOG() is a helper that prefixes the string with the log level
* and type, and call rte_log().
*
* @param l
* Log level. A value between EMERG (1) and DEBUG (8). The short name is
* expanded by the macro, so it cannot be an integer value.
* @param t
* The log type, for example, EAL. The short name is expanded by the
* macro, so it cannot be an integer value.
* @param ...
* The fmt string, as in printf(3), followed by the variable arguments
* required by the format.
* @return
* - 0: Success.
* - Negative on error.
*/
#define RTE_LOG(l, t, ...) \
rte_log(RTE_LOG_ ## l, \
RTE_LOGTYPE_ ## t, # t ": " __VA_ARGS__)
可見,RTE_LOG 宏包含定義爲可變長參數,其中包括兩個非常關鍵的必選參數:
- 日誌等級
- 日誌類型
日誌等級的定義如下:
/* Can't use 0, as it gives compiler warnings */
#define RTE_LOG_EMERG 1U /**< System is unusable. */
#define RTE_LOG_ALERT 2U /**< Action must be taken immediately. */
#define RTE_LOG_CRIT 3U /**< Critical conditions. */
#define RTE_LOG_ERR 4U /**< Error conditions. */
#define RTE_LOG_WARNING 5U /**< Warning conditions. */
#define RTE_LOG_NOTICE 6U /**< Normal but significant condition. */
#define RTE_LOG_INFO 7U /**< Informational. */
#define RTE_LOG_DEBUG 8U /**< Debug-level messages. */
日誌類型定義如下:
/* SDK log type */
#define RTE_LOGTYPE_EAL 0 /**< Log related to eal. */
#define RTE_LOGTYPE_MALLOC 1 /**< Log related to malloc. */
#define RTE_LOGTYPE_RING 2 /**< Log related to ring. */
#define RTE_LOGTYPE_MEMPOOL 3 /**< Log related to mempool. */
#define RTE_LOGTYPE_TIMER 4 /**< Log related to timers. */
#define RTE_LOGTYPE_PMD 5 /**< Log related to poll mode driver. */
#define RTE_LOGTYPE_HASH 6 /**< Log related to hash table. */
#define RTE_LOGTYPE_LPM 7 /**< Log related to LPM. */
#define RTE_LOGTYPE_KNI 8 /**< Log related to KNI. */
#define RTE_LOGTYPE_ACL 9 /**< Log related to ACL. */
#define RTE_LOGTYPE_POWER 10 /**< Log related to power. */
#define RTE_LOGTYPE_METER 11 /**< Log related to QoS meter. */
#define RTE_LOGTYPE_SCHED 12 /**< Log related to QoS port scheduler. */
#define RTE_LOGTYPE_PORT 13 /**< Log related to port. */
#define RTE_LOGTYPE_TABLE 14 /**< Log related to table. */
#define RTE_LOGTYPE_PIPELINE 15 /**< Log related to pipeline. */
#define RTE_LOGTYPE_MBUF 16 /**< Log related to mbuf. */
#define RTE_LOGTYPE_CRYPTODEV 17 /**< Log related to cryptodev. */
#define RTE_LOGTYPE_EFD 18 /**< Log related to EFD. */
#define RTE_LOGTYPE_EVENTDEV 19 /**< Log related to eventdev. */
#define RTE_LOGTYPE_GSO 20 /**< Log related to GSO. */
/* these log types can be used in an application */
#define RTE_LOGTYPE_USER1 24 /**< User-defined log type 1. */
#define RTE_LOGTYPE_USER2 25 /**< User-defined log type 2. */
#define RTE_LOGTYPE_USER3 26 /**< User-defined log type 3. */
#define RTE_LOGTYPE_USER4 27 /**< User-defined log type 4. */
#define RTE_LOGTYPE_USER5 28 /**< User-defined log type 5. */
#define RTE_LOGTYPE_USER6 29 /**< User-defined log type 6. */
#define RTE_LOGTYPE_USER7 30 /**< User-defined log type 7. */
#define RTE_LOGTYPE_USER8 31 /**< User-defined log type 8. */
/** First identifier for extended logs */
#define RTE_LOGTYPE_FIRST_EXT_ID 32
DPDK App 使用 RTE_LOG 的簡單示例如下:
RTE_LOG(INFO, EAL, "Just a test.\n");
但通常會爲 RTE_LOG 在封裝一層:
#define DPDK_APP_LOG(level,...) RTE_LOG(level, DPDK_APP, "["#level"] "__VA_ARGS__)
rte_log 和 rte_vlog 函數
int rte_log(uint32_t level, uint32_t logtype, const char *format, ...)
{
va_list ap;
int ret;
va_start(ap, format);
ret = rte_vlog(level, logtype, format, ap);
va_end(ap);
return ret;
}
rte_log 函數很簡單,就是一個可變長函數的實現,直接調用了 rte_vlog 函數。
int rte_vlog(uint32_t level, uint32_t logtype, const char *format, va_list ap)
{
int ret;
FILE *f = rte_logs.file; /* 日誌輸出到全局變量指向的終端或文件 */
/* 如果沒有設置全局變量,則打印到默認終端 */
if (f == NULL) {
/* default_log_stream 的設置在 eal_log_set_default 函數中,是在對日誌進行初始化時設置的 */
f = default_log_stream;
/* 如果默認輸出的地方也沒設置,則直接輸出到 stderr */
if (f == NULL) {
f = stderr;
}
}
/* 如果當前的打印等級超過設置的全局等級,直接退出 */
if (level > rte_logs.level)
return 0;
/* 如果當前日誌類型超出範圍,直接退出並返回 -1,表示存在問題 */
if (logtype >= rte_logs.dynamic_types_len)
return -1;
/* 如果當前日誌等級超出當前模塊設置的等級,直接退出 */
if (level > rte_logs.dynamic_types[logtype].loglevel)
return 0;
/* save loglevel and logtype in a global per-lcore variable */
/* 記錄當前的打印等級,目前在調用 syslog 函數時使用 */
RTE_PER_LCORE(log_cur_msg).loglevel = level;
RTE_PER_LCORE(log_cur_msg).logtype = logtype;
ret = vfprintf(f, format, ap);
fflush(f);
return ret;
}
日誌模塊初始化
DPDK 日誌模塊的初始化分爲兩部分:
- 第一階段(日誌模塊前期初始化):是在 DPDK 模塊被加載時執行的初始化,這部分初始化使得日誌模塊在 DPDK 的其他模塊初始化過程中可以使用。
- 第二階段(日誌模塊後期初始化):在 DPDK 環境抽象層初始化(rte_eal_init)時執行,用來設置日誌模塊的輸出。
第一階段初始化
rte_logs 結構體類型:
struct rte_logs {
uint32_t type; /**< 已啓用的日誌的位字段 */
uint32_t level; /**< 日誌級別 */
FILE *file; /**< 由 rte_openlog_stream 指定的輸出文件,或者爲 NULL */
size_t dynamic_types_len; /**< 動態日誌類型數量 */
struct rte_log_dynamic_type *dynamic_types; /**< 存儲每種動態類型的名字與級別 */
};
- type:表示啓用的 logs 模塊的位掩碼。在 18.05 版本中,這個變量還沒有用到。
- level:表示全局日誌級別,只輸出不大於該級別的日誌。
- file:表示日誌輸出的地方,可以是終端,也可以是指定的文件。
- dynamic_types_len:表示動態日誌類型的數量,每添加一個類型,該變量增加 1。
- dynamic_types:指向表示動態日誌類型的結構體,保存的日誌類型的名字的設置的等級,定義如下:
struct rte_log_dynamic_type {
const char *name; /* 日誌類型的名字 */
uint32_t loglevel; /* 該日誌類型的日誌級別,日誌模塊只輸出不大於該級別,不大於全局日誌級別的日誌 */
};
定義 rte_logs 全局日誌變量:
/* global log structure */
struct rte_logs rte_logs = {
.type = ~0, /* 默認啓用全部日誌類型 */
.level = RTE_LOG_DEBUG, /* 默認級別爲 DEBUG,即輸出所有日誌 */
.file = NULL,
};
第一階段的初始化:
RTE_INIT_PRIO(rte_log_init, LOG);
static void rte_log_init(void)
{
uint32_t i;
/* 設置 rte_logs.level */
rte_log_set_global_level(RTE_LOG_DEBUG);
/* 爲所有日誌類型分配內存,默認共 RTE_LOGTYPE_FIRST_EXT_ID(爲 32)個類型 */
rte_logs.dynamic_types = calloc(RTE_LOGTYPE_FIRST_EXT_ID,
sizeof(struct rte_log_dynamic_type));
if (rte_logs.dynamic_types == NULL)
return;
/* register legacy log types */
for (i = 0; i < RTE_DIM(logtype_strings); i++)
/* 註冊 logtype_strings[] 中定義的所有日誌類型 */
__rte_log_register(logtype_strings[i].logtype,
logtype_strings[i].log_id);
rte_logs.dynamic_types_len = RTE_LOGTYPE_FIRST_EXT_ID;
}
logtype_strings[] 數組的定義如下:
struct logtype {
uint32_t log_id;
const char *logtype;
};
static const struct logtype logtype_strings[] = {
{RTE_LOGTYPE_EAL, "lib.eal"},
{RTE_LOGTYPE_MALLOC, "lib.malloc"},
{RTE_LOGTYPE_RING, "lib.ring"},
{RTE_LOGTYPE_MEMPOOL, "lib.mempool"},
……
{RTE_LOGTYPE_EVENTDEV, "lib.eventdev"},
{RTE_LOGTYPE_GSO, "lib.gso"},
{RTE_LOGTYPE_USER1, "user1"},
{RTE_LOGTYPE_USER2, "user2"},
{RTE_LOGTYPE_USER3, "user3"},
{RTE_LOGTYPE_USER4, "user4"},
{RTE_LOGTYPE_USER5, "user5"},
{RTE_LOGTYPE_USER6, "user6"},
{RTE_LOGTYPE_USER7, "user7"},
{RTE_LOGTYPE_USER8, "user8"}
};
__rte_log_register 函數註冊日誌類型,將表示日誌類型的 id 與類型名保存到 rte_logs.dynamic_types 中,返回該類型在 rte_logs.dynamic_types 中的索引,因此這些日誌類型,如 RTE_LOGTYPE_EAL 等,同時也成爲了該類型在 rte_logs.dynamic_types 中的索引:
static int __rte_log_register(const char *name, int id)
{
char *dup_name = strdup(name);
if (dup_name == NULL)
return -ENOMEM;
rte_logs.dynamic_types[id].name = dup_name;
rte_logs.dynamic_types[id].loglevel = RTE_LOG_INFO;
return id;
}
最後設置日誌類型的數量:
rte_logs.dynamic_types_len = RTE_LOGTYPE_FIRST_EXT_ID;
可以看出,第一階段的日誌模塊初始化是爲了完成 DPDK 本身的日誌配置,爲後續加載 DPDK 其他模塊的時候提供日誌支持。
第二階段初始化
日誌模塊的後期初始化是通過在 rte_eal_init 函數中調用 rte_eal_log_init 函數來完成,該函數定義如下:
int rte_eal_log_init(const char *id, int facility)
{
FILE *log_stream;
log_stream = fopencookie(NULL, "w+", console_log_func);
if (log_stream == NULL)
return -1;
openlog(id, LOG_NDELAY | LOG_PID, facility);
eal_log_set_default(log_stream);
return 0;
}
首先使用 fopencook 函數創建了一個標準輸入輸出流:
extern FILE *fopencookie (void *__restrict __magic_cookie,
const char *__restrict __modes,
_IO_cookie_io_functions_t __io_funcs) __THROW __wur;
- __magic_cookie:是一種自定義的數據結構,用於和後面的 __io_funcs 配合使用,可以爲 NULL。
- __modes:表示打開方式,和 fopen 相同,包括:r、w、a、r+、w+、a+ 等。
- __io_funcs:是由四個函數指針(read、write、seek、close)組成的函數集,需要用戶實現這四個函數指針。
console_log_func 函數的定義如下:
static cookie_io_functions_t console_log_func = {
.write = console_log_write,
};
由於日誌模塊只需要輸出字符串,因此變量只實現了寫函數 console_log_write 函數,定義如下:
static ssize_t
console_log_write(__attribute__((unused)) void *c, const char *buf, size_t size)
{
char copybuf[BUFSIZ + 1];
ssize_t ret;
uint32_t loglevel;
/* 將日誌內容輸出到 stdout 標準輸出 */
ret = fwrite(buf, 1, size, stdout);
fflush(stdout);
/* truncate message if too big (should not happen) */
/* syslog 日誌輸出,每次最多輸出 BUFSIZ 個字符 */
if (size > BUFSIZ)
size = BUFSIZ;
/* Syslog error levels are from 0 to 7, so subtract 1 to convert */
/* 計算 syslog 日誌輸出級別。由於級別的定義是從 1 開始定義的,這裏需要減去 1 */
loglevel = rte_log_cur_msg_loglevel() - 1;
memcpy(copybuf, buf, size);
copybuf[size] = '\0';
/* 寫入 syslog 日誌 */
syslog(loglevel, "%s", copybuf);
return ret;
}
syslog 日誌初始化:
openlog(id, LOG_NDELAY | LOG_PID, facility);
設置 DPDK 日誌模塊的輸出對象:
eal_log_set_default(log_stream);
將剛剛創建並初始化的 log_stream,賦值給 default_log_stream。根據前文 rte_vlog 函數的定義,在輸出日誌時會使用。也就是說,默認情況下,DPDK 的標準日誌模塊會向標準輸出 stdout 輸出日誌信息,同時會向 syslog 中輸出日誌信息。
可見,DPDK 日誌模塊第二階段的初始化主要關注日誌的使用和打印方式,實際上這些主要都交由 DPDK App 來進行設置。
註冊新的日誌類型
除了上述 DPDK 默認的日誌類型外,DPDK 的日誌系統也支持爲 DPDK App 註冊新的日誌類型,也就是所謂的動態熱值類型系統。
- 添加新的日誌類型,通過 rte_log_register 函數,可以向系統中添加新的日誌類型。該函數定義如下:
/* register an extended log type */
int rte_log_register(const char *name)
{
struct rte_log_dynamic_type *new_dynamic_types;
int id, ret;
/* 在 rte_logs.dynamic_types 中查詢是否已經註冊同名日誌類型,如果已經註冊,則直接返回該類型的索引 */
id = rte_log_lookup(name);
if (id >= 0)
return id;
/* 爲新類型重新分配內存,爲 rte_logs.dynamic_types 增加一個類型的空間,並將其指向新類型 */
new_dynamic_types = realloc(rte_logs.dynamic_types,
sizeof(struct rte_log_dynamic_type) * (rte_logs.dynamic_types_len + 1));
if (new_dynamic_types == NULL)
return -ENOMEM;
rte_logs.dynamic_types = new_dynamic_types;
/* 將新類型註冊到 rte_logs.dynamic_types 中,索引就是 rte_logs.dynamic_types 空間中新分配的位置 */
ret = __rte_log_register(name, rte_logs.dynamic_types_len);
if (ret < 0)
return ret;
/* 類型總數增加 */
rte_logs.dynamic_types_len++;
return ret;
}
- 設置/獲取日誌的打印級別,包含了全局日誌打印級別的獲取與設置函數,以及指定模塊的日誌打印級別的獲取與設置函數。定義如下:
/* Set global log level */
/* 設置全局日誌打印級別 */
void rte_log_set_global_level(uint32_t level)
{
rte_logs.level = (uint32_t)level;
}
/* Get global log level */
/* 獲取全局日誌打印級別 */
uint32_t rte_log_get_global_level(void)
{
return rte_logs.level;
}
/* 獲取指定日誌類型的打印級別 */
int rte_log_get_level(uint32_t type)
{
if (type >= rte_logs.dynamic_types_len)
return -1;
return rte_logs.dynamic_types[type].loglevel;
}
/* 設置指定日誌類型的打印級別 */
int rte_log_set_level(uint32_t type, uint32_t level)
{
if (type >= rte_logs.dynamic_types_len)
return -1;
if (level > RTE_LOG_DEBUG)
return -1;
rte_logs.dynamic_types[type].loglevel = level;
return 0;
}
- 應用舉例,以 testpmd 這個程序爲例,在 main 函數就註冊了一個新的日誌類型:
testpmd_logtype = rte_log_register("testpmd");
返回值 testpmd_logtype 同時也表示了該類型在 rte_logs.dynamic_types 中的索引。之後設置該類型的打印級別爲 DEBUG,打印所有該類型的日誌:
rte_log_set_level(testpmd_logtype, RTE_LOG_DEBUG);
同時,在 testpmd 示例的頭文件中,還對日誌函數進行了對應的封裝:
#define TESTPMD_LOG(level, fmt, args...) \
rte_log(RTE_LOG_ ## level, testpmd_logtype, "testpmd: " fmt, ## args)
此後,就可以在 testpmd 程序中使用 TESTPMD_LOG 宏進行日誌打印了。
複用現有日誌類型
除了添加新的日誌類型,也可以複用現有的日誌類型,以 exception_path 這個示例程序爲例。在 ip_fragmentation/main.c 文件中,有如下定義:
#define RTE_LOGTYPE_IP_FRAG RTE_LOGTYPE_USER1
其中,RTE_LOGTYPE_USER1 到 RTE_LOGTYPE_USER8 這 8 個類型,就是爲用戶自定義準備的,所以可以直接使用。在使用時,調用 RTE_LOG 宏時直接使用 IP_FRAG 即可:
RTE_LOG(INFO, IP_FRAG, "XXXXXX");
參考文檔
https://www.sunxidong.com/36.html