目录
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.
");
但通常会为 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] = ' ';
/* 写入 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
相关阅读: