sed用法

修改服务器上的文本文件最简单的方法莫过于本机修改好传到服务器上. 本机上可用的文本编辑器, 无论是远古的神器VIM和Emacs, 还是现在程序员们中间流行的Sublime Text, 只要掌握, 无一不犀利无比. 然而要是服务器是放在南极运行的, 考虑到卫星通讯让人心痛的资费, 编辑它上面的文本, 唯一好用的工具莫过于鸿蒙初辟时的sed命令了. 下面收录一些使用sed实现文本编辑常用功能的用法, 方便查询.

sed 's/^/insert_content&/' #在每行开头插入
sed 's/^\(.*\)$/insert_content\1/' #在每行开头插入, 土帽用法
sed 's/$/insert_content&/' #在每行结尾插入
sed 's/^\(.*\)$/\1\insert_content/' #在每行结尾插入, 土帽用法
发表在 计算机与网络技术 | 标签为 , , , , | 留下评论

架设自己的VPN和WEB服务器

现在中国共产党对互联网的管制越发的严厉. 近一年来google更是被完全屏蔽, gmail的邮箱也是常常无法登陆. 个人在国内的网站必须去工信部备案, 否则域名不给解析. 如我这样偶尔需要翻墙查查资料, 又喜欢折腾个人网站的, 买个国外的VPS主机架设翻墙的VPN服务器和和个人网站WEB服务器是最好的解决方案。

Windows, Mac OSX和Linux都支持pptp和l2tp这两种VPN协议. 但是国内很多运营商都屏蔽了pptp, 所以VPN服务器还是使用l2tp适用. 我的VPS主机系统安装的是Ubuntu 12.04, 这个系统下面安装和配置l2tp不是很困难. 首先是安装openswan:

sudo apt-get install openswan

编辑/etc/ipsec.conf, 在该文件后面追加如下内容:

conn L2TP-PSK-NAT
        rightsubnet=vhost:%priv
        also=L2TP-PSK-noNAT
 
conn L2TP-PSK-noNAT
        authby=secret
        pfs=no
        auto=add
        keyingtries=3
        rekey=no
        dpddelay=10
        dpdtimeout=90
        dpdaction=clear
        ikelifetime=8h
        keylife=1h
        type=transport
        left=yourSeverIP
        leftprotoport=17/1701
        right=%any
        rightprotoport=17/%any

编辑/etc/ipsec.secrets, 在该文件后面追加如下内容:

yourServerIP %any:   PSK "yourSharedKey"

接下来安装xl2tpd:

sudo apt-get install xl2tpd

编辑/etc/xl2tpd/xl2tpd.conf, 在该文件后面追加如下内容:

[global]
ipsec saref = yes
 
[lns default]
ip range = 10.1.2.2-10.1.2.255
local ip = 10.1.2.1
refuse chap = yes
refuse pap = yes
require authentication = yes
ppp debug = yes
pppoptfile = /etc/ppp/options.xl2tpd
length bit = yes

ip range设置成A, B和C三类私有IP地址都行, ip local同样. 然后创建文件/etc/ppp/options.xl2tpd, 添加如下内容:

require-mschap-v2
ms-dns 8.8.8.8
asyncmap 0
auth
crtscts
lock
hide-password
modem
debug
name l2tpd
proxyarp
lcp-echo-interval 30
lcp-echo-failure 4

编辑文件/etc/ppp/chap-secrets, 在该文件后面追加如下内容:

"username" l2tpd "password" *

开启Linux内核ip包转发功能:

sudo sed -i 's/#net.ipv4.ip_forward=1/net.ipv4.ip_forward=1/g' /etc/sysctl.conf
sysctl -p

最后编辑/etc/rc.local, 添加如下内容:

for each in /proc/sys/net/ipv4/conf/*
do
    echo 0 > $each/accept_redirects
    echo 0 > $each/send_redirects
done
iptables -t nat -A POSTROUTING -j MASQUERADE
iptables -I FORWARD -p tcp --syn -i ppp+ -j TCPMSS --set-mss 1356

重启主机, 输入如下命令:

sudo ipsec verify

如果没有error信息, l2tpd VPN服务就架设成功.

VPS的CPU和内存资源有限, WEB服务器用nginx性能会好一些. 个人网站用的CMS, 比如说WordPressJoomla!, 都还会用到php和mySQL(也就是Linux + Nginx + MySQL + PHP/Perl/Python, LNMP), 还需要安装相应的模块:

sudo apt-get install nginx
sudo apt-get install php5-fpm #fastcgi
sudo apt-get install mysql-server
sudo apt-get install php5-mysql
sudo apt-get install php5-gd #GD模块, CMS的某些gallery插件会用到

编辑/etc/nginx/sites-enabled/default, 在server段里找到location ~ \.php$, 做如下修改:

location ~ \.php$ {
#       fastcgi_split_path_info ^(.+\.php)(/.+)$;
#       # NOTE: You should have "cgi.fix_pathinfo = 0;" in php.ini
#
#       # With php5-cgi alone:
#       fastcgi_pass 127.0.0.1:9000;
#       # With php5-fpm:
fastcgi_pass unix:/var/run/php5-fpm.sock;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}

编辑/etc/php5/fpm/pool.d/www.conf, 找到listen, listen.owner, listen.group和listen.mode等字段, 做如下修改:

;listen = 127.0.0.1:9000 #注释
listen = /var/run/php5-fpm.sock #监听unix socket
 
...
 
listen.owner = www-data #去掉注释
listen.group = www-data #去掉注释
listen.mode = 0660      #去掉注释

重启服nginx和php5-fpm. 创建文件/usr/share/nginx/www/index.php, 内容为:

<?php
    phpinfo();
?>

如果能够看到phpinfo的输出页面, 就表明LNMP安装配置成功.

有些时候希望一台VPS上创建多个不同域名的网站, 可以以/etc/nginx/sites-available/default为模板, 在server段的server_name后面添加域名, root后面添加站点的根目录, 在/etc/nginx/sites-enabled做符号链接即可.

以上架设配置虽然是在Ubuntu 12.04下实现, 应该也能通用于其他的Linux发行版, 可能出现的问题最多是配置文件的目录有所不同. 但是, Ubuntu 14.04下的L2TP VPN似乎按照上面描述的方式架设不成功, 在ipsec verify时会出现错误.

发表在 计算机与网络技术 | 标签为 , , , | 留下评论

再游丽江

两年后的夏天,同样是因为开会我又再次来到丽江。到达的那天晚上重游那熟悉的古城,找到木府旁那家曾经去过的酒家,点上一杯“醉佳人”和一盘烤鱼,望着城中石板路上熙熙攘攘的人群,自斟自饮。这里的一切都没变,甚至酒家老板喂养的那只黄猫仍淘气地在客人们的桌子之间穿梭。虽然位处繁华地段,但是坐着并不觉得吵闹。老板仿佛只有一张音乐碟,反复播放着那几首翻唱的歌曲。时不时的,“你存在我深深的脑海里”,曲婉婷的《我的歌声里》,飘荡在空气中,让我有些触动和忧伤。几乎是整整两年,同游一地,心里当时的忐忑和希望变为现在的决绝和遗忘。让时间和凉风带走我的情感。

发表在 心情与脾气 | 留下评论

从X.org近期的更新说开

最近, X.org公布了一个栈溢出导致登陆普通用户利用这一漏洞获取超级用户权限的bug, 这个bug已经存在X WINDOW中存在长达22年. 产生bug的代码如下:

char charName[100];
 
.....
 
if (sscanf((char *) line, "STARTCHAR %s", charName) != 1) {
   bdfError("bad character name in BDF file\n");
   goto BAILOUT; /* bottom of function, free and return error */
}

这段代码预先在栈空间上申请100个字节的内存空间charName, 之后用sscanf将line字符串中的对应字段拷贝到charName. 代码中, line字符串读取的内容是用户可以指定的. 不怀好意的用户可以让这个字段的字符数大于100, 这样正在运行的X WINDOW程序栈空间的内容就会被修改, 结果可能导致X Window崩溃; 更坏的结果则是因为X Window程序以超级用户执行, 栈溢出后导致该用户运行他编写的恶意程序后获得系统的root权限. 修补这个漏洞的方法很简单, 只要把在格式输入中的"%s"改成"%99s"即可. 查阅sscanf的帮助文档我们会发现, scanf函数对于字符串输入的中止条件是当遇到空格或者超过"%s"提示的长度. 修改成"%99s"后无论line格式如何, sscanf向charName写入的字符数都不会超过99, 这样就避免了爆栈的发生.

作为为了编写操作系统而发明的C语言, 它与其他那些比它更为高级的编程语言一个明显的不同就是它假定程序员的清楚的知道自己编写的每一条语句在操作系统执行时这个层面上的含义. 有些人认为这是C语言的特点, 有些人认为这是C语言的缺陷. 但无论如何, C程序员必须在编写程序的时候时时刻刻都要考虑自己代码中的每一个变量或指针指向的地址都在什么内存的地方(栈, 堆, 只读数据区等等), 改变指针指向的(往往是一块连续的)地址的值的时候, 是否有足够的空间. 使用C库函数的时候尽量避免使用本身设计不合理的函数(如gets); 自己设计函数的时候, 如果形参是指针, 在函数中还要修改指针指向的内容, 函数参数列表中必须有(显式或隐式)指定指针指向合法地址的长度的参量.

发表在 计算机与网络技术 | 标签为 | 留下评论

漠河

由于需要在低温环境下测试设备,2013年年底出差去了一趟漠河。这是我第一次去东北,去的就是东北最遥远的地方。从北京到漠河只有一班飞机,途中还要经停哈尔滨。在一年最冷的时候去那里的人很少,飞机从哈尔滨起飞后机舱里只是稀稀拉拉的座着几个人。漠河是在中国大陆少有的几个有民用机场的县市,机场甚至比跟腾冲的那个还小,起飞和降落都共用一条跑道。我们到达的时候已经下起小雪,停机坪上停着一架螺旋桨小飞机。


我们的测试观测站在一个叫北极村的地方,距离机场大约80公里,它是中国地理位置最北的村落。漠河地广人稀,从机场到北极村除了路过一个林场就没有别的村子。公路在大兴安岭的群山中蜿蜒穿过。山头都不高,满处覆盖着笔挺的桦树和松树。黄宇仁在他那本风靡一时的《万历十五年》中很多处提到白山黑水,黑水没能看到,白山在这个季节非常真实。

我们住在漠河气象站下属的一个宾馆里,宾馆住宿条件还不错,房间很大,有暖气和热水。观测室离宾馆很近,穿过几户人家就到了。观测室是我们租来的一栋刚建好的二层楼房,房间的设置看起来是准备当做一个旅馆用的。但是还没有通上暖气和供水,只能靠几个电加热器取暖。观测室出去走几步能到黑龙江上,隔江就是老毛子的地盘。江面早已结冰,能够从冰上走到对岸。但是我们一到当地人就告诫我们不要越过江心边界,据说越界被俄罗斯边防军抓住后会根据当地法律判刑半年(实际上是一年半,俄罗斯坐牢也是按工作日来算,一天八小时^_^)。大晴天在江面上走动,寒冷的气温和湛蓝的天空,颇有些我在南极的感觉。



可能是去过南极的缘故,对在漠河这里的冰雪风景并不特别的欣赏,留下的照片不多,从中挑几张我觉得很过得去的。




发表在 声色犬马 | 留下评论

UNIX网络编程从零起步(7): 讨论

前面六个小结文章基本上涵盖了UNIX网络编程(只针对TCP协议)的基本要点. 尽管所给的例程都可以编译运行, 然而它们仅仅只是个雏形, 和真正能够部署使用还有很长的距离.

高性能

我们在第六小结使用epoll和kqueue实现了非阻塞的服务器端, 并声称其性能最佳. 然而真正要让它实际运行的性能最佳, 我们需要精细的调整它的运行参数. 这里我们说的性能包括两个方面: 能够同时快速处理大量连接(high performance); 有很强的可扩展性(scalability), 例如当服务器CPU/core增加时程序性能也成正比增加.

线程VS进程

我们在5, 6两个小结都用到了pre-threaded的手段来消除创建线程的延迟. 既然延迟已经被消除, 那么是不是用进程也可以?确实可以用进程, 实际上像目前非常流行的nginx这样的轻量级httpd服务器使用的就是多进程而非多线程. 用多进程最明显的好处就是每个进程相互独立, 当出现突发因素导致某个工作进程异常退出时, 不会影响到其他工作线程. 如果对服务器程序的稳定性有严格的要求, 使用多进程给编程带来的负担要小一些. 提供http服务需要处理各种复杂的事务而事务之间少有进程间通讯, 也难怪主流的web服务器如apache和nginx都采用多进程. 多线程的好处是线程间通讯方便. 我们打开日志文件虽然使用了O_APPEND标志保证写入的原子性, 但是如果要添加日志回滚(删除或者重命名旧的日志文件)功能且保证日志不会丢失, 这就必然会涉及到工作任务之间同步的问题. 这是我使用多线程的原因.

多少个线程

到写这篇文章的2013年, 摩尔定律仍然成立, CPU芯片上得晶体管数量依旧成倍增长. 尽管CPU的主频较之10年前几乎没有变化, 但是一块芯片上包含了更多的核. 个人PC上四核已成标配, 服务器上拥有两颗八核CPU也很寻常. 多线程的目的就是为了充分利用这些核心来达到更好的性能. 运行我们程序的操作系统是通用分时(time-shared)系统, 所以开启线程的数目将直接影响到程序的性能. 和GPU的构架不同, CPU通用寄存器数目很少, 即便是线程其正文切换(context switch)开销也很可观. 开启过多线程, 大量计算时间消耗在正文切换上, 反倒降低了程序的性能. 显然, 程序所在主机上有多少个CPU核(intel的超线程不计算在内)就开启多少个线程是最好的选择.

操作系统上并非只运行我们的程序, 线程切换无法避免. 线程切回运行时, 在默认设置下系统不保证线程仍在上次运行时所在的CPU/core上运行. 这样设置的道理是操作系统无法获知程序运行多长时间. 假设在一台双核主机上, 同时A, B和C3个进程, A和B是后台守护进程, C是一个只运行几分钟的进程. 由于操作系统不知道它们的确切运行时间, 有可能将A和B分配到一个core上, C分配到另一个core上. 如果操作系统默认core与进程绑定, 那么几分钟之后主机CPU上的一个core处于繁忙状态, 而另一个core则处于闲置状态. 显然这种进程调度不是有效的, 操作系统默认不将core与进程绑定的原因也在于此.

现代的CPU都有缓存, 如果仍在原来的core上运行, 只需从L1或者L2缓存上读取线程正文. 但是如果更换运行core, 那就需要通过多核共享的L3缓存读取正文, 从而降低程序性能. 如果主机上有多个CPU, 恢复后再另外一个CPU的某个core上运行, 则要走主板总线, 情况会更加糟糕. 为了获得更高的性能, 我们还需要将工作线程跟core绑定. Linux提供shed_setaffinity和pthread_setaffinity_np来将线程和core绑定, BSD系列UNIX目前还没这样的接口.

负载均衡

为了最大限度的利用服务器计算机CPU, 服务器程序还需要能将客户发起的连接均匀的分配到工作线程上. 在非阻塞的server5.c的程序中, 当lfd为可读时, 其中一个工作线程会循环调用accept直到返回-1并且errno为EAGAIN为止. 相比于只调用一次accept, 这样做的好处是当发起大量连接时, 减少epoll_wait的调用次数和缩短服务器响应时间. 从短时间片上来说可能会导致某个工作线程比其他线程要繁忙, 但是从长时间间隔上来说工作线程之间的负载大致相当.

惊群与加锁

在server5.c中, 为了防止多个线程同时accept一个lfd, 在accept前后用了互斥锁. 多线程编程为了提高程序速度, 往往对线程锁的使用格外小心, 甚至还会设计一些无锁的数据结构. 我们的程序仍然使用互斥锁, 原因是: 我们开启的线程的数目并不多, 多个线程竞争同一个锁的情况并不多; 当线程处理并发连接数目很多时, 在一个循环内, 大部分的时间用在处理连接上, 加锁解锁的时间只占其中很小的一部分; 此外, 多个线程同事accept一个lfd, 虽然现代很多操作系统都会让其中一个返回成功, 其他失败(非阻塞)或者等待, 但是这只不过是把把矛盾转嫁到内核里, 相比于使用汇编原子操作实现的用户态锁(例如Linux的futex), 性能反而大大不如.

发表在 计算机与网络技术 | 标签为 , , | 留下评论

UNIX网络编程从零起步(6): 高并发非阻塞服务器端

尽管我们已经通过pre-threaded的手段提进一步提高了并发服务器端程序的性能, 但是它们仍然不适用于需要高并发的环境. 由于采用的是同步I/O的模型, 线程池里的每个工作线程一次迭代只能处理一个连接. 但是日志记录服务器又要提供长连接功能, 这就导致当长连接数超过预先定义的工作线程数目时, 服务器无法处理之后的连接. 增加线程数目除了耗费多的内存并不能解决这个问题. 唯一的解决办法就是使用非阻塞的I/O模型, 让工作线程在一次迭代中能处理很多的连接.

检查server4.c中的work_thr函数, 我们发现导致线程阻塞的两个地方是调用accept和read(暂时不考虑write可能导致的阻塞, 认为cfd永远都是write ready的). 因此必须将lfd和cfd都设置为NONBLOCK. POSIX提供I/O复用的两个接口select和poll. 前面提到过多个线程同时调用select监视同一个fd会带来更严重问题. 此外, 无论是select还是poll, 其性能随着监视文件描述符的增加而急剧下降(O(N2)). 要编写能够同时处理大量连接(~10k+)的服务器, 用POSIX提供的接口是无法满足这样的要求的.所幸的是基本上UNIX/UNIX-like的操作系统几乎都各自提供了系统相关的I/O复用系统调用, 如freeBSD的kqueue, Solaris的/dev/poll, Linux的epoll, 也有将这些系统相关的接口包装起来的库, 如libevent. 我们的终极服务器端程序直接采用epoll, 当然它也只能运行在LINUX的操作系统上(内核版本大于等于2.6).

服务器server5.c代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
#include "server.h"
#include <sys/epoll.h>
 
#define MAXEVENTS 10000
#define NTHREADS 4
 
int fd;
int lfd;
pthread_mutex_t lfd_mtx = PTHREAD_MUTEX_INITIALIZER;
 
void *work_thr(void *);
 
int
main(int argc, char *argv[])
{
    int i;
    char *logpath = (char *) malloc(PATHSIZE);
    char *servname = (char *) malloc(SERVNAMESIZE);
    pthread_t tid[NTHREADS];
    int flags;
 
    if (argc == 1) {
        snprintf(logpath, PATHSIZE, "server.log");
        snprintf(servname, SERVNAMESIZE, "5000");
    } else if (argc == 2) {
        snprintf(logpath, PATHSIZE, "server.log");
        snprintf(servname, SERVNAMESIZE, "%s", argv[1]);
    } else if (argc == 3) {
        snprintf(logpath, PATHSIZE, "%s", argv[1]);
        snprintf(servname, SERVNAMESIZE, "%s", argv[2]);
    } else {
        fprintf(stderr, "Usage:\n");
        fprintf(stderr, "%s [log file path] [server port]\n", argv[0]);
        exit(EXIT_FAILURE);
    }
 
    if ((fd = open_logfile(logpath)) < 0) {
        fprintf(stderr, "open_logfd error.\n");
        exit(EXIT_FAILURE);
    }
 
    if ((lfd = tcp_listen(NULL, servname, NULL, NULL)) < 0) {
        fprintf(stderr, "tcp_listen error.\n");
        exit(EXIT_FAILURE);
    }
 
    flags = fcntl(lfd, F_GETFL);
    fcntl(lfd, F_SETFL, flags | O_NONBLOCK);
 
    for (i = 0; i < NTHREADS; i++)
        pthread_create(&tid[i], NULL, work_thr, NULL);
 
    for (i = 0; i < NTHREADS; i++) 
        pthread_join(tid[i], NULL);
 
    free(logpath);
    free(servname);
    return 0;
}
 
void *
work_thr(void *arg)
{
    int cfd, efd;
    ssize_t n;
    int epfd;
    struct epoll_event event;
    struct epoll_event *events = (struct epoll_event *) malloc(sizeof(struct epoll_event));
    char *logbuf = (char *) malloc(LOGSIZE);
    int ret;
    int i;
    int flags;
    int ncfds = 0;
    int timeout;
 
    epfd = epoll_create(10000);
    event.data.fd = lfd;
    event.events = EPOLLIN | EPOLLONESHOT;
    epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &event);
 
    for (; ;) {
        if (ncfds == 0)
            timeout = 10000000;
        else
            timeout = 1;
 
        ret = epoll_wait(epfd, events, MAXEVENTS, timeout);
        for (i = 0; i < ret; i++) {
            efd = events[i].data.fd;
            if (lfd == efd) {
                pthread_mutex_lock(&lfd_mtx);
                for (; ;) {
                    if ((cfd = accept(lfd, NULL, NULL)) >= 0) {
                        flags = fcntl(cfd, F_GETFL);
                        fcntl(cfd, F_SETFL, flags | O_NONBLOCK);
                        event.data.fd = cfd;
                        event.events = EPOLLIN;
                        epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &event);
                        ncfds++;
                    } else {
                        if (errno == EAGAIN || errno == EWOULDBLOCK) {
                            break;
                        }
                    }
                }
                event.data.fd = lfd;
                event.events = EPOLLIN | EPOLLONESHOT;
                epoll_ctl(epfd, EPOLL_CTL_MOD, lfd, &event);
                pthread_mutex_unlock(&lfd_mtx);
            } else {
                cfd = efd;
                memset(logbuf, '\0', LOGSIZE);
                n = readn(cfd, logbuf, LOGSIZE);
                if (n == LOGSIZE) {
                    write(fd, logbuf, strlen(logbuf));
                } else {
                    ncfds--;
                    epoll_ctl(epfd, EPOLL_CTL_DEL, cfd, &event);
                    close(cfd);
                }
            }
        }
    }
 
    free(events);
    free(logbuf);
    close(epfd);
    return NULL;
}

主函数main和server4.c一模一样, 只是在第47行将lfd设置成非阻塞模式. 当客户端发起连接时, lfd会被标记为可读, 因此在第78行添加监视lfd可读. 同时也设置了EPOLLONESHOT, 这是因为epoll默认水平触发(level-triger), 只要有一个连接发起, 不管后续是否还有连接, lfd永远可读. EPOLLONESHOT让lfd可读事件触发一次后就从epoll中撤销(不是删除). epoll通知有事件发生后, 从第88行开始处理事件. 如果有连接发生, 那么我们就调用accept. 需要提醒的是, 从epoll返回(87行)到探测到有连接发起(90行), 可能连接被发起了很多次, 为了不丢掉任何一个发起的连接, 在第92行需要循环调用accept, 直到完成所有的连接, 也就是accept非阻塞lfd返回-1并且errno被设置为EAGAIN或者EWOULDBLOCK为止. 同时, 如果cfd不小于0, 把cfd也把设置成非阻塞并加到epoll监视中(94行). cfd同样也是默认水平触发, 这是因为要支持长连接. 如果采用边缘触发, 就要一直读到客户端终止连接, 这又回到了阻塞模型上去了. accept完了不要忘了lfd重新加到epoll中(108行, 注意不是EPOLL_CTL_ADD而是EPOLL_CTL_MOD). epoll同样有惊群现象, 它的帮助手册明确说明如果多个线程用epoll同时监视同一个fd的同一个事件, 有当事件发生时, 所有的epoll_wait都会返回. 这里, 我们忽略掉epoll的惊群, 但是为了性能, 仍然在accept上加锁. 处理客户端连接部分比较简单, 可读的时候就读取, 客户端连接关闭, 服务器端也被动关闭. 在第118行将要关闭的cfd从epoll中删除. 这步并不是必须, 因为关闭一个文件描述符后, 它会被自动从epoll重删除.
编译server5.c

gcc server5.c tcp_listen.c readn.c open_logfile.c -lpthread -Wall -O2 -o serv5

至此我们最后的多线程异步日志记录服务器程序完成. 这个程序是在目前操作系统运行的最高性能的服务器端. 接下来要做的事情只是在它的基础上添添补补, 加一些辅助的功能让它更为实用一些.

此外, 日志一部服务器程序也用kqueue实现, 并在MAC OS X系统下测试, server5b.c:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
#include "server.h"
#include <sys/event.h>
#include <sys/types.h>
#include <sys/time.h>
 
#define MAXEVENTS 10000
#define NTHREADS 4
 
#define IN_USE     1
#define NOT_IN_USE 0
 
int fd;
int lfd;
pthread_mutex_t lfd_mtx = PTHREAD_MUTEX_INITIALIZER;
 
void *work_thr(void *);
 
int
main(int argc, char *argv[])
{
    int i;
    char *logpath = (char *) malloc(PATHSIZE);
    char *servname = (char *) malloc(SERVNAMESIZE);
    pthread_t tid[NTHREADS];
    int flags;
 
    if (argc == 1) {
        snprintf(logpath, PATHSIZE, "server.log");
        snprintf(servname, SERVNAMESIZE, "5000");
    } else if (argc == 2) {
        snprintf(logpath, PATHSIZE, "server.log");
        snprintf(servname, SERVNAMESIZE, "%s", argv[1]);
    } else if (argc == 3) {
        snprintf(logpath, PATHSIZE, "%s", argv[1]);
        snprintf(servname, SERVNAMESIZE, "%s", argv[2]);
    } else {
        fprintf(stderr, "Usage:\n");
        fprintf(stderr, "%s [log file path] [server port]\n", argv[0]);
        exit(EXIT_FAILURE);
    }
 
    if ((fd = open_logfile(logpath)) < 0) {
        fprintf(stderr, "open_logfd error.\n");
        exit(EXIT_FAILURE);
    }
 
    if ((lfd = tcp_listen(NULL, servname, NULL, NULL)) < 0) {
        fprintf(stderr, "tcp_listen error.\n");
        exit(EXIT_FAILURE);
    }
 
    flags = fcntl(lfd, F_GETFL);
    fcntl(lfd, F_SETFL, flags | O_NONBLOCK);
 
    for (i = 0; i < NTHREADS; i++)
        pthread_create(&tid[i], NULL, work_thr, NULL);
 
    for (i = 0; i < NTHREADS; i++) 
        pthread_join(tid[i], NULL);
 
    free(logpath);
    free(servname);
    return 0;
}
 
int
find_idx(const struct kevent *changelist, int nchanges, int ident)
{
    int i, ret = nchanges;
    for (i = 1; i < nchanges; i++) {
        if (changelist[i].ident == ident) {
            ret = i;
            break;
        }
    }
    return ret;
}
 
void *
work_thr(void *arg)
{
    int cfd, kfd;
    ssize_t n;
    int kqfd;
    struct kevent *changelist = (struct kevent *) malloc(sizeof(struct kevent) * MAXEVENTS);
    struct kevent *eventlist = (struct kevent *) malloc(sizeof(struct kevent) * MAXEVENTS);
    char *logbuf = (char *) malloc(LOGSIZE);
    int ret;
    int i;
    int flags;
    int ncfds = 0;
    struct timespec timeout;
 
    int idx;
    kqfd = kqueue();
    for (i = 0; i < MAXEVENTS; i++)
        EV_SET(&changelist[i], lfd, EVFILT_READ, EV_ADD | EV_ONESHOT, EV_ADD | EV_ONESHOT, 0, NULL);
 
    for (; ;) {
        if (ncfds == 0) {
            timeout.tv_sec = 10000;
            timeout.tv_nsec = 0;
        }
        else {
            timeout.tv_sec = 1;
            timeout.tv_nsec = 1000000;
        }
 
        ret = kevent(kqfd, changelist, MAXEVENTS, eventlist, MAXEVENTS, &timeout);
 
        for (i = 0; i < ret; i++) {
            kfd = eventlist[i].ident;
            if (lfd == kfd) {
                pthread_mutex_lock(&lfd_mtx);
                for (; ;) {
                    if ((cfd = accept(lfd, NULL, NULL)) >= 0) {
                        flags = fcntl(cfd, F_GETFL);
                        fcntl(cfd, F_SETFL, flags | O_NONBLOCK);
                        ncfds++;
                        idx = find_idx(changelist, MAXEVENTS, lfd);
                        EV_SET(&changelist[idx], cfd, EVFILT_READ, EV_ADD, 0, 0, NULL);
                        if (ncfds > MAXEVENTS - 1)
                            break;
                    } else {
                        if (errno == EAGAIN || errno == EWOULDBLOCK || errno == ECONNABORTED) {
                            break;
                        }
                    }
                }
                EV_SET(&changelist[0], lfd, EVFILT_READ, EV_ADD | EV_ONESHOT, 0, 0, NULL);
                pthread_mutex_unlock(&lfd_mtx);
            } else {
                cfd = kfd;
                memset(logbuf, '\0', LOGSIZE);
                n = readn(cfd, logbuf, LOGSIZE);
                if (n == LOGSIZE) {
                    write(fd, logbuf, strlen(logbuf));
                } else {
                    ncfds--;
                    idx = find_idx(changelist, MAXEVENTS, cfd);
                    EV_SET(&changelist[idx], cfd, EVFILT_READ, EV_DELETE, 0, 0, NULL);
                    close(cfd);
                    EV_SET(&changelist[idx], lfd, EVFILT_READ, EV_ADD | EV_ONESHOT, 0, 0, NULL);
                }
            }
        }
    }
 
    free(changelist);
    free(eventlist);
    free(logbuf);
    close(kqfd);
    return NULL;
}

kqueue的功能要比epoll丰富一些, 但是API也要复杂一些. 和epoll一样, kqueue默认水平触发(flags字段设置EV_CLEAR则为边缘触发), 也支持ONESHOT. 大体上, server5.c和server5b.c的代码几乎一样. 但是由于kevent(等价于epoll_wait)需要把监视事件列表做为数组(一块连续的内存区域)作为输入, 而明显的由于客户端断开连接是乱序的, 无法保证输入changlist是连续. 解决办法就是客户端cfd断开后用lfd填充(见143行). 能够用这种方法的原因是, 在changelist中如果有多个元素有相同的(ident, filter)对, kevent把他们当做同一个时间处理, 并且只返回最后注册的那个. epoll的API在这点上设计比kqueue要合理一些, kqueue应该设置一个类似kqueue_ctl的接口来注册和注销监视事件.

发表在 计算机与网络技术 | 标签为 , , | 留下评论

UNIX网络编程从零起步(5): 预先创建线程(pre-threaded)服务器端之子线程accept

在主线程里accept并由主线程调度子线程, 这个有点像本来是条8车道德高速路中间一段硬是弄成单车道. 能不能主线程只是创建线程池, 让工作线程自己去accept呢? 这个当然可以, 我们的第四个服务器模型代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
#include "server.h"
 
#define MAXCLI 512
#define NTHREADS 128
 
int fd;
int lfd;
pthread_mutex_t lfd_mtx = PTHREAD_MUTEX_INITIALIZER;
 
void *work_thr(void *);
 
 
int
main(int argc, char *argv[])
{
    int i;
    char *logpath = (char *) malloc(PATHSIZE);
    char *servname = (char *) malloc(SERVNAMESIZE);
    pthread_t tid[NTHREADS];
 
    if (argc == 1) {
        snprintf(logpath, PATHSIZE, "server.log");
        snprintf(servname, SERVNAMESIZE, "5000");
    } else if (argc == 2) {
        snprintf(logpath, PATHSIZE, "server.log");
        snprintf(servname, SERVNAMESIZE, "%s", argv[1]);
    } else if (argc == 3) {
        snprintf(logpath, PATHSIZE, "%s", argv[1]);
        snprintf(servname, SERVNAMESIZE, "%s", argv[2]);
    } else {
        fprintf(stderr, "Usage:\n");
        fprintf(stderr, "%s [log file path] [server port]\n", argv[0]);
        exit(EXIT_FAILURE);
    }
 
    if ((fd = open_logfile(logpath)) < 0) {
        fprintf(stderr, "open_logfd error.\n");
        exit(EXIT_FAILURE);
    }
 
    if ((lfd = tcp_listen(NULL, servname, NULL, NULL)) < 0) {
        fprintf(stderr, "tcp_listen error.\n");
        exit(EXIT_FAILURE);
    }
 
    for (i = 0; i < NTHREADS; i++)
        pthread_create(&tid[i], NULL, work_thr, NULL);
 
    for (i = 0; i < NTHREADS; i++)
        pthread_create(tid[i], NULL, work_thr, NULL);
 
    free(logpath);
    free(servname);
    return 0;
}
 
void *
work_thr(void *arg)
{
    int cfd;
    ssize_t n;
 
    char *logbuf = (char *) malloc(LOGSIZE);
 
    for (; ;) {
        pthread_mutex_lock(&lfd_mtx);
        cfd = accept(lfd, NULL, NULL);
        pthread_mutex_unlock(&lfd_mtx);
        for (; ;) {
            memset(logbuf, '\0', LOGSIZE);
            n = readn(cfd, logbuf, LOGSIZE);
            if (n != LOGSIZE && n != 0) {
                fprintf(stderr, "readn error.\n");
                break;
            } else if (n == 0) {
                fprintf(stderr, "connection is closed.\n");
                break;
            } else { 
                write(fd, logbuf, strlen(logbuf));
            }
        }
        close(cfd);
    }
    free(logbuf);
    return NULL;
}

看起来server4.c的代码长度比server3.c的要短, 而且也没有用到队列数据结构, 为什么之前要说server3.c是最简单的pre-threaded模型呢? 这是因为我们需要更深的了解accept才能写出正确, 可移植和高效的代码. 在66行accept之前, 我们用现成互斥锁锁住accept. 既然POSIX要求accept是现成安全的, 为什么还需要加锁? 这就涉及到多个线程/进程并发accept同一个侦听端口时accept的语义. BSD系列UNIX的accept是系统调用, 可以看做原子操作. 多个线程阻塞在同一个侦听端口, 当有连接发生时有且只有一个线程返回, 其余的线程仍然保持阻塞. 但是System V系列(可能包括早期的Linux)accept是用其他系统调用实现的库函数, 不是原子操作. 同样的情况发生时, 所有的线程的accept都会返回-1, 并设置errno为EPROTO. 即便是BSD的实现, 实际上操作系统会唤醒所有accept线程, 然后让一个线程accpet返回, 其他线程继续阻塞. 这个现象称作”惊群”(thundering herd). 相比于互斥锁(很多操作系统上是在用户空间实现), 让操作系统内核去调度, 其实更消耗CPU资源. 所以无论是为了可移植还是为了效率, 给accept加一把锁都是最佳的选择.

对UNIX系统编程有些了解的人或许还会问, 那为什么不在lfd上用select和poll这样的I/O复用模型? 确实, select和poll也是线程安全的, 并且当连接发生时lfd会被标记为可读. 然而用I/O复用技术只不过将惊群由accept转嫁给select而已. 更为致命的是, POSIX规范里并未提及当多个线程/进程同时select同一个fd的语义. 或许有些select的实现只是跟accept一样唤醒一个线程其余继续阻塞, 有些实现所有线程都返回成功但是用IS_FDSET检查只有一个线程lfd被设置, 也有可能有些实现select都返回错误. 一般来说, 最好不要用select来检查lfd是否可读.

发表在 计算机与网络技术 | 标签为 , , | 留下评论

UNIX网络编程从零起步(4): 预先创建线程(pre-threaded)服务器端之主线程accept

虽然per thread的并发模型对中小程度的应用来说是足够, 但是我们需要考虑更大规模的应用. 很显然, 每个连接都创建一个线程, 就会有创建线程的延迟. 当链接数目过多时, 这种延迟就显现出来了. 而且每一个线程在默认下系统都会分配2M(32位的Linux系统如此)的线程栈空间, 当服务器内存有限时, 服务器也不可能同时运行太多的线程.

延迟的问题, 解决的办法就是在客户端发起连接之前就预先创建好一个工作线程池, 当有连接发生时, 线程池中的一个线程来处理这个连接. 我们的第一个pre-threaded的模型是最简单的主线程accept, 然后将cfd分配一个工作线程. 代码如下:
server3.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
#include "server.h"
 
#define MAXCLI 512
#define NTHREADS 128
 
int fd;
int cfd_queue[MAXCLI], head = 0, tail = 0;
pthread_mutex_t cfd_queue_mtx = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cfd_queue_cond = PTHREAD_COND_INITIALIZER;
 
void *work_thr(void *);
 
 
int
main(int argc, char *argv[])
{
    int i;
    int lfd, cfd;
    char *logpath = (char *) malloc(PATHSIZE);
    char *servname = (char *) malloc(SERVNAMESIZE);
    pthread_t tid;
 
    if (argc == 1) {
        snprintf(logpath, PATHSIZE, "server.log");
        snprintf(servname, SERVNAMESIZE, "5000");
    } else if (argc == 2) {
        snprintf(logpath, PATHSIZE, "server.log");
        snprintf(servname, SERVNAMESIZE, "%s", argv[1]);
    } else if (argc == 3) {
        snprintf(logpath, PATHSIZE, "%s", argv[1]);
        snprintf(servname, SERVNAMESIZE, "%s", argv[2]);
    } else {
        fprintf(stderr, "Usage:\n");
        fprintf(stderr, "%s [log file path] [server port]\n", argv[0]);
        exit(EXIT_FAILURE);
    }
 
    if ((fd = open_logfile(logpath)) < 0) {
        fprintf(stderr, "open_logfd error.\n");
        exit(EXIT_FAILURE);
    }
 
    if ((lfd = tcp_listen(NULL, servname, NULL, NULL)) < 0) {
        fprintf(stderr, "tcp_listen error.\n");
        exit(EXIT_FAILURE);
    }
 
    for (i = 0; i < NTHREADS; i++)
        pthread_create(&tid, NULL, work_thr, NULL);
 
    for (;;) {
        cfd = accept(lfd, NULL, NULL);
        pthread_mutex_lock(&cfd_queue_mtx);
        cfd_queue[tail] = cfd;
        if (++tail == MAXCLI)
            tail = 0;
        if (tail == head) {
            fprintf(stderr, "socket connecting queue is full.\n");
            exit(EXIT_FAILURE);
        }
        pthread_cond_signal(&cfd_queue_cond);
        pthread_mutex_unlock(&cfd_queue_mtx);
    }
    free(logpath);
    free(servname);
    return 0;
}
 
void *
work_thr(void *arg)
{
    pthread_detach(pthread_self()); 
    int cfd;
    ssize_t n;
 
    char *logbuf = (char *) malloc(LOGSIZE);
 
    for (; ;) {
        pthread_mutex_lock(&cfd_queue_mtx);
        while (head == tail)
            pthread_cond_wait(&cfd_queue_cond, &cfd_queue_mtx);
        cfd = cfd_queue[head];
        if (++head == MAXCLI)
            head = 0;
        pthread_mutex_unlock(&cfd_queue_mtx);
 
        for (; ;) {
            memset(logbuf, '\0', LOGSIZE);
            n = readn(cfd, logbuf, LOGSIZE);
            if (n != LOGSIZE && n != 0) {
                fprintf(stderr, "readn error.\n");
                break;
            } else if (n == 0) {
                fprintf(stderr, "connection is closed.\n");
                break;
            } else { 
                write(fd, logbuf, strlen(logbuf));
            }
        }
        close(cfd);
    }
    free(logbuf);
    return NULL;
}

线程是在连接之前创建, 所以在第49行和per thread不一样, cfd不可能在创建线程的时候通过pthread_create传递. 在第52行连接发生的时候, 主线程必须要将处理连接委派给一个工作子线程. 我们希望大致上先发起的连接先处理, 所以通过全局的共享队列的数据结构来委派任务最为合适. 我们使用最简单的用数组实现队列(见第7行). 主线程是生产者, 向cfd队列里添加任务;线程池的工作线程是消费者, 从cfd队列里取出任务并处理. 当队列为空时, 子线程就挂起等待新的任务(见81行); 而当连接发起时, 若还有子线程挂起, 主线程只唤醒其中的一个(见61行).

编译server3.c

gcc server3.c tcp_listen.c readn.c open_logfile.c -lpthread -Wall -O2 -o serv3

用测试脚本测试结果与per thread类似.

发表在 计算机与网络技术 | 标签为 , , | 留下评论

纪念Denis Ritchie

两年前的10月份,IT业界先后失去了两个重要的人物。第一个不用多说,他就是苹果公司的创始人之一Steve Jobs。伴随着iPhone 4和iPad 1在中国流行,Steve几乎成为在太平西岸的这个古老的国家里家喻户晓的传奇。Steve以其敏锐地艺术直觉和天才的商业眼光, 不仅让苹果这个在他离开后奄奄一息的公司成为市值最高、拥有现金流最多的公司,更是开辟了一个崭新的电子消费产业模式。而第二个,却是除了IT界其他行业鲜有人知的一个先驱,他就是UNIX操作系统和C语言的发明人,1983年图灵奖的获得者Denis Ritchie。

Denis的传奇始于1969年Multics项目失败。尽管贝尔实验室退出了这个项目,Denis和他的同事Ken Thompson还是在Multics的设计理念上做出了一些必要的简化,开发出了一个至今影响深远的操作系统UNIX。UNIX最早用B语言开发,很快Denis和Ken意识到B语言在操作系统开发上的局限性。1972年他们发明了C,意思是B语言的后继,他们用C语言重写了UNIX。从他们开始,所有能够用的操作系统,包括Windows、Mac OSX和Linux,它们的内核都是用C和少量汇编写成。尽管C语言是为了编写操作系统而发明,但它的简单和高效,使它的用途不仅仅在编写操作系统上。所有与操作系统紧密交道、系统资源有限、需要程序性能的地方,都能够见到C语言的影子。直到现在C语言仍在编程语言排名中稳居前三。C之后的编程语言,C++、java、javascript和PHP等等,其语法和表达无不源于C。A Brief, Incomplete, and Mostly Wrong History of Programming Languages这篇趣文中介绍λ算子和Pascal语言的时候都说到评论家批评他们都是“it is insufficiently C-like”,但是“This criticism occurs in spite of the fact that C has not yet been invented”!由此可见C语言影响之大,以至于后来发明一门新的编程语言,语法像不像C都成为判断这门语言好坏的标准了。

与Steve不同,Denis为人低调,一生都在贝尔实验室就职,直到实验室被阿尔卡特收购才退休。Denis终生未婚。当他的老朋友在2011年10月12日去新泽西州他所住的公寓拜访他时,才发现他已过世。Denis生前不为外人所知,逝去也是默默无闻。然而他对人类社会的贡献远远大于他所得的荣誉。我并不想比较Steve和Denis谁对社会贡献更大,只是想缅怀这位令人尊敬的逝者并向和Denis一样的那些至今活跃在一线的先驱们致敬。

发表在 计算机与网络技术 | 留下评论