前面分析了open-iscsi主要有一个守护进程iscsid,即通过本地socket和用户其他进程通信,比如iscsiadm管理执行任务时,需要和iscsid通信,iscsi的底层驱动则通过netlink和iscsid通信,而用户态其他进程还通过共享内存和iscsid日志守护进程通信。所以在iscsi服务启动后,我们会发现在系统中有两个名为iscsid的进程,其中一个为日志守护进程,另一个则是控制守护进程。
今天,我们接着分析iscsid和iscsiadm通过本地socket的进程间通信,在iscsid的main函数中:
if ((mgmt_ipc_fd = mgmt_ipc_listen()) < 0) {
log_close(log_pid);
exit(ISCSI_ERR);
}
建立了一个类似于服务器端的监听进程,在函数:
event_loop(ipc, control_fd, mgmt_ipc_fd);
中,通过poll等待进程通信事件的发生:
if (poll_array[POLL_IPC].revents) /*其他进程通信*/
mgmt_ipc_handle(mgmt_ipc_fd);
若有通信产生,则通过mgmt_ipc_handle函数进行处理,而mgmt_ipc_handle和其他进程间通信函数都在mgmt_ipc.c文件中:
/*进程间通信入口函数*/
void mgmt_ipc_handle(int accept_fd)
{
unsigned int command;
int fd, err;
/*任务结构体,包含请求任务和响应的内容*/
queue_task_t *qtask = NULL;
/*不同任务的处理函数指针*/
mgmt_ipc_fn_t *handler = NULL;
/*返回请求进程的用户名*/
char user[PEERUSER_MAX];
/*分配一个任务结构体*/
qtask = calloc(1, sizeof(queue_task_t));
if (!qtask)
return;
/*返回一个新的socket描述符*/
if ((fd = accept(accept_fd, NULL, NULL)) < 0) {
free(qtask);
return;
}
qtask->allocated = 1; /*标志这个结构是动态分配的*/
qtask->mgmt_ipc_fd = fd; /*这个任务通信的socket*/
/*对请求进程的用户进行认证*/
if (!mgmt_peeruser(fd, user) || strncmp(user, "root", PEERUSER_MAX)) {
err = ISCSI_ERR_ACCESS;
goto err;
}
/*读取任务请求信息*/
if (mgmt_ipc_read_req(qtask) < 0) {
mgmt_ipc_destroy_queue_task(qtask);
return;
}
/*可以得到请求服务的编号了*/
command = qtask->req.command;
qtask->rsp.command = command;
/*任务id合法就调用对应的处理函数进行处理*/
if (0 <= command && command < __MGMT_IPC_MAX_COMMAND)
handler = mgmt_ipc_functions[command];
if (handler != NULL) {
/* If the handler returns OK, this means it
* already sent the reply. */
err = handler(qtask);
if (err == ISCSI_SUCCESS)
return;
} else {
log_error("unknown request: %s(%d) %u",
__FUNCTION__, __LINE__, command);
err = ISCSI_ERR_INVALID_MGMT_REQ;
}
err:
/* This will send the response, close the
* connection and free the qtask */
/*出错后将结果返回给请求进程*/
mgmt_ipc_write_rsp(qtask, err);
}
其中,贯穿整个处理过程的是一个queue_task_t结构体,这个结构体在open-iscsi中扮演着重要的角色,也比较复杂:
/*任务结构体*/
typedef struct queue_task {
iscsi_conn_t *conn; /*iscsi会话信息*/
iscsiadm_req_t req; /*请求信息*/
iscsiadm_rsp_t rsp; /*响应信息*/
int mgmt_ipc_fd; /*socket 文件描述符*/
int allocated : 1; /*表示这个结构体是否是动态分配的,便于检查释放内存*/
/* Newer request types include a
* variable-length payload */
void *payload; /*自定义信息*/
} queue_task_t;
这个结构体的重要组成部分就是请求和响应两个结构,这两个结构根据请求任务的不同,结构也不相同,所以内部有一个union结构:
/* IPC Request 信息*/
typedef struct iscsiadm_req {
iscsiadm_cmd_e command; /*请求任务标号*/
uint32_t payload_len; /*queue_task结构体中payload的长度*/
union {
/* messages */
struct ipc_msg_session {
int sid;
node_rec_t rec;
} session;
struct ipc_msg_conn {
int sid;
int cid;
} conn;
struct ipc_msg_send_targets {
int host_no;
int do_login;
struct sockaddr_storage ss;
} st;
struct ipc_msg_set_host_param {
int host_no;
int param;
/* TODO: make this variable len to support */
char value[IFNAMSIZ + 1];
} set_host_param;
} u;
} iscsiadm_req_t;
/* IPC Response 信息*/
typedef struct iscsiadm_rsp {
iscsiadm_cmd_e command;
int err; /* ISCSI_ERR value */
union {
#define MGMT_IPC_GETSTATS_BUF_MAX (sizeof(struct iscsi_uevent) + \
sizeof(struct iscsi_stats) + \
sizeof(struct iscsi_stats_custom) * \
ISCSI_STATS_CUSTOM_MAX)
struct ipc_msg_getstats {
struct iscsi_uevent ev;
struct iscsi_stats stats;
char custom[sizeof(struct iscsi_stats_custom) *
ISCSI_STATS_CUSTOM_MAX];
} getstats;
struct ipc_msg_config {
char var[VALUE_MAXLEN];
} config;
struct ipc_msg_session_state {
int session_state;
int conn_state;
} session_state;
} u;
} iscsiadm_rsp_t;
接着分析函数中的流程:
if (!mgmt_peeruser(fd, user) || strncmp(user, "root", PEERUSER_MAX))
主要对对端进程的用户进行认证:
/*获取对端通信进程的用户名*/
static int
mgmt_peeruser(int sock, char *user)
{
/*linux支持认证的方式*/
#if defined(SO_PEERCRED)
/* Linux style: use getsockopt(SO_PEERCRED) */
struct ucred peercred;
socklen_t so_len = sizeof(peercred);
struct passwd *pass;
errno = 0;
/*获取认证信息*/
if (getsockopt(sock, SOL_SOCKET, SO_PEERCRED, &peercred,
&so_len) != 0 || so_len != sizeof(peercred)) {
/* We didn't get a valid credentials struct. */
log_error("peeruser_unux: error receiving credentials: %m");
return 0;
}
/*根据用户id返回用户名*/
pass = getpwuid(peercred.uid);
if (pass == NULL) {
log_error("peeruser_unix: unknown local user with uid %d",
(int) peercred.uid);
return 0;
}
strlcpy(user, pass->pw_name, PEERUSER_MAX);
return 1;
#elif defined(SCM_CREDS)
struct msghdr msg;
typedef struct cmsgcred Cred;
#define cruid cmcred_uid
Cred *cred;
/* Compute size without padding */
/* for NetBSD */
char cmsgmem[_ALIGN(sizeof(struct cmsghdr)) + _ALIGN(sizeof(Cred))];
/* Point to start of first structure */
struct cmsghdr *cmsg = (struct cmsghdr *) cmsgmem;
struct iovec iov;
char buf;
struct passwd *pw;
memset(&msg, 0, sizeof(msg));
msg.msg_iov = &iov;
msg.msg_iovlen = 1;
msg.msg_control = (char *) cmsg;
msg.msg_controllen = sizeof(cmsgmem);
memset(cmsg, 0, sizeof(cmsgmem));
/*
* The one character which is received here is not meaningful; its
* purposes is only to make sure that recvmsg() blocks long enough for
* the other side to send its credentials.
*/
iov.iov_base = &buf;
iov.iov_len = 1;
if (recvmsg(sock, &msg, 0) < 0 || cmsg->cmsg_len < sizeof(cmsgmem) ||
cmsg->cmsg_type != SCM_CREDS) {
log_error("ident_unix: error receiving credentials: %m");
return 0;
}
cred = (Cred *) CMSG_DATA(cmsg);
pw = getpwuid(cred->cruid);
if (pw == NULL) {
log_error("ident_unix: unknown local user with uid %d",
(int) cred->cruid);
return 0;
}
strlcpy(user, pw->pw_name, PEERUSER_MAX);
return 1;
#else
log_error("'mgmg_ipc' auth is not supported on local connections "
"on this platform");
return 0;
#endif
}
这个函数中主要是通过socket获取对端进程的身份信息,判断是否是root用户。
iscsid进程获取请求任务信息,并填充请求结构体:
/*获取任务请求的信息,填充task结构体*/
static int
mgmt_ipc_read_req(queue_task_t *qtask)
{
iscsiadm_req_t *req = &qtask->req; /*请求结构*/
int rc;
/*读取数据,填到请求结构中*/
rc = mgmt_ipc_read_data(qtask->mgmt_ipc_fd, req, sizeof(*req));
if (rc >= 0 && req->payload_len > 0) {
/* Limit what we accept */
if (req->payload_len > EXTMSG_MAX)
return -EIO;
/* Remember the allocated pointer in the
* qtask - it will be freed by write_rsp.
* Note: we allocate one byte in excess
* so we can append a NUL byte. */
/*读取payload数据*/
qtask->payload = malloc(req->payload_len + 1);
rc = mgmt_ipc_read_data(qtask->mgmt_ipc_fd,
qtask->payload,
req->payload_len);
}
return rc;
}
其中
/*从socket读取数据*/
static int
mgmt_ipc_read_data(int fd, void *ptr, size_t len)
{
int n;
while (len) {
n = read(fd, ptr, len);
if (n < 0) {
if (errno == EINTR)
continue;
return -EIO;
}
if (n == 0) {
/* Client closed connection */
return -EIO;
}
/*持续读取数据*/
ptr += n;
len -= n;
}
return 0;
}
这样,就获取了对端进程请求的是什么服务,和传递的一些参数信息,下面就需要iscsid进行响应的处理,这里的处理函数从一个数组中取,根据请求任务的不同,选取不同的处理函数:
/*可以得到请求服务的编号了*/
command = qtask->req.command;
qtask->rsp.command = command;
/*任务id合法就调用对应的处理函数进行处理*/
if (0 <= command && command < __MGMT_IPC_MAX_COMMAND)
handler = mgmt_ipc_functions[command];
mgmt_ipc_functions的原型为:
typedef int mgmt_ipc_fn_t(struct queue_task *);
/*不同任务id对应的处理函数地址*/
static mgmt_ipc_fn_t * mgmt_ipc_functions[__MGMT_IPC_MAX_COMMAND] = {
[MGMT_IPC_SESSION_LOGIN] = mgmt_ipc_session_login, /*和iscsi target建立连接*/
[MGMT_IPC_SESSION_LOGOUT] = mgmt_ipc_session_logout, /*与target断开会话*/
[MGMT_IPC_SESSION_SYNC] = mgmt_ipc_session_sync, /*同步会话*/
[MGMT_IPC_SESSION_STATS] = mgmt_ipc_session_getstats, /*查询会话状态*/
[MGMT_IPC_SEND_TARGETS] = mgmt_ipc_send_targets, /*发送targets*/
[MGMT_IPC_SESSION_INFO] = mgmt_ipc_session_info, /*查询会话信息*/
[MGMT_IPC_CONN_ADD] = mgmt_ipc_conn_add, /*增加连接*/
[MGMT_IPC_CONN_REMOVE] = mgmt_ipc_conn_remove, /*删除连接*/
[MGMT_IPC_CONFIG_INAME] = mgmt_ipc_cfg_initiatorname, /*iscsi 配置,获取initiatorname*/
[MGMT_IPC_CONFIG_IALIAS] = mgmt_ipc_cfg_initiatoralias, /*获取initiator 别名*/
[MGMT_IPC_CONFIG_FILE] = mgmt_ipc_cfg_filename, /*获取配置文件路径*/
[MGMT_IPC_IMMEDIATE_STOP] = mgmt_ipc_immediate_stop,/*停止*/
[MGMT_IPC_NOTIFY_ADD_NODE] = mgmt_ipc_notify_add_node, /*增加一个node*/
[MGMT_IPC_NOTIFY_DEL_NODE] = mgmt_ipc_notify_del_node, /*删除一个node*/
[MGMT_IPC_NOTIFY_ADD_PORTAL] = mgmt_ipc_notify_add_portal, /*增加端口*/
[MGMT_IPC_NOTIFY_DEL_PORTAL] = mgmt_ipc_notify_del_portal, /*删除端口*/
};
typedef enum iscsiadm_cmd {
MGMT_IPC_UNKNOWN = 0,
MGMT_IPC_SESSION_LOGIN = 1,
MGMT_IPC_SESSION_LOGOUT = 2,
MGMT_IPC_SESSION_ACTIVESTAT = 4,
MGMT_IPC_CONN_ADD = 5,
MGMT_IPC_CONN_REMOVE = 6,
MGMT_IPC_SESSION_STATS = 7,
MGMT_IPC_CONFIG_INAME = 8,
MGMT_IPC_CONFIG_IALIAS = 9,
MGMT_IPC_CONFIG_FILE = 10,
MGMT_IPC_IMMEDIATE_STOP = 11,
MGMT_IPC_SESSION_SYNC = 12,
MGMT_IPC_SESSION_INFO = 13,
MGMT_IPC_ISNS_DEV_ATTR_QUERY = 14,
MGMT_IPC_SEND_TARGETS = 15,
MGMT_IPC_NOTIFY_ADD_NODE = 16,
MGMT_IPC_NOTIFY_DEL_NODE = 17,
MGMT_IPC_NOTIFY_ADD_PORTAL = 18,
MGMT_IPC_NOTIFY_DEL_PORTAL = 19,
__MGMT_IPC_MAX_COMMAND
} iscsiadm_cmd_e;
相应的处理函数处理完成之后,会将结果发送给请求的进程:
/*将请求结果返回给对端进程*/
void
mgmt_ipc_write_rsp(queue_task_t *qtask, int err)
{
if (!qtask)
return;
log_debug(4, "%s: rsp to fd %d", __FUNCTION__,
qtask->mgmt_ipc_fd);
if (qtask->mgmt_ipc_fd < 0) { /*socket 描述符不存在*/
mgmt_ipc_destroy_queue_task(qtask);
return;
}
qtask->rsp.err = err;
/*发出数据*/
if (write(qtask->mgmt_ipc_fd, &qtask->rsp, sizeof(qtask->rsp)) < 0)
log_error("IPC qtask write failed: %s", strerror(errno));
/*关闭socket*/
close(qtask->mgmt_ipc_fd);
/*销毁task结构*/
mgmt_ipc_destroy_queue_task(qtask);
}
同时销毁分配的task结构体:
/*销毁task结构体*/
static void
mgmt_ipc_destroy_queue_task(queue_task_t *qtask)
{
if (qtask->mgmt_ipc_fd >= 0) /*关闭socket*/
close(qtask->mgmt_ipc_fd);
if (qtask->payload) /*释放payload内存*/
free(qtask->payload);
if (qtask->allocated) /*释放task结构内存*/
free(qtask);
}
这里,我们可以看到qtask->allocated标记十分巧妙,可以根据这个结构是否是动态分配的进行动态释放。
上面就把iscsid服务端的主要通信过程分析完了,下面我们分析一个其他进程又是怎样和iscsid进行发起请求的。
/*iscsi请求进程执行任务请求*/
int iscsid_exec_req(iscsiadm_req_t *req, iscsiadm_rsp_t *rsp, int start_iscsid)
{
int fd;
int err;
/*发送请求,返回建立的socket描述符*/
err = iscsid_request(&fd, req, start_iscsid);
if (err)
return err;
/*从socket获取返回结果*/
return iscsid_response(fd, req->command, rsp);
}
发起进程,将请求任务id和一些信息填充到req结构中后,调用这个函数,结果将返回到rsp结构中,其中start_iscsid是标记若iscsid守护进程未启动是否需要启动。
/*执行请求*/
int iscsid_request(int *fd, iscsiadm_req_t *req, int start_iscsid)
{
int err;
/*建立连接*/
err = iscsid_connect(fd, start_iscsid);
if (err)
return err;
/*发送请求信息*/
if ((err = write(*fd, req, sizeof(*req))) != sizeof(*req)) {
log_error("got write error (%d/%d) on cmd %d, daemon died?",
err, errno, req->command);
close(*fd);
return ISCSI_ERR_ISCSID_COMM_ERR;
}
return ISCSI_SUCCESS;
}
执行请求的函数主要是建立和iscsid的通信,然后将req结构的内容发送出去:
/*向守护进程请求连接*/
static int iscsid_connect(int *fd, int start_iscsid)
{
int nsec;
struct sockaddr_un addr;
/*建立socket*/
*fd = socket(AF_LOCAL, SOCK_STREAM, 0);
if (*fd < 0) {
log_error("can not create IPC socket (%d)!", errno);
return ISCSI_ERR_ISCSID_NOTCONN;
}
/*填充地址*/
memset(&addr, 0, sizeof(addr));
addr.sun_family = AF_LOCAL;
memcpy((char *) &addr.sun_path + 1, ISCSIADM_NAMESPACE,
strlen(ISCSIADM_NAMESPACE));
/*
* Trying to connect with exponential backoff
*/
/*递增等待连接,注意nsec<<=1,是每次等待时间翻倍*/
for (nsec = 1; nsec <= MAXSLEEP; nsec <<= 1) {
if (connect(*fd, (struct sockaddr *) &addr, sizeof(addr)) == 0)
/* Connection established */
return ISCSI_SUCCESS;
/* If iscsid isn't there, there's no sense
* in retrying. */
/*若第一次被拒绝连接,可能是iscsid守护进程未启动,则根据start_iscsid判断是否启动iscsid*/
if (errno == ECONNREFUSED) {
if (start_iscsid && nsec == 1)
iscsid_startup();
else
break;
}
/*
* Delay before trying again
*/
if (nsec <= MAXSLEEP/2)
sleep(nsec);
}
log_error("can not connect to iSCSI daemon (%d)!", errno);
return ISCSI_ERR_ISCSID_NOTCONN;
}
这里的超时等待时间设计很好。
这里有个启动iscsid守护进程的小插曲:
/*启动iscsid守护进程*/
static void iscsid_startup(void)
{
char *startup_cmd;
/*从配置文件,获取启动命令*/
startup_cmd = cfg_get_string_param(CONFIG_FILE, "iscsid.startup");
if (!startup_cmd) {
log_error("iscsid is not running. Could not start it up "
"automatically using the startup command in the "
"/etc/iscsi/iscsid.conf iscsid.startup setting. "
"Please check that the file exists or that your "
"init scripts have started iscsid.");
return;
}
/*执行启动命令*/
if (system(startup_cmd) < 0)
log_error("Could not execute '%s' (err %d)",
startup_cmd, errno);
}
链接建立后,就发送请求信息:
/*发送请求信息*/
if ((err = write(*fd, req, sizeof(*req))) != sizeof(*req)) {
服务端执行完成,会将结果发送回来,这里需要接受响应信息:
/*获取请求服务的结果*/
int iscsid_response(int fd, iscsiadm_cmd_e cmd, iscsiadm_rsp_t *rsp)
{
int iscsi_err;
int err;
/*从socket读取iscsid进程返回的结果*/
if ((err = recv(fd, rsp, sizeof(*rsp), MSG_WAITALL)) != sizeof(*rsp)) {
log_error("got read error (%d/%d), daemon died?", err, errno);
iscsi_err = ISCSI_ERR_ISCSID_COMM_ERR;
} else
iscsi_err = rsp->err;
close(fd);
if (!iscsi_err && cmd != rsp->command)
iscsi_err = ISCSI_ERR_ISCSID_COMM_ERR;
return iscsi_err;
}
这样,用户态进程间的通信就分析完了,可以发现openiscsi通过本地socket来通信的细节处理十分巧妙,乐趣无穷。
而且open-iscsi整个源码中涉及到三种进程间通信方式,根据传送数据的特点选择不同的方式,分析起源码可以了解很多通信处理的知识。