分类 技术志 下的文章

APR分析-线程篇

并行一直是程序设计领域的难点,而线程是并行的一种重要的手段,而且线程的一些特性也能在进程并行时发挥很好的作用(在“线程同步篇”中详细阐述)。

APR线程的源代码的位置在$(APR_HOME)/threadproc目录下,本篇blog着重分析unix子目录下的thread.c文件内容,其相应头文件为$(APR_HOME)/include/apr_threadproc.h。

一、线程基础
深入理解计算机系统》(以下称CS.APP)一书中对线程基础概念的讲解让我眼前豁然开朗,这里不妨引述一下:
(1) 在传统观点中,进程是由存储于用户虚拟内存中的代码、数据和栈,以及由内核维护的“进程上下文”组成的,其中“进程上下文”又可以看成“程序上下文”和“内核上下文”组成,可参见下面图示:
进程–
      |- 进程上下文
             |- 程序上下文
                   |- 数据寄存器
                   |- 条件码
                   |- 栈指针
                   |- 程序计数器
            |- 内核上下文
                   |- 进程ID
                   |- VM结构
                   |- Open files
                   |- 已设置的信号处理函数
                   |- brk pointer
    |- 代码、数据和栈(在虚存中)
            |- 栈区 <– SP
            |- 共享库区
            |- 运行时堆区 <– brk
            |- 可读/写数据区
            |- 只读代码/数据区 <– PC

(2) 另种观点中,进程是由线程、代码和数据以及内核上下文组成的,下图更能直观的展示出两种观点的异同:
进程 –+
   |- 线程
           |- 栈区 <– SP
           |- 线程上下文
           |- 线程ID
           |- 数据寄存器
           |- 条件码
           |- 栈指针
           |- 程序计数器
  |- 内核上下文
           |- 进程ID
           |- VM结构
           |- Open files
           |- 已设置的信号处理函数
           |- brk pointer
  |- 代码、数据(在虚存中)
           |- 共享库区
           |- 运行时堆区 <– brk
           |- 可读/写数据区
           |- 只读代码/数据区 <– PC

对比两种观点我们可以得出以下几点结论:
(a) 从观点(2)可以看出进程内的多个线程共享进程的内核上下文和代码、数据(当然不包括栈区);
(b) 线程上下文比进程上下文小,且切换代价小;
(c) 线程不像进程那样有着“父-子”体系,同一个进程内的线程都是“对等的”,主线程与其他线程不同之处就在于其是进程创建的第一个线程。

二、APR线程管理接口
如今应用最广泛的线程包就是Posix Thread了。APR对线程的封装也是基于Posix thread的。

APR线程管理接口针对apr_thread_t这个基本的数据结构进行操作,apr_thread_t的定义很简单:
/* apr_arch_threadproc.h */
struct apr_thread_t {
    apr_pool_t *pool;
    pthread_t *td;
    void *data;
    apr_thread_start_t func;
    apr_status_t exitval;
};
这个结构中包含了线程ID、线程函数以及该函数的参数数据。不过APR的线程函数定义与Pthread的有不同,“Pthread线程函数”是这样的:
typedef void *(start_routine)(void*);
而“APR线程函数”如下:
typedef void *(APR_THREAD_FUNC *apr_thread_start_t)(apr_thread_t*, void*);

1、apr_thread_create
apr_thread_create内部定义了一个dummy_worker的“Pthread线程函数”,并将apr_thread_t结构作为参数传入,然后在dummy_worker中启动“APR的线程函数”。在该函数的参数列表中有一项类型为apr_threadattr_t:
struct apr_threadattr_t {
    apr_pool_t *pool;
    pthread_attr_t attr;
};
这个类型封装了线程的属性,不同的线程属性会导致线程的行为有所不同。Pthread提供多种线程属性设置接口,可是APR并未全部提供,必要时我觉得可以自己来调用Pthread接口。APR提供的属性设置接口包括设置线程的可分离性、线程栈大小和栈Guard区域属性。

2、apr_thread_exit
进程退出我们可以直接调用exit函数,而线程退出也有几种方式:
(1) 隐式退出 – 可以理解为线程main routine代码结束返回;
(2) 显式退出 – 调用线程包提供的显式退出接口,在apr中就是apr_thread_exit;
(3) 另类显式退出 – 调用exit函数,不仅自己退出,其所在线程也跟着退出了;
(4) 被“黑”退出 – 被别的“对等”线程调用pthread_cancel而被迫退出。
apr_thread_exit属于种类(2),该种类退出应该算是线程的优雅退出了。apr_thread_exit做了3个工作,分别为设置线程返回值、释放pool中资源和调用pthread_exit退出。

3、apr_thread_join和apr_thread_detach
进程有waitpid,线程有join。线程在调用apr_thread_exit后,只是其执行停止了,其占有的“资源”并不一定释放,这里的“资源”我想就是“另种观点”中的“线程上下文”,线程有两种方式来释放该“资源”,这主要由线程的“可分离”属性决定的。如果线程是“可分离的”,当线程退出后就会自动释放其“资源”,如果线程为“非可分离的”,则必须由“对等线程”调用join接口来释放其资源。apr_thread_detach用来将其调用线程转化为“可分离”线程,而apr_thread_join用来等待某个线程结束并释放其资源。

三、小结
基本的线程管理接口相对较简单,关键是对线程概念的理解。接下来的“线程同步”则是件比较有趣的话题。

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

如发现本站页面被黑,比如:挂载广告、挖矿等恶意代码,请朋友们及时联系我。十分感谢! Go语言第一课 Go语言精进之路1 Go语言精进之路2 Go语言编程指南
商务合作请联系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