open-iscsi/scst 追踪二 (open-iscsi 用户态管理系统技术架构分析)

1.   整体架构

 

       
   
 
 
open-iscsi/scst 追踪二 (open-iscsi 用户态管理系统技术架构分析)_第1张图片

1.1. 命名UNIX域套接字

UNIX域套接字用于同一台机器上的运行的进程之间的通信,它是双向的、一种高效的IPC。UNIX域套接字可以使用标准的套接字接接口创建。

1.2. netlink

netlink套接字是用以实现用户进程与内核进程通信的一种特殊的进程间通信(IPC) ,也是网络应用程序与内核通信的最常用的接口。

Netlink套接字可以使用标准的套接字接口来创建。socket(),bind(), sendmsg(), recvmsg() close()很容易地应用到 netlink socket

2.   iscsid实现

2.1.守护进程的实现

iscsid是一个守护进程,iscsistart和iscsiadm这两个程序通过IPC和守护进程iscsid通信,将任务委托给守护进程iscsid,iscsid执行完任务,将结果通过IPC反馈给iscsidstart或者iscsidadm。

2.1.1.  信号处理

相关结构:

 struct sigaction {

  void     (*sa_handler)(int);

  void     (*sa_sigaction)(int,siginfo_t *, void *);

  sigset_t   sa_mask;

  int        sa_flags;

};

struct sigaction:代表指定信号的处理动作。

字段名字

类型

说明

sa_handler

函数指针

信号捕捉函数,当接受到信号时,该函数会被调用。系统根据不同的信号已经设置两个捕捉函数:SIG_IGN和SIG_DEF

sa_mask

sigset_t

信号编号的一个集合,某个信号的捕捉函数被调用,且该信号在sa_mask中,那么后来的该信号将被阻塞,只有捕捉函数完成,该信号才能被递送。

sa_flags

int

指定对该信号进行处理的各个选项。

sa_sigaction

函数指针

如Sa_flag中SA_SIGINFO被设置,那么捕捉函数将由sa_sigaction来完成而不是sa_handler。

 

int sigaction(int signum, conststruct sigaction *act, struct sigaction *oldact);

功能:检查或者修改于指定编号的信号相关的处理动作。

signum:信号编号

act:指向structsigaction结构的指针,调用完sigaction函数后,接受到signum号信号信号时,将按照struct sigaction设定的处理动作来处理signum号信号,必须为非NULL。

oldact:指向structsigaction结构的指针,将调用sigaction函数以前处理signum号信号的处理动作保存在oldact中,可以设置为NULL。

int sigemptyset(sigset_t *set)

功能:将信号集set设置为空信号集,set=0不能代替该函数。

set:信号集

在main函数刚开始执行的时候设置信号处理动作:

1.设置信号捕捉函数,sa_new.sa_handler = catch_signal,这个函数switch信号编号进行不同的处理,它的实现只有在捕捉到SIGTERM时进行了实际动作:kill iscisd,对其他信号的处理都是空的。

2.清空sa_new中的sa_mask字段sigemptyset(&sa_new.sa_mask),说明某个信号的捕捉函数被调用时,不阻塞该信号的递送。

3.调用sigaction将SIGINT、SIGPIPE、SIGTERM的处理动作设置为sa_new,并把以前的处理动作保存在sa_old中。说明我们只关心SIGINT、SIGPIPE、SIGTERM这三个信号,其他的都按系统默认设置来处理。信号捕捉函数捕捉到这三个信号时,只有是SIGTERM时,进行了实际的操作,SIGINT、SIGPIPE都是空操作,但是SIGINT、SIGPIPE信号必须被我们的程序捕捉,如果不被捕捉,那么将按系统默认来处理,系统是默认都是结束该进程。这两信号对iscisd来说没有意义 (守护进程没有终端,iscisd也没用到管道),所以需要我们的程序捕捉该信号,但不进行任何处理。

2.1.2.  守护进程的实现

相关函数:

mode_t umask(mode_t mask)

功能:设置调用该函数进程的创建文件的模式的屏蔽码。影响open和mkdir函数创建文件模式。返回以前的屏蔽码。

相关函数:

pid_t fork(void)

功能:复制调用它的进程创建一个新的进程。新进程叫做子进程,调用fork的进程叫父进程。

返回值:调用一次会返回两次,对父进程返回子进程的ID,对子进程返回0。

子进程继承父进程的如下属性:

1. 文件描述符

2. 控制终端

3. 有效用户ID、组ID

4. 进程组ID

5. 当前工作目录

6. 信号屏蔽和信号处理方式

7. 文件模式创建屏蔽字

pid_tsetsid(void):

调用该函数的进程必须不能是一个进程组的组长进程,否则出错。

功能:

1.调用该函数的进程变成新会话首进程,该进程是该会话的唯一进程

2.该进程成为一个新进程组的组长进程,新进程组ID是该进程的ID

3.该进程没有控制终端。

一个守护进程就是一个没有终端的进程,所以创建一个守护进程,则该进程在没成为守护进程前必须满足调用setsid函数的要求。

调用流程:

1. umask(0177),该进程以后创建的文件,组用户和其他用户没有读、写、执行的权限。所属用户没有执行权限。

2. pid = fork(),复制父进程创建子进程。

3. if(pid > 0),那么该流程在父进程中执行:

3.1 调用exit退出,子进程将成为孤儿进程。子进程继承了父进程的进程组ID,父进程fork子进程后马上退出,那么子进程不可能是该进程组的组长进程(进程ID等于进程组ID的进程是组长进程),因为父进程在fork子进程前父进程的进程组就存在,那么组长进程也存在,所以子进程的进程ID是新分配的,不可能等于组长进程的ID。这些条件都是调用setsid函数的必要条件。

4. if(pid == 0),子进程的流程中:

4.1 chdir("/")设置子进程的当前工作目录,如果父进程的当前工作目录在一个可挂载的文件系统中,那么子进程的当前工作目录也会在挂载的文件系统中,守护进程是常驻程序,那么就有可能该目录所在文件系统随时被卸载。

4.2 调用dup2将子进程的标准输入、标准输出、标准错误输出文件描述设置为一个指向/dev/null的文件描述符。守护进程是没有终端的,我们不希望看到一个守护进程将log打到控制终端上,而子进程继承了父进程的终端,一般标准输入、标准输出、标准错误输出文件描述都是和终端相关的,所以我们的程序要将这些描述符都定向/dev/null上,这样我们不在从父进程的终端上获取输入,将输出打印到父进程的终端上。

4.3 setsid()使子进程成为守护进程

3.    本地进程间通信

3.1. 服务进程实现

3.1.1.  相关结构:

命名UNIX域套接口地址格式:

struct sockaddr_un {

sa_family_tsun_family;

charsun_path[108];

}

字段名字

类型

说明

sun_family

sa_family_t

确定通信的格式,包括地址格式

sun_path

char型数组

一个文件的路径名。当我们将一地址绑定至UNIX域套接口时,系统用该路径创建一个类型为S_IFSOCK的文件。该文件用于向客户进程告知套接口的名字。该文件不能被打开,也不能由应用程序用于通信。

poll函数使用的结构,用来表示在该文件描述上poll函数关心的操作

struct pollfd

  {

    int fd;

    short int events;

    short int revents;

  };

字段名字

类型

说明

fd

int

轮询函数poll在那个文件描述符上

events

short int

poll函数关心的是该文件描述符上读还是写等

revents

short int

poll执行成功,该文件描述上实际可以进行的操作(读或者写),只有events关心时间超过一件,该值才有意义

一个进程的所属的用户ID和组ID

struct ucred

{

  pid_t pid;

  uid_t uid;

  gid_t gid;

};

字段名字

类型

说明


 

pid

pid_t

进程描述符

uid

uid_t

进程的用户ID,一般那个用户执行了该程序,该用户就是该进程的用户ID;特殊的当该程序的设置用户ID位被设置,那么不管谁运行该程序,对应的进程用户ID都是该程序所属的用户

gid

gid_t

进程的组ID

 

3.1.2.  相关函数

int socket(int domain, int type, intprotocol)

功能:创建一个网络套接字,成功返回套接字描述符,否则返回-1

domain:确定通信的特性,包括地址格式。系统已经设定各种的用途的domain。命名UNIX域套接字使用AF_LOCAL(和AF_UNIX相同涵义)。

type:进一步确定通信特性,命名UNIX域套接字使SOCK_STREAM

protocol:通常是0,意思是按照domain和type选择合适的协议(TCP、UDP)。

int bind(int sockfd, const structsockaddr *addr, socklen_t addrlen);

将一个地址绑定到一个套接字文件描述符上。命名UNIX域套接字使用的地址由结构struct sockaddr_un来表示,也就是将结构sockaddr_un绑定到套接字描述符上。

sockfd:地址绑定在指定套接字描述符上

addr:指向地址结构的指针

addrlen:地址结构的大小以字节为单位

int listen(int sockfd, int backlog);

功能:服务进程调用它后,表示已经可以处理通信了。

sockfd:服务器监听的套接字描述符

backlog:该套接字上最多可以等待处理的请求通信数

int getsockopt(int sockfd, int level,int optname, void *optval, socklen_t *optlen);

功能:获取该套接口的设置选项,命名UNIX域套接口主要利用该函数来接受通信程序的凭证(主要看是不是root用户)。

sockfd:获取指定套接字的属性

level:想要获取协议栈上的那一层的选项,命名UNIX域套接口使用SOL_SOCKET。

optname:获取选项的名字,系统定义;获取凭证使用SO_PEERCRED。

optval:获取到的选项值存放的地址。

optlen:optval的大小,单位字节

int poll(struct pollfd *fds, nfds_tnfds, int timeout);

功能:I/O多路转接函数,可以在多个文件描述符上轮询是否有某个文件描述符可读或者可写。该函数返回值大于零表示有文件描述符可供读写;等于零表示超时;小于零表示出错。

我们同时需要关注两个套接字描述符:一个是netlink和内核通信的套接字,一个本地进程间通信使用的命名UNIX域套接字描述符,我们不能在这两个描述符任意一个上使用read或者write,因为我们不知道这两描述符那个是可用的。例如当我们调用read阻塞在某个描述符上时,但另一个描述符有数据可供读写,进程会用卡在这。

fds:一个指向structpollfd结构数组的指针。每个数组元素代表一个我们关心的文件描述描述及期望该文件描述符上的事件。函数返回时也会把该文件描述上真正发生的事件记录到数组元素struct pollfd中。

nfds:关心的最大文件描述符数量

timeout:超过timeout(单位微妙)还没有可用的文件描述符,则函数返回0。

3.1.3.  函数调用流程

1. 声明一个struct sockaddr_un addr

2. fd = socket(AF_LOCAL, SOCK_STREAM, 0)创建一个命名UNIX域套接口

3. addr.sun_family = AF_LOCAL设置协议族为命名UNIX域套接口

4. memcpy((char *) &addr.sun_path + 1, ISCSIADM_NAMESPACE,

strlen(ISCSIADM_NAMESPACE))和客户进程约定好的路径名,该文件仅向客户进程通告套接口的名字。

5. bind(fd,(struct sockaddr *) &addr, sizeof(addr)))将地址addr绑定到套接口描述符fd上

6. listen(fd,32)监听套接口描述符fd上的链接

7. 声明struct pollfd poll_array[POLL_MAX],我们关心netlink和UNIX域套接口两个描述符,POLL_MAX等于2。将两个套接口描述符及关心的读事件赋给poll_array

8. wile(1)

9. res = poll(poll_array, POLL_MAX, ACTOR_RESOLUTION);在两个描述上轮询:

当res>0判断下那个套接口可用,如果netlink可用调用相关的函数;如果UNIX套接口可用

10. 调用getscokopt验证链接的程序是root用户。

11. 调用fd = accept(accept_fd, NULL, NULL)接受客户进程的链接。

12. 调用read从UNIX域套接口读取数据,格式化后根据不同command,调用netlink或者ioctl和openiscis内核模块通信

13. 将结果通过write函数写到UNIX域套接口中,发送给客户进程。

3.2. 客户进程实现

iscisadm通过命名UNIX域套接口和iscisd通信,将任务委托给iscisd,iscisd完成实际工作,将结果反馈给iscisadm。

3.2.1.  相关函数

int connect(int sockfd, const structsockaddr *addr, socklen_t addrlen);

功能:命名UNIX域套接口是面向连接的网络服务,在开始交换数据前需要在请求的服务的进程套接字(客户进程)和提供服务的进程套接字建立连接。成功返回0,出错返回-1。

sockfd:客户进程创建的套接字描述符

struct sockaddr:客户进程的地址格式,必须和服务进程一致

addlen:地址的的长度,字节为单位

3.2.2.  创建套接口

和服务进程相同。结构struct sockaddr_un中的sun_path路径要和服务进程设置成同一个路径,否则会导致不能和服务进程通信。

 

3.2.3.  和服务器进程通信

1. for循环条件nsec<= MAXSLEEP,则:

2. connect(*fd, (struct sockaddr *) &addr, sizeof(addr)

3. 如果connect函数返回值等于0.连接成功,for循环结束

4. sleep(nsec),nsec<<1

5. 将解析好的iscisadm命令通过write函数写到套接字描述符sockfd,等待服务进程接受。

6. recv(fd, rsp, sizeof(*rsp), MSG_WAITALL))函数接受服务进程的反馈,判断命令是否接受成功。

7. close(sockfd),关闭套接字描述符

4.    数据结构

4.1. 命名UNIX域套接口的并发访问

iscisadm在很短时间给iscisd发送很多请求(每个请求都是一个命令),可能发生前面的命令还没完成后面的命令又到达的情况,iscisd没有使用互斥锁之类的而是使用了两个链表,一个存放iscisadm发送的请求,一个存放iscisd执行命令后的反馈信息(通过netlink或者ioctl)。反馈链表开始的时候是空的,当poll函数返回0时,将请求链表上的actor移致到反馈链表尾部,当反馈信息的套接口可读时,将信息读入到链表头部结构中。

struct actor{

        struct list_head list;

        actor_state_e state;

        void *data;

        void (*callback)(void * );

        uint64_t scheduled_at;

        uint64_t ttschedule;

}

字段名字

类型

说明

list

struct list_head

将该结构链接到一个全局链表上

state

actor_state_e

枚举类型,标记该结构当前的状态,例如正在等待反馈信息,超时等

data

void *

存放iscisd的命令或者netlink的反馈信息

callback

函数指针

当该结构表示的一个请求完成时,完成善后工作;释放data指向的内存空间。

scheduled_at

uint64_t

结构从请求链表上移至反馈链表上时的时间

ttschedule

uint64_t

在请求或者反馈流程上延迟的时间

 

4.2. 请求和响应

在服务进程和客户进程间,客户进程需要向服务进程发送数据,格式采用

struct iscsiadm_req这个结构,该结构结构中使用联合的形式将不同形式的请求组织到一个结构,服务进程可以根据struct iscsiadm_req中command的不同来格式化收到的数据。

服务进程需要向客户进程反馈数据格式采用structiscsiadm_rsp这个结构,该结构中也是用了联合。客户进程可以根据struct iscsiadm_rsp中的command来格式化收到的数据。

服务进程根据自身能提供的功能,将每个功能封装成一个函数,将这些函数存放到一个函数数组中,客户进程发送的命令都解析成在函数数组中的下标,存放在struct iscsiadm_req的command字段中,服务进程收到请求,只需根command调用函数数组中的对应函数即可。这些command的预定放在一个文件中,提供给iscisd和iscisadm一起使用。

你可能感兴趣的:(存储系统/架构)