关于NCSI我们可以参考标准介绍DMTF链接: https://www.dmtf.org/sites/default/files/standards/documents/DSP0222_1.1.0.pdf https://www.dmtf.org/sites/default/files/standards/documents/DSP0222_1.2.0b.pdf
也可参考以下文章IBM曾经的文章,现在已经失传了。
王俊元, 软件工程师, IBM 王寒芷, 软件工程师, IBM
: NCSI(Network Controller Sideband Interface)分布式管理任务组(Distributed Management Task Force, DMTF)由一个管理控制器和多个网络控制器组成理控制器和多个网络控制器组成。DMTF 为 NCSI 此外,还定义了基于以太网的完整控制命令请求和响应标准,NCSI 还有单线程、超时重传等机制。本文结合NCSI简单演示标准 Linux 在网络协议栈注册,初始化 NCSI 并使用协议 socket 接口完整 NSCI 操作过程。
引言
随着网络技术的快速发展,人们对服务器的外部管理和控制提出了更高的要求。通过外部管理,工程师可以通过网络连接到相应的服务器,进行一系列的管理和维护,而不需要在嘈杂的实验室环境中停留很长时间。NCSI(Network Controller Sideband Interface)分布式管理任务组(Distributed Management Task Force, DMTF)边带接口网络控制器的工业标准用于支持服务器带外管理。
NCSI 的简单介绍
NCSI 的简单介绍
一般来说,服务器的网络管理模块结构如图所示 1 所示:
它主要包括:管理控制器(Management Controller, MC),一个或多个(NCSI 最支持的电气特性 4 网络控制器(Network Controller, NC)。一方面,网络控制器连接外部网络接口和内部主机接口,另一方面,它与管理控制器有外部接口。
带外接口的网络数据包主要分为两类:一类是外部网络与管理控制器之间传输的普通数据包,网络控制器只转发;另一类是管理控制器与网络控制器之间传输的带控制信息的数据包。它们通常是管理控制器对网络控制器配置的一些修改。网络控制器需要对此类数据包做出相应的响应。管理控制器与网络控制器之间带外接口的电气性能和通信协议由 NCSI 来定义的。
此外,NCSI 还提供了相应的传输协议栈,以确保这种带外管理。NCSI 如图所示 2 所示:
从图 2 可见:
- 在 NCSI 物理层位于传输协议栈的底层,NCSI 基于物理层 RMII 接口的;
- 数据链路层位于物理层以上,NCSI基于以太介质的数据链路层;
- NCSI 有两种协议支持位于数据链路层以上种是管理控制器和网络控制器之间的交互 NCSI另一种是管理控制器与外部网络通信的网络数据协议,如:ARP 协议、DHCP 协议、NetBIOS协议等,它们只在管理控制器和外部网络之间被网络控制器转发。本文重点讨论了第一个协议,即控制命令协议。
NCSI 控制命令协议
NCSI 控制命令允许管理控制器初始化和规范自己 NCSI 接口流量,配置 NCSI 监控网络控制器的信道过滤规则和运行状态。 NCSI 管理控制器是所有控制命令的发起者,而网络控制器响应管理控制器发送的控制命令。
NCSI 实例号(IID)
所有通过 NCSI 命令发送的 NCSI 所有请求都有唯一的标志符 Instance ID(IID)。IID 是一个 8 比特长的标识序列。每个 NCSI 响应包的 IID 都等于对应 NCSI 请求包的 IID。通过使用 IID,能有效识别新的 NCSI 请求,提高 NCSI 要求与响应相匹配,并区分新的 NCSI 请求和超时重传 NCSI 请求。
收到网络控制器的标记 IID 的 NCSI 请求时,应遵循以下操作原则:
在响应包的相应位置标记 NCSI 响应对应的 NCSI 请求的 IID。
如果收到的 NCSI 请求的 IID 和之前收到的一样,说明是超时重发。 NCSI 请求。NCSI 标准规定,此时网络控制器必须重发 NCSI 响应请求。根据具体情况,网络控制器可以使用 NCSI 命令,回到前一次 NCSI 如果该命令的执行结果在当前时刻没有失效。或者,网络控制器也可以在当前时刻重新执行 NCSI 命令。
如果收到的 NCSI 请求的 IID 与之前收到的不同,网络控制器必须将此请求视为新请求 NCSI 请求。
对于一个重新初始化的网络控制器,它收到了第一个 NCSI 无论他是否在重新初始化之前收到同样的请求,请求都必须是一个新的请求 IID 的 NCSI 请求。
它正在处理管理控制器 IID 遵循以下原则:
每个新的 NCSI 请求实例必须与以前不同 IID。
如果一个 NCSI 请求需要被重传,则其 IID 必须和以前一样 NCSI 请求的 IID 相同。
由于 NCSI 响应包的 IID 等于对应 NCSI 请求包的 IID,所以 IID 管理控制器可以进一步用于确认特定的 NCSI 请求是否响应标志。
单线程机制
目前,网络控制器只能支持单线程 NCSI 命令。也就是说,网络控制器一次只能执行一个 NCSI 命令。只有当它收到它时。 NCSI 请求发送响应包后,才能继续接受下一个 NCSI 请求。
这种单线程机制使管理控制器向同一网络控制器发送 NCSI 请求只能通过单线程进行。当管理控制器发送到网络控制器时。 NCSI 在收到网络控制器发出的信息之前,它必须保持等待状态 NCSI 直到响应或超时重传。
超时重传机制
如果在 NCSI 在命令超时的时间间隔内,管理控制器尚未收到网络控制器的回复 NCSI 响应,则管理控制器必须以相同的 IID 重发之前的 NCSI 请求。
必须尝试管理控制器 NCSI 请求至少 3 才能报告网络控制器的错误。
在网络环境相对恶劣的情况下,管理控制器可能在收到之前发送 NCSI 在响应请求时,超过了超时重发的时间间隔。在这种情况下,管理控制器将收到同一个 NCSI 请求的两个响应。管理控制器必须有相应的机制来检测第二个 NCSI 回应,丢弃。
异步事件通知包
AEN(Asynchronous Event Notification)该包是网络控制器在某些状态下主动向管理控制器发送的通知数据包,并影响其接口的正常运行。由于 NCSI 命令只是网络控制器处理的众多数据包中的一小部分,网络控制器的一系列事件都会影响到命令的正常执行。这些事件包括:链路状态变化、操作系统驱动加载卸载、芯片复位等。
网络控制器将向管理控制器发送哪些事件? AEN 管理控制器通过使用控制位来定义包(control bit),网络控制器是否启用异步事件通知可以单独定义。
AEN 包是工作在 NCSI 请求 - 一种以外的应答机制 NCSI 包。也就是说,AEN 包既不是 NCSI 请求,不是任意的 NCSI 响应请求。AEN 即使一个包没有答案,即使没有答案 AEN 包在传输过程中丢失,管理控制器无法知道。
一个 AEN 包的 IID 始终为 0x00。
NCSI 包结构
NCSI 的以太网帧头
正如前面所述,NCSI 基于以太介质。 NCSI 包都是封装的 IEEE 802.3 以太网帧格式中定义的,即:任何一个 NCSI 袋子都是封装的 14 以太网头及字节 4 在字节的以太网尾,如图所示 3 所示:
图 3. NCSI 以太帧格式
目的地址字段:
NCSI 以太帧的第 0 到 5 字节分别代表以太网的地址 5 到 0 由于传输,字节 NCSI 命令的信道没有分配特定的信道 MAC 地址,所以 NCSI 以太帧的目的地地址为广播地址(FF:FF:FF:FF:FF:FF)。
如果网络控制器收到目的地址,则不是 FF:FF:FF:FF:FF:FF 的 NCSI 包,则它会直接丢弃该数据包并且返回一个错误响应。
源地址字段:
NCSI 以太帧的第 6 到 11 以太网源地址分别代表字节 5 到 0 字节。如果 NCSI 包来自管理控制器,所以源地址字段可以是任意值。果 NCSI 包来自于网络控制器,则源地址字段必须为 FF:FF:FF:FF:FF:FF。
类型字段:
NCSI以太帧的第 12、13 字节分别代表以太网类型的第 1、0 字节。对于NCSI 控制包而言,其类型字段为 0x88F8。
NCSI 控制包头
每个 NCSI 包(包括 NCSI 请求包、响应包以及 AEN 包)都有一个 16 字节长的控制包头,以大端在先顺序排列,如图 4 所示:
图 4. NCSI 控制包头格式
Management Controller ID(MC ID)字段
MC ID 字段 1 字节长,标识了 NCSI 包对应的管理控制器。在 NCSI 1.0.0a 版本中,该字节规定为 0x00。
网络控制器每收到一个管理控制器发出的 NCSI 请求时都必须将请求包中的 MC ID 字段拷贝到响应包的 MC ID 字段中。
Header Revision 字段
Header Revision 字段 1 字节长,标识了 NCSI 命令包头的版本号。在 NCSI 1.0.0a 版本中,该字节规定为 0x01.
IID 字段
IID 字段 1 字节长。正如上文所述,网络控制器可以用 IID 来辨别当前 NCSI 命令是一个新的请求还是一个超时重传的请求;管理控制器可以用 IID 来确认某个特定的 NCSI 请求是否得到了相应。
Command 字段
Command 字段 1 字节长,用于标识特定的 NCSI 命令请求与响应。每个 NCSI 请求命令都对应着 0x00 到 0x7F 之间唯一的一个命令号,而对该 NCSI 请求包的响应的命令号就是把 NCSI 请求的命令号的最高位置为 1,这样就建立了 128 个 NCSI 请求命令与相应的 128 个 NCSI 响应命令之间的一一对应关系。
Channel ID 字段
Channel ID 字段 1 字节长。每个 NCSI 管理控制器可以对应一个或者多个 package,这些 package 可以属于同一个网络控制器,也可以属于不同的网络控制器。每个 package 的内部又可以定义一个或者多个信道(channel)。所有的 NCSI 数据包都是在某个特定的 channel 中传输的,Channel ID 字段就标识了当前 NCSI 数据包所在的 channel。
Payload Length 字段
Payload Length 字段 12 比特长,标识了紧随 NCSI 包头之后的 NCSI 载荷的长度。
Reserved 字段
图中标有 Reserved 的区域均为保留字段,通常被置为 0。
NCSI 控制包载荷
NCSI 控制包载荷的数据都是按照大端在先的顺序排列的,包括数据、载荷填充、校验和以及以太包填充 4 部分,如图 5 所示:
数据(Data)部分:
NCSI 请求不含 Data 部分。
每个 NCSI 响应都有 2 字节的响应值和 2 字节的原因值。对于某些 NCSI 命令的响应,还有一定长度的附加信息。
载荷填充部分:
如果 NCSI 的 Data 部分长度不是 4 字节的整数倍,那么就需要将其填充为 4 字节的整数倍。所有的填充值均为 0x00.
校验和部分:
校验和部分 4 字节长。管理控制器和网络控制器可以利用校验和部分来对 NCSI 包进行校验,也可以将校验和部分设为全 0,从而表明该 NCSI 包不需要校验。
以太包填充部分:
根据 IEEE 802.3 标准,所有的以太帧长度必须大于 64 字节,也就是说,NCSI 包头和 NCSI 载荷的长度之和必须大于 46 字节。事实上,大部分的 NCSI 包都无法满足这一条件。因此,几乎所有的 NCSI 包的末尾都需要作一定长度的填充。
AEN 包格式
NCSI 的 AEN 包格式如图 6 所示:
图 6. AEN 包格式
发送 AEN 包的网络控制器需要在 NCSI 包头的 Channel ID 字段里标识发生相应事件的 channel 号。
AEN 包的 NCSI 头的 IID 字段始终为 0x00,command 字段始终为 0xFF,Payload Length 字段始终为 0x04,也就是说 AEN 包的 NCSI 载荷长度为 4。
AEN 包的 NCSI 载荷中 3 字节为保留字段,另外一个字节为 AEN 类型字段,对应的含义如表 1 所示:
AEN类型值 含义 0x00 链路状态发生了改变 0x01 需要对网络控制器进行配置 0x02 网络控制器驱动的状态发生了改变 0x03 ~ 0x7F 保留 0x80 ~ 0xFF 各厂商
自定义的 AEN 事件
NCSI 在 Linux 上的简单实现
硬件支持
要实现 NCSI,首先需要选择支持 NCSI 的 NIC,目前大部分网络芯片厂商都有支持 NCSI 的产品,比如 Broadcom 的 BCM57710,Intel 的 Intel82576 等。
在 Linux 网络协议栈中初始化 NCSI 协议族
在 Linux 的网络协议栈中初始化 NCSI 协议族需要了解 Linux 的网络协议栈,这不是本文讨论的重点,本节只是简单介绍在 Linux 网络协议栈的三个层面(Socket 层、Sock 层和 sk_buff 层)上初始化 NCSI 接口。之所以只是介绍这三层,是因为 socket{}、sock{}、sk_buff{} 是 Linux 网络协议栈中最重要的数据结构,也是数据流的连接通道。比如,发送报文时,数据会由 socket{} 通过相应的 proto_ops{} 把数据传给 sock{},sock{} 又通过 proto{} 把数据传到 sk_buff;反过来,当收到报文时,sk_buff{} 通过 net_protocol{} 把数据传给 sock{},后者又通过 proto{} 把数据传给 socket{},socket{} 最后把数据传给用户层,有关这方面具体详细细节在这里不做过多赘述。
Socket 层,注册 net_proto_family 结构体
清单 1. 注册 net_proto_family 结构体到全局数组 net_families 中
#define PF_NCSI 27 /* NCSI Address Family */
static struct net_proto_family ncsi_family_ops = {
.family = PF_NCSI,
.create = ncsi_create,
.owner = THIS_MODULE,
};
sock_register(&ncsi_family_ops);
Sock 层,填充 proto_ops 结构体,该结构体的成员变量是一些函数指针,分别对应了 NCSI 操作的函数。
清单 2. 填充 proto_ops 结构体
static struct proto_ops ncsi_ops = {
.family = PF_NCSI,
.owner = THIS_MODULE,
.release = ncsi_release,
.bind = sock_no_bind,
.connect = sock_no_connect,
.socketpair = sock_no_socketpair,
.accept = sock_no_accept,
.getname = sock_no_getname,
.poll = sock_no_poll,
.ioctl = sock_no_ioctl,
.listen = sock_no_listen,
.shutdown = sock_no_shutdown,
.setsockopt = sock_no_setsockopt,
.getsockopt = sock_no_getsockopt,
.sendmsg = ncsi_sendmsg,
.recvmsg = ncsi_recvmsg,
.mmap = sock_no_mmap,
.sendpage = sock_no_sendpage,
};
sk_buff 层,实现从 NIC 接收 NCSI packet 的功能,其中 ncsi_rcv 是从 NIC 中接收 RAW packet 的接口。
清单 3. 实现从 NIC 接收 NCSI packet 的功能
#define NCSI_PROTOCOL 0x88F8
static struct packet_type ncsi_packet_type = {
.type = __constant_htons(NCSI_PROTOCOL),
.func = ncsi_rcv,
};
dev_add_pack(&ncsi_packet_type);
利用套接字 socket 实现 NCSI 的操作
在 Linux 的网络协议栈中初始化了 NCSI socket 的三元组:< 地址族,类型,具体协议 >,也正好是调用 socket 系统函数的 3 个参数。内核中这 3 个数据结构,就可以创建 sock{} 结构。
首先,在利用套接字实现 NCSI 操作之前,为了提高程序的通用性和易读性、减少不一致性,需要对 NCSI 协议族进行宏定义:
清单 4. 对 NCSI 协议族进行宏定义
#define AF_NCSI 27 /* NCSI 地址族定义 */
其次,为了便于对 NCSI 包头进行整体性操作,这里根据前文所述的 NCSI 包格式,定义了 NCSI 的包头结构 ncsihdr:
清单 5. NCSI 包头结构的定义
/* NCSI 包头结构的定义 */
struct ncsihdr {
unsigned char mc_id;
unsigned char hdr_rev;
unsigned char reserved0;
unsigned char cmd_iid;
unsigned char cmd;
unsigned char chnl_id;
unsigned short payload_len;
unsigned int reserved2;
unsigned int reserved3;
};
从根本上说,NCSI 主要包括 4 种原子操作:创建 NCSI socket、关闭 NCSI socket、向已创建的 NCSI socket 发送 NCSI 命令以及从已创建的 NCSI socket 获得 NCSI 响应。一个最简单的 NCSI 命令操作流程如图 7 所示:
其他任意操作都可以通过使用这 4 种原子操作的组合来实现。为简单起见,本文仅对这 4 中最基本的 NCSI 操作给出了在 linux 下的相应实现,而对于由这 4 种操作引申而出的其它复杂操作就不做过多的赘述了。 (1) 创建 NCSI socket:
清单 6. 创建 NCSI socket
int OpenNCSISocket ()
{
// 调用 socket 函数创建一个能够进行网络通信的套接字:协议族为 AF_NCSI,套接字类型为 SOCK_RAW
sd = socket(AF_NCSI, SOCK_RAW, 0);
if (sd < 0)
{
cout << "NCSI socket creation failed" << endl;
return -1;
}
return sd;
}
(2) 关闭 NCSI socket:
清单 7. 关闭 NCSI socket
int CloseNCSISocket(int sd)
{
close(sd); // 调用 close 函数关闭已创建的套接字
return 0;
}
(3) 向已创建的 NCSI socket 发送 NCSI 命令:
清单 8. 向已创建的 NCSI socket 发送 NCSI 命令
int SendRawNCSIPacket( int sd, // 已创建的 NCSI socket
int Port_Num, //NCSI 命令发送到的以太网接口号
unsigned char cmd, //NCSI 命令号
unsigned char channel_id, // 承载 NCSI 命令的信道号
unsigned char *pu8Data, // 指向 NCSI 载荷的指针
unsigned short data_length//NCSI 载荷的长度
)
{
int status;
struct ncsihdr *ncsih; //NCSI 包头
struct iovec xmit_iovec[2]; // 创建 linux I/O 向量 iovec 结构数组
struct msghdr xmit_message; // 创建 linux 信息头 msghdr 结构数组
unsigned char pkt_buffer[60]; // 发送缓冲区
unsigned char *ncsi_payload ; //NCSI 载荷指针
// 封装以太网帧头
memset(pkt_buffer, 0, 60);
memset(pkt_buffer, 0xff, 12); // 目的地址和源地址均为 0xFF FF FF FF FF FF
pkt_buffer[12] = 0x88;
pkt_buffer[13] = 0xf8; // 以太网帧头类型字段为 0x88f8,表示 NCSI 命令
// 封装 NCSI 包头
ncsih = (struct ncsihdr *) (pkt_buffer+14);
ncsih->mc_id = 0x0; // 设置管理控制器 ID 为 0x0
ncsih->hdr_rev = 0x1; // 设置 NCSI 头版本号围 0x1
ncsih->cmd_iid = inciid(); // inciid() 是一个函数,用于获得当前发送数据包的 instance id
ncsih->cmd = cmd; // 设置 NCSI 命令的命令号
ncsih->chnl_id = channel_id; // 设置 NCSI 命令传输的信道号
// 设置 NCSI 载荷的长度。由于网络字节序使用的都是大端模式,所以要调用 htons 进行相应的转换
ncsih->payload_len = htons(data_length);
// 封装 NCSI 载荷
ncsi_payload = (unsigned char *)(pkt_buffer + 14 + sizeof(struct ncsihdr));
if ( (pu8Data!=NULL) && (data_length!=0) )
{
memcpy(ncsi_payload, pu8Data, data_length);
}
// 封装 xmit_message
xmit_message.msg_name = NULL;
xmit_message.msg_namelen = 0;
xmit_message.msg_iov = xmit_iovec;
xmit_message.msg_iov[0].iov_base = (void *)&Port_Num;
xmit_message.msg_iov[0].iov_len = sizeof(int);
xmit_message.msg_iov[1].iov_base = (void *)pkt_buffer;
xmit_message.msg_iov[1].iov_len = 60;
xmit_message.msg_iovlen = 2;
xmit_message.msg_control = NULL;
xmit_message.msg_controllen = 0;
xmit_message.msg_flags = 0;
// 调用 linux 的 sendmsg 发送 NCSI 请求
status = sendmsg(sd, &xmit_message, 0);
return status;
}
(4) 从已创建的 NCSI socket 获得 NCSI 响应:
清单 9. 从已创建的 NCSI socket 获得 NCSI 响应
int RecvRawNCSIPacket( int sd, // 已创建的 NCSI socket
unsigned char *pu8NCSIpacket, // 用于存放接收数据的缓冲区
int buf_size, // 接收缓冲区的大小
int *Port_Num, // 接收 NCSI 响应的以太网接口号
int wait_time // 等待时间,单位毫秒
)
{
int status;
struct msghdr recv_message; // 创建 linux 信息头 msghdr 结构数组
struct iovec recv_iovec[3]; // 创建 linux I/O 向量 iovec 结构数组
if ( (NULL == pu8NCSIpacket) || (0 == buf_size )
return -1;
// 封装 recv_message
recv_message.msg_name = NULL;
recv_message.msg_namelen = 0;
recv_message.msg_iov = recv_iovec;
recv_message.msg_iov[0].iov_base = (void *)&wait_time;
recv_message.msg_iov[0].iov_len = sizeof(int);
recv_message.msg_iov[1].iov_base = (void *) pu8NCSIpacket;
recv_message.msg_iov[1].iov_len = buf_size;
recv_message.msg_iov[2].iov_base = (void *)Port_Num;
recv_message.msg_iov[2].iov_len = sizeof (int *);
recv_message.msg_iovlen = 3;
recv_message.msg_control = NULL;
recv_message.msg_controllen = 0;
recv_message.msg_flags = 0;
// 调用 linux 的 recvmsg 接收 NCSI 响应
status = recvmsg(sd, &recv_message, 0);
return status;
}
总结
本文结合 DMTF 的 NCSI 标准,对 NCSI 的控制命令协议以及包结构作了详细的描述,并在此基础之上,进一步简单演示了在 Linux 网络协议栈中注册、初始化 NCSI 协议,并且利用 socket 接口实现一个完整的 NSCI 操作的过程,希望能够为使用 NCSI 标准进行带外管理的网络管理人员及 linux 网络编程爱好者提供有益的参考。