2005年十二月月 发布的文章

APR源代码分析-网络IO篇

“这个世界如果没有了网络就好比没有了石油、没有了电一样,是多么的可怕呀。”相信世界上已经有很多很多的人能够同意这种观点了,通过这个观点也可以看出网络在现代人们心中的地位。而运行在网络节点上的网络应用程序则是在幕后默默地为人们提供着服务。Apache Server就是其中一个典型的代表。而APR网络I/O库则像磐石一样支撑着Apache Server的运行。

APR网络I/O的源代码的位置在$(APR_HOME)/network_io目录下,本篇blog着重分析unix子目录下的各.c文件内容,其相应头文件为$(APR_HOME)/include/apr_network_io.h。

以程序员的视角来看待网络,这样我们可以忽略一些网络的基础概念。下面将循序渐进地接触网络,并说明APR是如何支持这些网络概念的。

一、IP地址 — 主机通信
我们熟知的并且每天工作于其上的因特网是一个世界范围的主机的集合,这个主机集合被映射为一个32位(目前)或者64位(将来)IP地址;而IP地址又被映射为一组因特网域名;一个网络中的主机上的进程能通过一个连接(connection)和任何其他网络中的主机上的进程通信。

1、IP地址存储
在如今的IPV4协议中我们一般使用一个unsigned int来存储IP地址,在UNIX平台下,使用如下结构来存储一个IP地址的值:
/* Internet address structure */
struct in_addr {
 unsigned int s_addr; /* network byte order (big-endian) */
};
这里值得一提的是APR关于IP地址存储的做法,看如下代码:
#if (!APR_HAVE_IN_ADDR)
/**
 * We need to make sure we always have an in_addr type, so APR will just
 * define it ourselves, if the platform doesn't provide it.
 */
struct in_addr {
    apr_uint32_t  s_addr;
};
#endif
APR保证了其所在平台上in_addr的存在。还有一点儿需要注意的是在in_addr中,s_addr是以网络字节序存储的。如果你的IP地址不符合条件,可通过调用一些辅助接口来做转换,这些接口包括:
htonl : host to network long ;
htons : host to network short ;
ntohl : network to host long ;
ntohs : network to host short.

2、IP地址表示
我们平时看到的IP地址都是类似“xxx.xxx.xxx.xxx”这样的点分十进制的。上面说过IP地址使用的是一个unsigned int整形数来表示。这样就存在着一个IP地址表示和IP地址存储之间的一个转换过程。APR提供这一转换支持,我们用一个例子来说明:
#include
#include
#include "apr_network_io.h"
#include "apr_arch_networkio.h"

int main(int argc, const char * const * argv, const char * const *env)
{
        apr_app_initialize(&argc, &argv, &env);

        char    presentation[100];
        int     networkfmt;
        memset(presentation, 0, sizeof(presentation));

        apr_inet_pton(AF_INET, "255.255.255.255", &networkfmt);
        printf("0x%x\n", networkfmt);

        apr_inet_ntop(AF_INET, &networkfmt, presentation, sizeof(presentation));
        printf("presentation is %s\n", presentation);

        apr_terminate();
        return 0;
}
APR提供apr_inet_pton将我们熟悉的点分十进制形式转换成一个整型数存储的IP地址;而apr_inet_ntop则将一个存整型数存储的IP地址转换为我们可读的点分十进制形式。这两个接口的功能类似于系统调用inet_pton和inet_ntop,至于使用哪个就看你的喜好了^_^。

二、SOCKET — 进程通信
前面提到过通过一个连接(connection)可以连接两个internet不同或相同主机上的不同进程,这个连接是点对点的。而从Unix内核角度来看,SOCKET则是连接的一个端点。每个SOCKET都有一个地址,其地址由主机IP地址和通讯端口号组成。一个连接有两个端点,这样一个连接就可以由一个SOCKET对唯一表示了。这个SOCKET对是这个样子的(cliaddr:cliport, servaddr:servport)。

那么在应用程序中我们如何获取和使用这一互联网上的进程通讯利器呢?每个平台都为应用程序提供了一套SOCKET编程接口,APR又在不同平台提供的接口之上进行了封装,使代码可以在不同平台上编译运行,而且易用性也有所提高。

1、SOCKET描述符
SOCKET属于系统资源,我们必须通过系统调用来申请该资源。SOCKET资源的申请类似于FILE,在使用文件时我们通过调用open函数获取文件描述符,类似我们也可通过调用下面的接口来获取SOCKET描述符:
int socket(int domain, int type, int protocol);
从Unix程序的角度来看,SOCKET就是一个有相应描述符的打开的文件。在APR中我们可以通过调用apr_socket_create来创建一个APR自定义的SOCKET对象,该SOCKET结构如下:
/* apr_arch_networkio.h */
struct apr_socket_t {
    apr_pool_t *cntxt;
    int socketdes;
    int type;
    int protocol;
    apr_sockaddr_t *local_addr;
    apr_sockaddr_t *remote_addr;
    apr_interval_time_t timeout;
#ifndef HAVE_POLL
    int connected;
#endif
    int local_port_unknown;
    int local_interface_unknown;
    int remote_addr_unknown;
    apr_int32_t options;
    apr_int32_t inherit;
    sock_userdata_t *userdata;
#ifndef WAITIO_USES_POLL
    /* if there is a timeout set, then this pollset is used */
    apr_pollset_t *pollset;
#endif
};
该结构中的socketdes字段其实是真正存储由socket函数返回的SOCKET描述符的,其他字段都是为APR自己所使用的,这些字段在Bind、Connect等过程中使用。另外需要提及的就是要分清SOCKET描述符和SOCKET地址(IP地址,端口号),前者是系统资源,而后者用来描述一个连接的一个端点的地址。SOCKET描述符可以代表任意的SOCKET地址,也可以绑定到某个固定的SOCKET地址上(在后面有说明)。我们如果不显式将SOCKET描述符绑定到某SOCKET地址上,系统内核就会自动为该SOCKET描述符分配一个SOCKET地址。

2、SOCKET属性
还是与文件对比,在文件系统调用中有一个fcntl接口可以用来获取或设置已分配的文件描述符的属性,如是否Block、是否Buffer等。SOCKET也提供类似的接口调用setsockopt和getsockopt。在APR中等价于该功能的接口是apr_socket_opt_set和apr_socket_opt_get。APR在apr_network_io.h中提供如下SOCKET的参数属性:
#define APR_SO_LINGER        1    /**< Linger */
#define APR_SO_KEEPALIVE     2    /**< Keepalive */
#define APR_SO_DEBUG         4    /**< Debug */
#define APR_SO_NONBLOCK      8    /**< Non-blocking IO */
#define APR_SO_REUSEADDR     16   /**< Reuse addresses */
#define APR_SO_SNDBUF        64   /**< Send buffer */
#define APR_SO_RCVBUF        128  /**< Receive buffer */
#define APR_SO_DISCONNECTED  256  /**< Disconnected */
… …
另外从上面这些属性值(都是2的n次方)可以看出SOCKET也是使用一个属性控制字段中的“位”来控制SOCKET属性的。
再有APR提供一个宏apr_is_option_set来判断一个SOCKET是否拥有某个属性。

3、Connect、Bind、Listen、Accept — 建立连接
这里不详述C/S模型了,只是说说APR支持C/S模型的一些接口。

(1) apr_socket_connect
客户端连接服务器端的唯一调用就是connect,connect试图建立一个客户端进程与服务器端进程的连接。apr_socket_connect的参数分别为客户端已经打开的一个SOCKET以及指定的服务器端的SOCKET地址(IP ADDR : PORT)。apr_socket_connect内部实现的流程大致如以下代码:
apr_socket_connect
{
 do {
         rc = connect(sock->socketdes,
                     (const struct sockaddr *)&sa->sa.sin,
                     sa->salen);
     } while (rc == -1 && errno == EINTR);   ——– (a)

 if ((rc == -1) && (errno == EINPROGRESS || errno == EALREADY)
                   && (sock->timeout > 0)) {
        rc = apr_wait_for_io_or_timeout(NULL, sock, 0); ——— (b) 注[1]
        if (rc != APR_SUCCESS) {
            return rc;
        }

 if (rc == -1 && errno != EISCONN) {
         return errno;   ——— (c)
     }
 
 初始化sock->remote_addr;
 
 … …
}
对上述代码进行若干说明:
(a) 执行系统调用connect连接服务器端,注意这里做了防止信号中断的处理,这个技巧在以前的文章中提到过,这里不详述;
(b) 如果系统操作正在进行中,调用apr_wait_for_io_or_timeout进行超时等待;
(c) 错误返回,前提errno不是表示已连接上。
一旦apr_socket_connect成功返回,我们就已经成功建立了一个SOCKET对,即一个连接。

(2) apr_socket_bind
Bind、Listen和Accept这三个过程是服务器端用于接收“连接”的必经之路。其中Bind就是告诉操作系统内核显式地为该SOCKET描述符分配一个SOCKET地址,这个SOCKET地址就不能被其他SOCKET描述符占用了。在服务器编程中Bind几乎成为了“必选”之调用,因为一般服务器程序都有自己的“名气很大”的SOCKET地址,如TELNET服务端口号23等。apr_socket_bind也并未做太多的工作,只是简单的调用了bind系统接口,并设置了apr_socket_t结构的几个local_addr字段。

(3) apr_socket_listen
按照《Unix网络编程 Vol1》的说法,SOCKET描述符在初始分配时都处于“主动连接”状态,Listen过程将该SOCKET描述符从“主动连接”转换为“被动状态”,并告诉内核接受该SOCKET描述符的连接请求。apr_socket_listen的背后直接就是listen接口调用。

(4) apr_socket_accept
Accept过程在“被动状态”SOCKET描述符上接受一个客户端的连接,这时系统内核会自动分配一个新的SOCKET描述符,内核为该描述符自动分配一个SOCKET地址,来代表这条连接的服务器端。注意在SOCKET编程接口中除了socket函数能分配新的SOCKET描述符之外,accept也是另外的一个也是唯一的一个能分配新的SOCKET描述符的系统调用了。apr_socket_accept首先在pool中分配一个新的apr_socket_t结构变量,然后调用accept,并设置新变量的各个字段。

4、Send/Recv — 数据传输
网络通信最重要的还是数据传输,在SOCKET编程接口中最常见的两个接口就是recv和send。在APR中分别有apr_socket_recv和apr_socket_send与前面二者对应。下面逐一分析。
(1) apr_socket_recv
首先来看看apr_socket_recv的实现过程:
apr_socket_recv
{
 if (上次调用apr_socket_recv没有读完所要求的字节数) { ———-(a)
  设置sock->options;  
  goto do_select;
 }

 do {
         rv = read(sock->socketdes, buf, (*len)); —— (b)
     } while (rv == -1 && errno == EINTR);
 
 if ((rv == -1) && (errno == EAGAIN || errno == EWOULDBLOCK)
                    && (sock->timeout > 0)) {
do_select:
         arv = apr_wait_for_io_or_timeout(NULL, sock, 1);
         if (arv != APR_SUCCESS) {
              *len = 0;
              return arv;
         }
         else {
              do {
                  rv = read(sock->socketdes, buf, (*len));
              } while (rv == -1 && errno == EINTR);
         }
     }  ———— (c)

 设置(*len)和sock->options;  ————-(d)
 … …
}
针对上面代码进行简单说明:
(a) 一次apr_socket_recv调用完全有可能没有读完所要求的字节数,这里做个判断以决定是否继续读完剩下的数据;
(b) 调用read读取SOCKET缓冲区数据,注意这里做了防止信号中断的处理,这个技巧在以前的文章中提到过,这里不详述;
(c) 如果SOCKET操作正在忙,我们调用apr_wait_for_io_or_timeout等待,直到SOCKET可用。这里我觉得好像有个问题,想象一下如果上一次SOCKET的状态为APR_INCOMPLETE_READ,那么重新调用apr_socket_read后在SOCKET属性中去掉APR_INCOMPLETE_READ,然后进入apr_wait_for_io_or_timeout过程,一旦apr_wait_for_io_or_timeout失败,那么就直接返回了。而实际上SOCKET仍然应该处于APR_INCOMPLETE_READ状态,而下次再调用apr_socket_read就直接进入一轮完整数据的读取过程了,不知道这种情形是否能否发生。
(d) 将(*len)设置为实际从SOCKET Buffer中读取的字节数,并根据这一实际数据与要求数据作比较来设置sock->options。

(2) apr_socket_send
apr_socket_send负责发送数据到SOCKET Buffer,其实现的方式与apr_socket_recv大同小异,这里就不分析了。

三、小结
APR Network I/O中还有对Multicast的支持,由于平时不常接触,这里不分析了。

注[1]:
/* in errno.h */
#define EISCONN         133     /* Socket is already connected */
#define EALREADY        149     /* operation already in progress */
#define EINPROGRESS     150     /* operation now in progress */

APR源代码分析-进程同步篇

最新的统计数据显示Apache服务器在全世界仍然占据着Web服务器龙头老大的位置,而且市场占有率遥遥领先,所以学习Apache相关知识是完全正确的方向,这里我们继续分析APR进程同步相关内容。

进程同步的源代码的位置在$(APR_HOME)/locks目录下,本篇blog着重分析unix子目录下的proc_mutex.c、global_mutex文件内容,其相应头文件为$(APR_HOME)/include/apr_proc_mutex.h、apr_global_mutex.h。其用于不同进程之间的同步以及多进程多线程中的同步问题。

APR提供三种同步措施,分别为:
apr_thread_mutex_t – 支持单个进程内的多线程同步;
apr_proc_mutex_t – 支持多个进程间的同步;
apr_global_mutex_t  – 支持不同进程内的不同线程间同步。
在本篇中着重分析apr_proc_mutex_t。

1、同步机制
APR提供多种进程同步的机制供选择使用。在apr_proc_mutex.h中列举了究竟有哪些同步机制:
typedef enum {
    APR_LOCK_FCNTL,         /* 记录上锁 */
    APR_LOCK_FLOCK,         /* 文件上锁 */
    APR_LOCK_SYSVSEM,       /* 系统V信号量 */
    APR_LOCK_PROC_PTHREAD,  /* 利用pthread线程锁特性 */
    APR_LOCK_POSIXSEM,      /* POSIX信号量 */
    APR_LOCK_DEFAULT        /* 默认进程间锁 */
} apr_lockmech_e;

这几种锁机制,随便拿出哪一种都很复杂。APR的代码注释中强调了一点就是“只有APR_LOCK_DEFAULT”是可移植的。这样一来用户若要使用APR进程同步机制接口,就必须显式指定一种同步机制。

2、实现点滴
APR提供每种同步机制的实现,每种机制体现为一组函数接口,这些接口被封装在一个结构体类型中:

/* in apr_arch_proc_mutex.h */
struct apr_proc_mutex_unix_lock_methods_t {
    unsigned int flags;
    apr_status_t (*create)(apr_proc_mutex_t *, const char *);
    apr_status_t (*acquire)(apr_proc_mutex_t *);
    apr_status_t (*tryacquire)(apr_proc_mutex_t *);
    apr_status_t (*release)(apr_proc_mutex_t *);
    apr_status_t (*cleanup)(void *);
    apr_status_t (*child_init)(apr_proc_mutex_t **, apr_pool_t *, const char *);
    const char *name;
};

之后在apr_proc_mutex_t类型中,apr_proc_mutex_unix_lock_methods_t的出现也就在情理之中了:)
/* in apr_arch_proc_mutex.h */
struct apr_proc_mutex_t {
    apr_pool_t *pool;
    const apr_proc_mutex_unix_lock_methods_t *meth;
    const apr_proc_mutex_unix_lock_methods_t *inter_meth;
    int curr_locked;
    char *fname;
    … …
#if APR_HAS_PROC_PTHREAD_SERIALIZE
    pthread_mutex_t *pthread_interproc;
#endif
};

这样APR提供的用户接口其实就是对mech各个“成员函数”功能的“薄封装”,而真正干活的其实是apr_proc_mutex_t中的meth字段的“成员函数”,它们的工作包括mutex的创建、获取(加锁)和清除(解锁)等。以“获取锁”为例APR的实现如下:
APR_DECLARE(apr_status_t) apr_proc_mutex_lock(apr_proc_mutex_t *mutex)
{
    return mutex->meth->acquire(mutex);
}

3、同步机制
按照枚举类型apr_lockmech_e的声明,我们知道APR为我们提供了5种同步机制,下面分别简单说说:
(1) 记录锁
记录锁是一种建议性锁,它不能防止一个进程写已由另一个进程上了读锁的文件,它主要利用fcntl系统调用来完成锁功能的,记得在以前的一篇关于APR 文件I/O的Blog中谈过记录锁,这里不再详细叙述了。

(2) 文件锁
文件锁是记录锁的一个特例,其功能由函数接口flock支持。值得说明的是它仅仅提供“写入锁”(独占锁),而不提供“读入锁”(共享锁)。

(3) System V信号量
System V信号量是一种内核维护的信号量,所以我们只需调用semget获取一个System V信号量的描述符即可。值得注意的是与POSIX的单个“计数信号量”不同的是System V信号量是一个“计数信号量集”。所以我们在注意的是在初始化时设定好信号量集的属性以及在调用semop时正确选择信号量集中的信号量。在APR的System V信号量集中只是申请了一个信号量。

(4) 利用线程互斥锁机制
APR使用pthread提供的互斥锁机制。原本pthread互斥锁是用来互斥一个进程内的各个线程的,但APR在共享内存中创建了pthread_mutex_t,这样使得不同进程的主线程实现互斥,从而达到进程间互斥的目的。截取部分代码如下:
new_mutex->pthread_interproc = (pthread_mutex_t *)mmap(
                                       (caddr_t) 0,
                                       sizeof(pthread_mutex_t),
                                       PROT_READ | PROT_WRITE, MAP_SHARED,
                                       fd, 0);

(5) POSIX信号量
APR使用了POSIX有名信号量机制,从下面的代码中我们可以看出这一点:
/* in proc_mutex.c */
apr_snprintf(semname, sizeof(semname), "/ApR.%lxZ%lx", sec, usec); /* APR自定义了一种POSIX信号量命名规则,在源代码中有说明 */
psem = sem_open(semname, O_CREAT, 0644, 1);

4、如何使用
我们知道父进程的锁其子进程并不继承。APR进程同步机制的一个典型使用方法就是:“Create the mutex in the Parent, Attach to it in the Child”。APR提供接口apr_proc_mutex_child_init在子进程中re-open the mutex。

5、小结
APR提供多种锁机制,所以使用的时候要根据具体应用情况细心选择。

如发现本站页面被黑,比如:挂载广告、挖矿等恶意代码,请朋友们及时联系我。十分感谢! Go语言第一课 Go语言精进之路1 Go语言精进之路2 商务合作请联系bigwhite.cn AT aliyun.com

欢迎使用邮件订阅我的博客

输入邮箱订阅本站,只要有新文章发布,就会第一时间发送邮件通知你哦!

这里是 Tony Bai的个人Blog,欢迎访问、订阅和留言! 订阅Feed请点击上面图片

如果您觉得这里的文章对您有帮助,请扫描上方二维码进行捐赠 ,加油后的Tony Bai将会为您呈现更多精彩的文章,谢谢!

如果您希望通过微信捐赠,请用微信客户端扫描下方赞赏码:

如果您希望通过比特币或以太币捐赠,可以扫描下方二维码:

比特币:

以太币:

如果您喜欢通过微信浏览本站内容,可以扫描下方二维码,订阅本站官方微信订阅号“iamtonybai”;点击二维码,可直达本人官方微博主页^_^:
本站Powered by Digital Ocean VPS。
选择Digital Ocean VPS主机,即可获得10美元现金充值,可 免费使用两个月哟! 著名主机提供商Linode 10$优惠码:linode10,在 这里注册即可免费获 得。阿里云推荐码: 1WFZ0V立享9折!


View Tony Bai's profile on LinkedIn
DigitalOcean Referral Badge

文章

评论

  • 正在加载...

分类

标签

归档



View My Stats