标签 Unix 下的文章

APR源代码分析-整体篇

由于部门所使用的底层库与Apache Server有着“一定的渊源”,所以总有一种想看看Apache的实现的冲动。最近项目收尾,愿望终可实现。

一、何为APR?
Apache Server经过这么多年的发展后,将一些通用的运行时接口封装起来提供给大家,这就是Apache Portable Run-time libraries, APR。

二、APR的目录组织
www.apache.org上下载apr-1.1.1.tar.gz到本地解压后,发现APR的目录结构很清晰。
1) 所有的头文件都放在$(APR)/include目录中;
2) 所有功能接口的实现都放在各自的独立目录下,如threadproc、mmap等;
3) 此外就是相关平台构建工具文件如Makefile.in等。曾经看过ACE的代码,ACE的所有源文件(.cpp)都放在一个目录下,显得很混乱。APR给我的第一印象还不错。
4) 进入各功能接口子目录,以threadproc为例,在其下面的子目录有5个,分别为beos、netware、os2、unix和win32。从APR的名字也可以理解,每个子目录下都存放着各个平台的独特实现源文件。

三、APR构建
如果想要使用APR,需要先在特定平台上构建它,这里不考虑多个平台的特性,仅针对Unix平台进行分析。
1) apr.h、apr.h.in、apr.h.hw和apr.h.hnw的关系
在$(APR)/include目录下,由于APR考虑移植性等原因,最基本的apr.h文件是在构建时自动生成的,其中apr.h.in类似一模板作为apr.h生成程序的输入源。其中apr.h.hw和apr.h.hnw分别是Windows和NetWare的特定版本。

2) 编译时注意事项
在Unix上编译时,注意$(APR)/build下*.sh文件的访问权限,应该先chmod一下,否则Make的时候会提示ERROR。

四、应用APR
我们首先make install一下,比如我们在Makefile中指定prefix=$(APR)/dist,则make install后,在$(APR)/dist下会发现4个子目录,分别为bin、lib、include和build,其中我们感兴趣的只有include和lib。下面是一个APR app的例子project。
该工程的目录组织如下:
$(apr_path)
 - dist
    – lib
    – include
 - examples
    – apr_app
      – Make.properties
      – Makefile
      – apr_app.c

我们的Make.properties文件内容如下:
#
# The APR app demo
#
CC              = gcc -Wall

BASEDIR         = $(HOME)/apr-1.1.1/examples/apr_app
APRDIR          = $(HOME)/apr-1.1.1
APRVER          = 1

APRINCL         = $(APRDIR)/dist/include/apr-$(APRVER)
APRLIB          = $(APRDIR)/dist/lib

DEFS            = -D_REENTRANT -D_POSIX_PTHREAD_SEMANTICS -D_DEBUG_
LIBS            = -L$(APRLIB) -lapr-$(APRVER) \
                  -lpthread -lxnet -lposix4 -ldl -lkstat -lnsl -lkvm -lz -lelf -lm -lsocket -ladm
INCL            = -I$(APRINCL)
CFLAGS          = $(DEFS) $(INCL)

Makefile文件内容如下:
include Make.properties

TARGET  = apr_app

OBJS    = apr_app.o

all: $(TARGET)

$(TARGET): $(OBJS)
        $(CC) ${CFLAGS} -o $@ $(OBJS) ${LIBS}
clean:
        rm -f core $(TARGET) $(OBJS)

而apr_app.c文件采用的是$(apr_path)/test目录下的proc_child.c文件。编译运行一切OK!

五、GO ON
分析APR的过程也是我学习Unix高级系统机制的过程,有时间我会继续APR分析的。

同步问题讨论-Tony与Alex的对话系列

Alex正在电脑前面作冥思苦想状,这时Tony悄悄地走到Alex的身后,观察了一会儿…

Tony : 看来今天我们要讨论同步问题了。
Alex : (惊奇地回头)。Hey Man , you scared me! 你说的没错,我正在学习同步这一块儿呢,有什么高见不妨说出来吧,我洗耳恭听!
Tony : 不敢不敢。关于进程和线程同步的问题,W. Richard Stevens在他的那本经典的“UNIX Network Programming Volume 2”中有过详尽的讲解,你不妨仔细阅读一下。
Alex : 远水解不了近渴。你还是大概跟我说说吧!
Tony : OK, 我们就拿一个最简单例子来探讨一下吧。在拿出例子之前我们来回顾一下同步的由来。Alex你说说为什么要同步呢?
Alex : 有共享就要同步,就好比超市的POS,如果没有好的同步顾客活动的策略,那超市不就乱了套了么,大家都争着抢着去结账。
Tony : 嗯,没错。mess world is not what we need! 互斥和条件变量是我们经常使用的同步手段,当然更高级的还有信号灯等。
Alex : 逐一说明吧,看来今天又会有不小的收获^_^
Tony : 历史上有个特别有名的问题叫做“生产者-消费者”问题,又叫“有限缓冲区”问题,我们今天的例子大约就是这个样子的。
Alex : (入迷的样子)
Tony : 我们的例子是这样的,我们有“生产者”和“消费者”两个角色,他们共享某一整型变量,规定如下:
       1)生产者发现产品已经被消费了,便生产,即将该共享变量置为1;
       2)消费者发现有产品了,便消费,即将该共享变量置为0;
       很简单吧。我们还是用老办法,由简入难,我们可以使用最简单的手段“互斥锁”来完成这个任务。
Alex : 我知道“互斥锁”,但是了解得并不深,先讲讲理论把!
Tony : 互斥,顾名思义互相排斥,它是最基本的同步手段,一般用来保护“临界区”,“临界区”是一段代码,看起来互斥保护了临界区这段代码的,实质上互斥保护的是“临界区”中被操纵的数据。
Alex : 互斥是不是即可用于线程,也可以用于进程呢?
Tony : 都可以,在我们的例子中我们使用线程,因为线程间共享一个数据空间,实现起来比较容易;进程间要想共享数据就需要额外的支持,比如共享内存等。
Alex : 噢。
Tony : 我们开始吧,按照例子中所述我们应该有两个线程,分别代表生产者和消费者。按照W. Richard Stevens的指导,我们将我们的互斥锁和我们的共享数据放在一个结构体内。

//数据结构定义
#define MAX_COUNT 100

typedef struct sharedata_t{
 pthread_mutex_t lock;
 int val;
}sharedata_t;

sharedata_t shared = {PTHREAD_MUTEX_INITIALIZER};

//主函数
int main(){
 pthread_t producer;
 pthread_t consumer;

 pthread_create(&producer, NULL, produce, NULL);
 pthread_create(&consumer, NULL, consume, NULL);

 pthread_join(producer, NULL);
 pthread_join(consumer, NULL);

 return 0;
}
这些都很简单,关键的是produce和consume两个线程执行函数。
Alex : 如前面所说,produce和consume在访问shared时候一定要先对lock上锁。
Tony : 没错,在任意时刻都只有一个线程在操纵shared变量。代码如下:

void *produce(void *arg){
 int count = 0;
 for( ; ; ){
  pthread_mutex_lock(&shared.lock);
  if(shared.val == 1){//如果已经生产了我就不生产了,直到消费者消费掉
   pthread_mutex_unlock(&shared.lock);
   continue;
  }
  shared.val = 1;
  count++;
  if(count > MAX_COUNT){
   pthread_mutex_unlock(&shared.lock);
   break;
  }
  printf("the %d th produce\n", count);//线程共享进程stdout缓冲区,如果不加以保护,就会被另一个线程的输出刷新
  pthread_mutex_unlock(&shared.lock);
 }
}

void *consume(void *arg){
 int count = 0;
 for( ; ; ){
  pthread_mutex_lock(&shared.lock);
  if(shared.val == 0){//如果还没生产呢,我就暂时不能消费
   pthread_mutex_unlock(&shared.lock);
   continue;
  }
  shared.val = 0;
  count++;
  if(count > MAX_COUNT){
   pthread_mutex_unlock(&shared.lock);
   break;
  }
  printf("the %d th consume\n", count);
  pthread_mutex_unlock(&shared.lock);
 }
}

Tony : 现在这个程序是正确的,但是却不是理想的,他的输出结果肯定是如下的:
the 1 th produce
the 1 th consume
the 2 th produce
the 2 th consume

the 100 th produce
the 100 th consume

Alex : 是producer和consumer交替对吧。
Tony : 没错!运行一下,你感觉如何呢?
Alex : 好像有些慢!我觉得produce和consume两个函数中关于shared.val的值的轮转测试是比较耗时的,而且每次测试前后都要上锁、解锁。
Tony : 说的没错!像这样的轮询是极其浪费CPU时间的。我们不是没有办法解决的,我们可以利用条件变量的方式来解决它,不过针对这个例子来说,使用条件变量从代码上看理解起来就会有些困难了。
Alex : 继续说!
Tony : 条件变量提供一种等待-唤醒机制,可以这样理解如果消费者发现没有产品,它并不继续轮训,而是睡眠,直到生产者生产出产品,并将之唤醒消费。反过来说也一样,那就是如果生产者发现生产出来的产品还没有被消费者消费掉,就同样睡眠,直到消费者将产品消费掉,并将生产者唤醒生产,这样就省下了大量的CPU时间,性能提升可不是一点半点的。不过我们实现起来的时候要更加小心。
Alex : Just go on!
Tony : 要想使用条件变量,我们还需要定一个结构,用来存放我们的条件。

typedef struct conddata_t{
 pthread_mutex_t lock;
 pthread_cond_t cond;
 int ready;
}conddata_t;

conddata_t condd = {PTHREAD_MUTEX_INITIALIZER, PTHREAD_COND_INITIALIZER, 0};

主函数无需改变,修改后的produce和consume如下,由于不好理解,所以我讲上了不少的注释,可以帮助理解。

void *produce(void *arg){
 int count = 0;

 for( ; ; ){
  pthread_mutex_lock(&condd.lock);
  while(condd.ready == 1)//已生产,还没消费完,此时producer休眠
   pthread_cond_wait(&condd.cond, &condd.lock);
  condd.ready = 0;//消费完,还没生产呢,此时consumer休眠
  pthread_mutex_unlock(&condd.lock);

  pthread_mutex_lock(&shared.lock);
  shared.val = 1;
  count++;
  if(count >= MAX_COUNT){
   pthread_mutex_unlock(&shared.lock);
   condd.ready = 1;//生产者生产完最后一个后退出了,告诉消费者
   pthread_cond_signal(&condd.cond);//告诉消费者可以消费最后一个了
   break;
  }
  printf("the %d th produce\n", count);//线程共享进程stdout缓冲区,如果不加以保护,就会被另一个线程的输出刷新
  pthread_mutex_unlock(&shared.lock);

  pthread_mutex_lock(&condd.lock);
  condd.ready = 1;//生产完了,等待消费
  pthread_mutex_unlock(&condd.lock);
  pthread_cond_signal(&condd.cond);//告诉消费者可以消费了
 }
}

void *consume(void *arg){
 int count = 0;
 for( ; ; ){
  pthread_mutex_lock(&condd.lock);
  while(condd.ready == 0)//没有东西可以消费,消费者休眠
   pthread_cond_wait(&condd.cond, &condd.lock);
  condd.ready = 1;//这在消费,请生产者等待,生产者休眠
  pthread_mutex_unlock(&condd.lock);

  pthread_mutex_lock(&shared.lock);
  shared.val = 0;
  count++;
  if(count >= MAX_COUNT){
   pthread_mutex_unlock(&shared.lock);
   break;
  }
  printf("the %d th consume\n", count);
  pthread_mutex_unlock(&shared.lock);

  pthread_mutex_lock(&condd.lock);
  condd.ready = 0;//告诉生产者消费完了,该生产了
  pthread_mutex_unlock(&condd.lock);
  pthread_cond_signal(&condd.cond);
 }

}

代码更长了。有些东西不必要解释,看看注释认真思考一下就能得到答案的。

Alex : 晓得。
Tony : 这回我们来看看性能,第一个实现cpu占用98% 耗时近10秒,而第二个实现几乎瞬间完成。
Alex : 我还在思考,的确不容易理解。
Tony : 还要注意的是资源的释放,这可是一个重要的问题。你慢慢思考吧,我去喝杯coffee。^_^

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