标签 Solaris 下的文章

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 */

学习虚存-自上而下

如果它不存在,但是你能看见它 — 它是虚拟的(IBM宣传虚拟内存之用语)。虚拟内存技术是计算机发展史上的一项重要的技术,它帮助应用程序摆脱了“体积”的限制。

记得上大学时,有一本书好像叫做“计算机网络 – 自顶向下”,全名记不太清了。书中从人们接触最多也最熟悉的“应用层”开始讲,一直讲到“物理层”,看完这本书后感觉效果不错。所以按照这种方法我也尝试着自上而下的去学习“虚存”,从我们最熟悉的C库接口调用说起,一直谈到底层的硬件支持设施。

1、初学者的疑惑
初学者往往都会写出以下这样的例子程序来学习malloc和free的使用。
int main() {
        int *p = malloc(10000);
        printf("p's address is 0x%p\n", p);
        free(p);
        return 0;
}
但往往结果让这些初学者们感到疑惑。比如上述的例子,在SUN SPARC 64编译后其输出如下:
p's address is 0x100100dc0
看到这样的结果,初学者往往心里嘀咕,“这台机器物理内存才4G,其地址空间总共才4294967296(dec),而0x100100dc0转换十进制为4296019392(dec),这个地址明显已经超出了我的物理内存的限制,这是怎么回事呢?”。其实这里的解释很简单:因为我们看到的都是“虚拟内存地址”。

2、“堆”为何物
malloc是个极其常见的内存分配接口函数,它主要负责运行时在“堆”上为程序动态分配内存空间。我们总是在口头上谈论着“堆”,那么“堆”到底为何物呢?我们已经知道了有“虚拟地址”这个东西的存在,想必“堆”和“虚拟地址”有着千丝万缕的联系^_^。我们来翻看一些经典书籍中的描述。在CSAPP[注1]中的描述是这样的:“堆是进程地址空间中的一段“虚拟地址”空间。在大多数的Unix系统中,堆是映射“二进制零区域(demand-zero)”实现的。其位置在bss段后,其增长方向为高地址方向”。

3、内存映射
前面谈到“demand-zero”这个新名词,那么什么叫“映射到demand-zero”呢?这里蕴含着一个极其重要的概念“内存映射”。内存映射好似一道桥梁,将放在物理磁盘上的对象和一段进程“虚拟地址”空间连接起来。磁盘上的对象,主要指的就是文件,在多数Unix的实现中支持两种文件的内存映射,分别为Regular File和匿名文件(如demand-zero)。映射的过程大致为将文件分成若干“虚拟内存基本单元(页)”大小存于“交换区”,直到CPU指令第一次访问到某个单元时,这个单元才真正被加载到物理内存中。

4、虚拟内存,何方神圣
看到这是不是有些“云里雾里”的感觉亚^_^。其实对于用户进程来说,它是看不到CPU和OS是如何相互配合完成内存管理的。它只认为它面前的是一个这样的情景:“一个完全被我拥有的CPU、一个从拥有M地址空间的物理内存(M = 2的n次方,n为地址总线宽度)…”。这里的用户进程眼中的“物理内存”实际就是“虚拟内存”。虚拟意味着假象,我们知道一个用户进程运行时可能仅仅占用的物理内存的一小部分。看来用户进程被欺骗了。而这个骗局是由操作系统和CPU共同布置的。为了让这个骗局一直维持下去,CPU和OS还是做了很多工作的,究竟有哪些工作呢?我们一一来看看。

1) 交换区(swap)
为了支持虚拟内存,操作系统在物理内存、磁盘之间交换数据的基本单元为“页”。页的大小是固定的,其因操作系统而异。这样一个用户进程在被加载之前首先要被分成若干个“页”,这些页存储在磁盘上。那么是不是进程启动后所有的页都被加载到物理内存中呢?答案是NO。在当前的Unix操作系统中,都有一个叫“交换区”的地方,“交换区”在磁盘上,它存储的是“已分配的虚拟内存页”。又有些糊涂是吧,什么叫已分配的页呢?一个进程虚拟内存页的加载流程大致是这样的:一旦用户进程一虚拟页需要被加载,则操作系统会在“交换区”中为该页分配一个页,一旦CPU访问的虚拟地址落入该页地址空间,则该页才被换入到物理内存中。在这个过程中虚拟页有多个状态,分别如下:
未分配的 - 进程虚拟页未得到加载指令,仍安静的待在磁盘上;
未缓存的 - OS为该进程虚拟页在交换区分配了一个空间,但是该虚拟页还未被引用;
已缓存的 - 该虚拟页被引用,被载入到物理内存中。

2) 换入换出
物理内存容量有限,当物理内存无空间存储新的内存页的时候,就需要将某些内存页从物理内存中移出以为新页腾出空间。这个过程对于那些被移出的页来说,就叫“换出”;相反对于那些新加入到物理内存中的页来说就叫做“换入”。

5、从缓存角度看虚存
现代计算机的存储体系是呈金字塔状的。越接近顶层,速度越快,容量越小,价格越贵;越接近底层,速度越慢,容量越大,价格越低。这样就形成了一个逐级缓存的机制。第K层设备永远是第K+1层设备的缓存。按照这种说法,在早期计算机中,主存是磁盘的缓存,CPU内的高级Cache是主存的缓存。现代计算机基本都支持虚拟内存机制,而虚存页是存储在磁盘上的,虚存页在主存中换入换出。按照缓存的概念,虚存属于容量大,速度慢的第K+1层,而处于第K层的主存就可以看作是虚拟内存的缓存。那么一切缓存理论就都可以应用在虚存和物理内存之间了,比如换入换出算法等。

6、硬件支持
在支持虚拟内存机制的计算机中,CPU都是以虚拟地址形式生成指令地址或者数据地址的,而这个虚拟地址对于物理内存来说是不可见的,那么是谁来屏蔽这个差异的呢?答案是MMU(Memory Management Unit)。MMU负责将CPU发出的虚拟地址转换成相应的物理内存地址。MMU不是孤立工作的,OS为其提供了很好的支持,OS在物理内存中为MMU维护着一张全局的页表,来帮助MMU找到正确地物理内存地址。

7、小结
这里简短而概要的对虚存进行了说明,虚存机制很复杂,不是一句两句能说清楚的,还需要慢慢探索^_^

[注1]
CS.APP – 《computer systems a programmer's perspective》 中文名:《深入理解计算机系统》。

如发现本站页面被黑,比如:挂载广告、挖矿等恶意代码,请朋友们及时联系我。十分感谢! 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