dpdk源码分析:交互式命令行的实现(一) 命令添加

  • 本文已参加【新人创作礼】活动,一起开启掘金创作之路。
  • 在实习的时候就对dpdk中 cmdline 的实现方式感兴趣,苦于待干的事情太多,一直没有空记录。
  • 最近抽出时间准备进行一次较为深入的分析,来看一下dpdk是如何使用c语言来实现 cmdline 的.
  • 本文中使用的dpdk库版本为:16.04

一、分析

1. 从一段代码入手

本段代码摘自 app/test-pmd/cmdline.c,这是dpdk例程 testpmd 其中的一个命令实现。

/* *** SHOW PORT INFO *** */
struct cmd_showport_result {
	cmdline_fixed_string_t show;
	cmdline_fixed_string_t port;
	cmdline_fixed_string_t what;
	uint8_t portnum;
};

static void cmd_showport_parsed(void *parsed_result,
				__attribute__((unused)) struct cmdline *cl,
				__attribute__((unused)) void *data)
{
	struct cmd_showport_result *res = parsed_result;
	...若干实现
}

cmdline_parse_token_string_t cmd_showport_show =
	TOKEN_STRING_INITIALIZER(struct cmd_showport_result, show,
				 "show#clear");
cmdline_parse_token_string_t cmd_showport_port =
	TOKEN_STRING_INITIALIZER(struct cmd_showport_result, port, "port");
cmdline_parse_token_string_t cmd_showport_what =
	TOKEN_STRING_INITIALIZER(struct cmd_showport_result, what,
				 "info#stats#xstats#fdir#stat_qmap#dcb_tc");
cmdline_parse_token_num_t cmd_showport_portnum =
	TOKEN_NUM_INITIALIZER(struct cmd_showport_result, portnum, UINT8);

cmdline_parse_inst_t cmd_showport = {
	.f = cmd_showport_parsed,
	.data = NULL,
	.help_str = "show|clear port info|stats|xstats|fdir|stat_qmap|dcb_tc X (X = port number)",
	.tokens = {
		(void *)&cmd_showport_show,
		(void *)&cmd_showport_port,
		(void *)&cmd_showport_what,
		(void *)&cmd_showport_portnum,
		NULL,
	},
};

  这便是 testpmd 中一个很关键的命令 show port info X 的实现。其中可以发现上面的实现,从上到下可以分为四个部分:

  1. 一个 struct 定义,其中储存着命令行中输入命令的参数。这段源码中即为 struct cmd_showport_result,其中储存着三个 string,一个 int 成员。这四个成员则依次对应 show port info X 这四个部分。
  2. 一个回调函数,其中对命令输入的内容进行处理,命令功能的实现就在这个函数里。从上面的源码里可以看出此函数的第一个传参为输入命令解析后的结果,其类型是第一部分 struct 的结构体指针。
  3. 若干 cmdline_parse_token,与第一部分 struct 定义的内容相关。
  4. 一个被封装好的类型 cmdline_parse_inst_t,一共有四个参数,其中第一个参数为第二部分的函数,第二个部分为空,第三部分为帮助信息 help_str,第四部分为第三部分新建的若干令牌 cmdline_parse_token

  接下来将从第四部分,最关键的 cmdline_parse_inst_t 来分析。

2. 命令类型 cmdline_parse_inst_t

cmdline_parse_inst_t   cmdline_parse_inst_t 的定义在 lib/librte_cmdline/cmdline_parse.h 中,内容如上,可以看到其是将结构体 cmdline_inst 封装后的类型。联系上文内容以及此段代码内的注释,dpdk cmdline 中命令的类型已经大致清楚。

  在 dpdk cmdline 中,每一个 cmdline_parse_inst_t 都代表一个命令,其包含有四个成员。第一个成员为一个回调函数,内容为此条命令需要执行的内容。第三个成员为帮助信息 help_str,在按tab或是help的时候,则会补全或是弹出相关信息。第四个成员即为一个令牌数组,其中储存着命令中的参数,这块内容接下来会进行分析。  

3. 令牌类型 cmdline_parse_token_hdr_t

cmdline_parse_token_hdr_t   cmdline_token_hdr_t 的定义也在 lib/librte_cmdline/cmdline_parse.h 中,内容如上。

  cmdline_token_hdr_t 是对结构体 cmdline_token_hdr 封装后的类型,其中共有两个成员。其中一个是结构体 cmdline_token_ops 的指针,另一个是 uint 类型的偏移量。通过注释可以得知,此处的偏移量为解析结果存入结构的位置,但是 ops 结构体还是不知道是什么东西。

4. 结构体 cmdline_token_ops

ops   cmdline_token_ops 的定义仍在 lib/librte_cmdline/cmdline_parse.h 中,内容如上。

  ops 结构体里含义四个回调函数,是比较复杂的...粗略来看,需要自定义一些回调函数来实现不同种类令牌的解析与补全等操作。这四个回调函数的具体内容这里不再赘述,预计会单独开一篇记录来分析 cmdline_token_ops 结构体内的回调函数的功能、流程和思路。

  从这里以及前文可以得知一个东西,那就是根据令牌类型的不同,某些回调函数的实现也存在差异。比如说最开始摘出的代码,其对 uint8 类型令牌的定义流程如下:

struct cmd_showport_result {
	...
	uint8_t portnum;
};
...
cmdline_parse_token_num_t cmd_showport_portnum =
	TOKEN_NUM_INITIALIZER(struct cmd_showport_result, portnum, UINT8);
...
cmdline_parse_inst_t cmd_showport = {
	...
	.tokens = {
		...
		(void *)&cmd_showport_portnum,
		NULL,
	},
};

  可以看到,在 uint8 令牌的创建流程中,令牌的类型是 cmdline_parse_token_num_t,而不是 cmdline_parse_token_hdr_t,且在构造中使用了宏函数 TOKEN_NUM_INITIALIZER。在向命令结构体 cmd_showport 中传递上面生成好的令牌时,使用 (void*) 进行强制转换来避免类型上的不匹配。这样可以保证多种类型共用一个 cmdline_parse_token_hdr_t 接口,我感觉这有点C语言多态的感觉。   目前来看,cmdline 中的令牌类型一共有以下四种,接下来将来简单看一下这四种类型的实现方法。

  • 字符串令牌:匹配静态字符串,静态字符串列表或任何字符串。
  • 数字令牌:匹配一个可以签名或无符号的数字,从8位到32位。
  • IP地址令牌:匹配IPv4或IPv6地址或网络。
  • 以太网*地址令牌:匹配MAC地址。

5. 子令牌类型 以 cmdline_parse_token_num_t 为例

相关路径为:lib/librte_cmdline/cmdline_parse_num.h num   可以看到在num类型的令牌结构体中,其中包含有两个成员,第一个为一个 cmdline_token_hdr 结构体(即总令牌结构体,其中包含一个ops结构体和一个偏移量),第二个则是一个 cmdline_token_num_data 结构体,其定义如下。 结构体   cmdline_token_num_data 结构体中存储着一个枚举类型 type,即为num的具体类型,可以看到支持8到64位的无符号/有符号整型。 定义   最上面的样例代码中,一个num类型的令牌是按如上的步骤来生成的,此令牌的类型即为 cmdline_parse_token_num_t,然后等号后是一个宏函数,传入参数为命令结果结构体、该令牌对应的结构体中的参数以及该令牌中的 numtype。其中宏函数的定义如下: 宏   其中hdr相关内容中,ops结构体是已经定义好的 cmdline_token_num_ops,这是依据num类型令牌定制的ops结构体。下面就是offset偏移量,其为一个宏函数,定义为 #define offsetof(type, field) ((size_t) &( ((type *)0)->field) ),通过这个宏函数,即可计算出指定成员在其所属结构体内的位置,实现的非常优雅可以说。然后最后则是一个numtype,储存具体类型。

  等于说子令牌类型,相比于令牌类型 cmdline_parse_token_hdr_t 来讲,在hdr结构体的基础上,增加了一个type。而在传的时候,用一个 (void*) 来进行强制转换,个人感觉算是一种C风格的向上转型吧。

总结:每个子令牌类型都有其独特的ops结构体,储存在hdr结构体中;hdr结构体中还有一个offset,来储存指定成员的位置,从而可以把处理结果放进成员中。另外还存在一个type来储存类型的具体内容,从而配合ops中定制的回调函数。

二、新增命令流程总结

1. 新建命令结构体

  如下新建命令结构体 test,定义其回调函数为 fun_test,其 token 令牌仅一个,为 cmd_test

cmdline_parse_inst_t test = {
	.f = fun_test,
	.data = NULL,
	.help_str = "这里是帮助信息", 
	.tokens = {
		(void *)&cmd_test,
		NULL,
	},
};

2. 新建命令参数结构体

  如下新建命令参数结构体 test_result,其中只含一个 string 类型的成员 cmd

struct test_result {
	cmdline_fixed_string_t cmd;
};

3. 生成相关令牌

  如下生成 string 类型令牌 cmd_test,其对应的命令为 test。

cmdline_parse_token_string_t cmd_test =
	TOKEN_STRING_INITIALIZER(struct test_result, cmd, "test");

4. 编写回调函数

  如下编写回调函数 fun_test,规定当输入命令 test 后的操作。

static void fun_test(void *parsed_result,
				__attribute__((unused)) struct cmdline *cl,
				__attribute__((unused)) void *data)
{
	struct test_result *res = parsed_result;
	...若干实现
}

  由此,一个新的命令就添加完毕了。这就是添加命令的大致流程,只不过例子中的参数较少,所以比较简略,而且省去了最难的回调函数的编写过程。  

三、总结

  三个月前第一次看dpdk的源码时,可以说是看的非常艰难,其中各式各样的回调函数,以及封装了一层又一层的类型可以说是最大的阻碍。而且其中还包含不少关于硬件的知识点,我当时可以说是一窍不通。但是三个月的实习中,我逐渐是了解一些基础的内容,可以说是逐渐理解了编写的思路和一些知识点。   dpdk的源码中,使用C实现了不少类似于多态、接口的东西,从中我学习到了很多C的写法,可以说令我叹为观止。目前我准备先研究一下cmdline的实现,看一下dpdk中是如何解析和传递命令的,剩下的(大部分)内容应该会简单了解,对有兴趣的点再尝试深入一点分析。

猜你喜欢

转载自juejin.im/post/7118018490400768031