正在阅读:

linux服务器性能调优-连接保持

276

一. 目的

总结linux高性能服务器调优手段 - 连接保持。

在多进程的网络CS模式下,服务器fork产生的子进程在fork调用返回后,

子进程共享父进程的所有打开的描述字。即使在子进程中调用exec函数,

所有描述字通常还是保持打开的状态,也就是描述字是跨exec函数的。

这也是为什么在exec只有的子进程仍然可以调用父进程共享的套接字的原因

二. 总结

1.  保持原理 - 进程间传递描述符

每个进程都拥有自己独立的进程空间,这使得描述符在进程之间的传递变得有点复杂,

这个属于高级进程间通信的内容,下面就来说说,顺便把 Linux 和 Windows 平台都讲下。

Linux 系统系下,子进程会自动继承父进程已打开的描述符,实际应用中,

可能父进程需要向子进程传递“后打开的描述符”,或者子进程需要向父进程传递;

或者两个进程可能是无关的,显然这需要一套传递机制.

a. 方法1  - unix 域套接字传递描述符

#include <sys/types.h>

#include <sys/socket.h>

int sendmsg(int s, const struct msghdr *msg, unsigned int flags);

int recvmsg(int s, struct msghdr *msg, unsigned int flags);

只有unix域协议才能在进程间传递文件描述符,

如果想要在没有亲缘关系的进程间传递,则不能用socketpair函数,要用socket()函数 .

socketpair(PF_UNIX, SOCK_STREAM, 0, sockfds)

socket(AF_UNIX, SOCK_STREAM)

 

两个进程之间建立一个 Unix 域套接字接口作为消息传递的通道,

Linux 系统上使用 socketpair函数可以很方面便的建立起传递通道,

然后发送进程调用 sendmsg 向通道发送一个特殊的消息,

内核将对这个消息做特殊处理,从而将打开的描述符传递到接收进程.

这种方式有几个特点:

1. 需要注意的是传递描述符并不是传递一个 int 型的描述符编号,

而是在接收进程中创建一个新的描述符,并且在内核的文件表中,

它与发送进程发送的描述符指向相同的项.

2.  在进程之间可以传递任意类型的描述符,比如可以是 pipe ,

open , mkfifo 或 socket , accept 等函数返回的描述符,而不限于套接字.

3.  一个描述符在传递过程中(从调用 sendmsg 发送到调用 recvmsg 接收),

内核会将其标记为"在飞行中" ( in flight).在这段时间内,即使发送方试图关闭该描述符,

内核仍会为接收进程保持打开状态, 发送描述符会使其引用计数加 1.

4.  描述符是通过辅助数据发送的(结构体 msghdr 的 msg_control 成员),

在发送和接收描述符时,总是发送至少 1 个字节的数据,即使数据没有实际意义。

否则当接收返回 0 时,接收方将不能区分这意味着“没有数据”

(但辅助数据可能有套接字)还是"文件结束符".

5.  msghdr 的 msg_control 缓冲区必须与 cmghdr 结构对齐,

可以看到后面代码的实现使用了一个 union 结构来保证这一点.

下面先看下msghdr结构体的定义:

struct msghdr {

void       *msg_name;

socklen_t    msg_namelen;

struct iovec  *msg_iov;

size_t       msg_iovlen;

void       *msg_control;

size_t       msg_controllen;

int          msg_flags;

};

套接口地址成员 msg_name 与 msg_namelen :

只有当通道是数据报套接口时才需要, msg_name 指向要发送或是接收信息的套接口地址。

msg_namelen 指明了这个套接口地址的长度。

msg_name 在调用 recvmsg 时指向接收地址,在调用 sendmsg 时指向目的地址.

注意: msg_name 定义为 (void *)数据类型,因此并不需要将套接口地址显示转换为 (struct sockaddr *)

msg_iov 成员指向一个 struct iovec 数组, iovc 结构体在 sys/uio.h 头文件定义:

struct iovec {

ptr_t iov_base; /* Starting address */

size_t iov_len; /* Length in bytes */

};

附属数据缓冲区成员 msg_control 与 msg_controllen :

描述符就是通过它发送的,msg_control 指向附属数据,而msg_controllen 指明缓冲区大小.

接收信息标记位 msg_flags :

 

下面应该介绍 cmsghdr 结构体:

struct cmsghdr {

socklen_t cmsg_len;

int cmsg_level;

int cmsg_type;

/* u_char cmsg_data[]; */

};

cmsg_len  附属数据的字节数, 这包含结构头的尺寸,这个值是由 CMSG_LEN() 宏计算的;

cmsg_level  表明了原始的协议级别 ( 例如, SOL_SOCKET) ;

cmsg_type  表明了控制信息类型 :

SCM指的是套接字级控制信息,socket_level cnotrol message

SCM_RIGHTS ,附属数据对象是文件描述符;

SCM_CREDENTIALS , 附属数据对象是一个包含证书信息的结构 ) ;

被注释的 cmsg_data 用来指明实际的附属数据的位置,帮助理解。

对于 cmsg_level 和 cmsg_type ,当下我们只关心 SOL_SOCKET 和 SCM_RIGHTS 。

msghdr 和 cmsghdr 辅助宏

为创建辅助数据,首先用控制消息缓冲的长度初始化msghdr结构的msg_controllen成员。

接着,用CMSG_FIRSTHDR()在msghdr中取出第一个控制消息,

用CMSG_NEXTHDR()获取接下来的所有元素。对于每个控制消息,

用CMSG_LEN()初始化其cmsg_len,其余的cmsghdr头字段和数据部分用CMSG_DATA()填充。

最终,msghdr的msg_controllen字段应该被设置为缓冲区中所有控制消息的CMSG_SPACE()之和。

有关msghdr的更多信息,参见recvmsg(2)。

当控制消息缓冲太短而无法存下所有消息,MSG_CTRUNC标记将被设置到msghdr的msg_flags成员中。

#include <sys/socket.h>

struct cmsghdr *CMSG_FIRSTHDR(struct msghdr *msgh);

输入参数:指向 struct msghdr 结构的指针;

返回指向附属数据缓冲区内的第一个附属对象的 struct cmsghdr 指针。

如果不存在附属数据对象则返回的指针值为 NULL

struct cmsghdr *CMSG_NXTHDR(struct msghdr *msgh, struct cmsghdr *cmsg);

输入参数:指向 struct msghdr 结构的指针,指向当前 struct cmsghdr 的指针;

这个用于返回下一个附属数据对象的 struct cmsghdr 指针,

如果没有下一个附属数据对象,这个宏就会返回 NULL 。

通过这两个宏可以很容易遍历所有的附属数据

struct msghdr msgh;

struct cmsghdr *cmsg;

for (cmsg = CMSG_FIRSTHDR(&msgh); cmsg != NULL;

cmsg = CMSG_NXTHDR(&msgh,cmsg) {

// 得到了cmmsg,就能通过CMSG_DATA()宏取得辅助数据了

}

size_t CMSG_ALIGN(size_t length);

size_t CMSG_SPACE(size_t length);

入参数: 附属数据缓冲区中的对象大小;

计算 cmsghdr 头结构加上附属数据大小,并包括对其字段和可能的结尾填充字符,

注意 CMSG_LEN() 值并不包括可能的结尾填充字符。

CMSG_SPACE() 宏对于确定所需的缓冲区尺寸是十分有用的。

注意如果在缓冲区中有多个附属数据,一定要同时添加多个 CMSG_SPACE() 宏调用来得到所需的总空间

printf("CMSG_SPACE(sizeof(short))=%d/n", CMSG_SPACE(sizeof(short))); // 返回16

printf("CMSG_LEN(sizeof(short))=%d/n", CMSG_LEN(sizeof(short))); // 返回14

size_t CMSG_LEN(size_t length);

输入参数: 附属数据缓冲区中的对象大小;

计算 cmsghdr 头结构加上附属数据大小,包括必要的对其字段,

这个值用来设置 cmsghdr 对象的 cmsg_len 成员。

void *CMSG_DATA(struct cmsghdr *cmsg);

输入参数:指向 cmsghdr 结构的指针 ;

返回跟随在头部以及填充字节之后的附属数据的第一个字节 ( 如果存在 ) 的地址,

比如传递描述符时,代码将是如下的形式:

struct cmsgptr *cmptr;

. . .

int fd = *(int *)CMSG_DATA(cmptr); // 发送:*(int *)CMSG_DATA(cmptr) = fd;

 

b. 方法2 - ioctl 传递文件描述符

文件描述符用两个ioctl命令经由STREAMS管道交换,这两个命令是:I_SENDFD和I_RECVFD

为了发送一个描述符,将ioctl的第三个参数设置为实际描述符.

当接收一个描述符时,ioctl的第三个参数是一指向strrecvfd结构的指针

struct strrecvfd {

int fd; /* new descriptor */

uid_t uid; /* effective user ID of sender */

gid_t gid; /* effective group ID of sender */

char fill[8];

};

recv_fd读STREAMS管道直到接收到双字节协议的第一个字节(null字节)。

当发出I_RECVFD ioctl命令时,位于流首读队列中的下一条消息应当是一个描述符,

它是由I_SENDFD发来的,或者是一条出错消息

三. 其他 - windows平台传递描述符

总结在windows上实现进程间文件描述符传递

Windows平台上内核对象都是HANDLE,如果要在进程间传递内核对象,Windows提供了DuplicateHandle函数。

复制的HANDLE和原HANDLE实际上指向的是内核中的同一个对象。

对于Socket而言,则需要使用WSADuplicateSocket来传递Socket,

这个操作不像DuplicateHandle那么直观,先来看看函数原型:

int WSADuplicateSocket(

SOCKET s, // 要复制的socket对象

DWORD dwProcessId, // 接收进程的进程号

LPWSAPROTOCOL_INFO lpProtocolInfo // 把Socket的信息复制到这里

);

大概步骤如下:

1)由 WSASocket WSAConnect 获取Socket (发送进程)

2) 请求接收进程ID号 (可以通过共享内存取得)(发送进程)

3) 传递给发送进程ID号 (发送进程)

4) 获取接收进程ID号  (发送进程)

5) 调用函数WSADuplicateSocket 得到WSAPROTOCOL_INFO 结构体对象  (发送进程)

6) 将WSAPROTOCOL_INFO 对象发送给目标进程 (发送进程)

7) 取得WSAPROTOCOL_INFO结构  (发送进程)

8) 调用WSASocket创建共享Socket描述符. (发送进程)

9) 使用Socket进行数据通信 (发送进程)

10) 调用closesocket 关闭Socket  (发送进程)

四. 其他 - sendmsg发送多个描述符

辅助数据是一组struct cmsghdr结构的序列。这个序列只应该使用手册页上描述的宏访问,

而不是直接操作。参见特定协议的手册页了解可用的控制消息类型。

每个套接字所允许的辅助消息缓冲的上限值可以在/proc/sys/net/core/optmem_max设置

发送端:

接受端:

五. 其他 - sendmsg传递凭证

在传送文件描述符方面,UNIX域套接字和STREAMS管道之间的一个区别是,

用STREAMS管道时我们得到发送进程的身份。

FreeBSD 5.2.1和Linux 2.4.22支持在UNIX域套接字上发送凭证,但实现方式不同。

在FreeBSD,将凭证作为cmsgcred结构传送。

#define CMGROUP_MAX 16

struct cmsgcred {

pid_t cmcred_pid; /* sender's process ID */

uid_t cmcred_uid; /* sender's real UID */

uid_t cmcred_euid; /* sender's effective UID */

gid_t cmcred_gid; /* sender's read GID */

short cmcred_ngroups; /* number of groups */

gid_t cmcred_groups[CMGROUP_MAX]; /* groups */

};

当传送凭证时,仅需为cmsgcred结构保留存储空间。

内核将填充该结构以防止应用程序伪装成具有另一种身份。

在Linux中,将凭证作为ucred结构传送

struct ucred {

uint32_t pid; /* sender's process ID */

uint32_t uid; /* sender's user ID */

uint32_t gid; /* sender's group ID */

};

同于FreeBSD的是,Linux要求在传送前先将结构初始化。

内核将确保应用程序使用对应于调用程序的值,或具有适当的权限使用其他值。

客户端必须要为这三个成员赋值,否则内核检查会通不过,报权限错误,

除非客户端以 root 权限运行,服务器使用 recvmsg 接收凭证,没有什么特别的地方,

只要注意一点,对 accept 返回的套接字设置 SO_PASSCRED 选项。

int onoff = 1;

setsockopt(client_fd, SOL_SOCKET, SO_PASSCRED, &onoff, sizeof(onoff))

发送端:

如果遇到下面编译不过的问题

error: ‘SCM_CREDENTIALS’ undeclared

记得在头文件之前加上宏定义

#define _GNU_SOURCE

接收端:

留下脚印,证明你来过。

*

*

流汗坏笑撇嘴大兵流泪发呆抠鼻吓到偷笑得意呲牙亲亲疑问调皮可爱白眼难过愤怒惊讶鼓掌
关闭