UNIX网络编程从零起步(3): 一连接一线程(per thread)服务器端

交互式的服务器端几乎毫无实用价值. 通常情况提供网络服务的端口都会允许多个客户端“同时”发起连接, 这就涉及到并发(concurrence)这个概念. 通过创建进程或线程, 我们都可以实现并发. 创建一个新的进程, 系统会分配给子进程一个独立的虚拟地址空间, 尽管有写时拷贝(copy-on-write, COW), fork新的进程仍然会耗费可观的CPU时间. 同时, 父进程还需要处理SIGCHLD信号. 当接收到这个信号时必须调用wait系列系统调用, 否则可能会产生大量的僵尸进程, 最后导致炒作系统崩溃. 此外, 如果父进程与子进程或者子进程之间需要通讯, 就会用到进程通讯机制, 带来编程上的复杂性. 线程之间共享虚拟地址空间, 创建一个线程比创建一个进程消耗的资源少得多, 线程之间可以通过全局变量以及堆内存通讯(实际上栈上的内存也是共享, 只是一个线程无法知道另外一个线程在栈上的变量何时会出栈, 所以线程间从来不用栈内存通讯). 多线程并发编程比多进程并发编程在某种程度上来说要容易. 当然多线程编程要小心的对付的是如何用线程同步锁和防止死锁. 接下来我都会用多线程实现并发. 这些程序只要稍作修改就能用作多进程的并发.

先从最简单的per therad模型入手. 主线程循环accpet, 将cfd传递到子线程, 子线程读取连接发送来的消息并保写入到记录文件中. 需要做日志记录的程序往往都是持续长久运行的程序, 如果只是记录一条日志就关闭连接, 下一条日志要记录又重新发起连接, 这会相当浪费资源. 所以, 在我们的子线程中, 客户端关闭连接, 也就是read返回0时, 子线程才会退出.
server2.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
#include "server.h"
 
int fd;
 
void *work_thr(void *);
 
int
main(int argc, char *argv[])
{
    int lfd;
    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 (;;) {
        int *cfd;
        cfd = (int *) malloc(sizeof(int));
        *cfd = accept(lfd, NULL, NULL);
        pthread_create(&tid, NULL, work_thr, cfd);
    }
    free(logpath);
    free(servname);
    return 0;
}
 
void *
work_thr(void *arg)
{
    pthread_detach(pthread_self()); 
    int cfd = *(int *)arg;
    free(arg);
    ssize_t n;
 
    char *logbuf = (char *) malloc(LOGSIZE);
 
    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;
}

我们的第一个并发服务器端在主线程中调用accept, 然后将cfd传递给子线程, 由子线程接收客户端发送的消息并写入日志文件. 第44行创建线线程时给子线程传递的cfd是预先通过malloc分配的内存地址, 而非声明一个在栈上的int型的变量讲变量地址传递给子线程. 其中的原因是如果采用后者的方式传递参数, 在第55行赋值之前如果主线程accept再次返回, arg的内容就会更新, 导致丢掉了一个连接和资源泄露. 工作线程work_thr中处理连接的代码和交互式服务器端处理处理连接的代码基本相同. 需要提醒的是, 作为一个合格的C程序员要时刻记得只要在任何地方向操作系统申请了资源, 例如堆上的内存, 打开了文件,开启了新的线程/进程, 都要在适当的地方释放掉这些资源, 尤其是在申请和释放不在同一个函数中时. 我们54行detache了工作线程(如果不detache, 则要记得在合适的线程中join), 在56行free了42行分配的内存, 在74行关闭掉了cfd.

编译server2.c

gcc server2.c tcp_listen.c readn.c open_logfile.c -lpthread -Wall -O2 -o serv2

我们的服务器是并发的, 所以需要写个脚本来测试. 脚本如下:

#!/bin/bash
function send_log()
{
    fun_no=$1
    for ((i = 0; i < 10; i++))
    do
        ./cli "send $i in $fun_no call"
    done
 
}
 
for ((i = 0; i < 10; i++))
do
    send_log $i &
done

运行这个脚本我们会发现日志文件的记录序号不完全按照顺序排列, 这与操作系统的调度顺序有关.

这种per thread模型的服务器端能够满足中等规模的并发, 同时处理1k左右的连接不会有太大压力. 在我参与的这个项目中, 我使用的就是这种模型的日志记录程序.

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

UNIX网络编程从零起步(2): 交互式(interactive)服务器端

介绍完客户端程序后, 现在开始从最基本的交互式服务器端开始讲解. 所谓交互式服务就是在某个时刻只能处理一个连接. 服务器端主程序很简单:
server1.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
#include "server.h"
int
main(int argc, char *argv[])
{
    int lfd, cfd, fd;
    char *logpath = (char *) malloc(PATHSIZE);
    char *logbuf = (char *) malloc(LOGSIZE);
    char *servname = (char *) malloc(SERVNAMESIZE);
 
    ssize_t n;
 
    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 (;;) {
        cfd = accept(lfd, NULL, NULL);
        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);
    free(logpath);
    free(servname);
    return 0;
}

同样, 为了程序与协议无关, 在server1.c中第32行调用自己编写的tcp_listen函数. 这个函数的实现见tcp_connet.c, 它调用getaddrinfo(22行), 根据获得的地址信息完成TCP建立连接之前的调用socket(29行), bind(33行)和listen(46行)这三个系统调用的准备工作. 在第32行我们添加侦听端口lfd选项为SO_REUSEADDR. 这个属性对于不同的传输层协议有不同的含义. 对于TCP侦听端口来说它的主要用途包括两个方面: (1)服务器端程序重启, 但是该程序创建的子进程还在处理连接, 如果不设置这个选项重启时调用bind会失败; (2)如果服务器端有多个IP, 每个IP都使用了相同的端口对外提供服务, 如果不设置这个选项, 从启用第二个IP开启服务开始的bind都会失败. 为了程序更为健壮和适应更多类型的网络环境, 建议只要是使用TCP传输协议, 服务器端程序都应该给侦听端口添加SO_REUSEADDR.
tcp_listen.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
#include "server.h"
 
int
tcp_listen(const char *hostname, const char *servname, SA *sockaddr, socklen_t *addrlen)
{
    struct addrinfo *ailist, *aip;
    struct addrinfo hint;
    const int optval = 1;
    int lfd, ret;
 
    memset(&hint, '\0', sizeof(struct addrinfo));
    hint.ai_flags = AI_CANONNAME;
    hint.ai_family = AF_UNSPEC;
    hint.ai_socktype = SOCK_STREAM;
    hint.ai_protocol = IPPROTO_TCP;
    hint.ai_flags = AI_PASSIVE;
    hint.ai_addrlen = 0;
    hint.ai_addr = NULL;
    hint.ai_canonname = NULL;
    hint.ai_next = NULL;
 
    if ((ret = getaddrinfo(hostname, servname, &hint, &ailist)) != 0) {
        fprintf(stderr, "getaddrinfo error.\n");
        return -1;
    }
 
    aip = ailist;
    do {
        lfd = socket(aip->ai_family, aip->ai_socktype, aip->ai_protocol);
        if (lfd < 0)
            continue;
        setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval));
        if (bind(lfd, aip->ai_addr, aip->ai_addrlen) == 0) {
            if (addrlen != NULL) {
                *addrlen = aip->ai_addrlen;
            }
            if (sockaddr != NULL && addrlen != NULL)
                memcpy(sockaddr, aip->ai_addr, *addrlen);
            break;
        }
        close(lfd);
    } while ((aip = aip->ai_next) != NULL);
 
    freeaddrinfo(ailist);
 
    if (listen(lfd, BACKLOG) < 0)
        return -1;
 
    return lfd;
}

和write一样, 用read从stream socket读取内容如果被信号中断, read不是返回-1而是返回已读取的字节并设置errno为EINTER. 除此之外, 当TCP接收缓冲区被填满时, 一次读取只能读取到缓冲区内的内容. 在我开始着手这个项目的时候我对TCP/IP不熟, 还不知道stream socket的这个特性. 那时我写程序从设备控制程序提供的端口读取设备状态信息, 发现不管我设置一次读取多少个字节, 总会有时候能够读取到所有状态信息, 有时只能返回部分信息. 后来仔细查阅书籍, 才发现遇到这种情况需要分多次读取. server1.c中41行readn的实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
readn(int fd, void *vptr, size_t n)
{
    size_t nleft;
    ssize_t nread;
    char *ptr;
 
    ptr = vptr;
    nleft = n;
 
    while (nleft > 0) {
        if ((nread = read(fd, ptr, nleft)) < 0) {
            if (errno == EINTR)
                nread = 0;
            else
                return -1;
        } else if (nread == 0)
            break;
        nleft -= nread;
        ptr += nread;
    }
 
    return (n - nleft);
}

还需要一提的是server1.c中第25行open_logfile函数. 这个函数打开一个文件, 如果该文件不存在则创建一个文件.
open_logfile.c

1
2
3
4
5
6
7
8
#include "server.h"
int
open_logfile(const char *path)
{
    int fd;
    fd = open(path, O_RDWR | O_CREAT | O_APPEND, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
    return fd;
}

添加O_APPEND标志可以保证每次调用write都是原子的从文件末尾追加写入的内容. 在并发日志系统中非常重要.

编译serve1.c

gcc server1.c tcp_listen.c readn.c open_logfile.c -Wall -O2 -o serv1

运行serv1

./serv1

开启另外一个终端

netstat -na | grep 5000
tcp4 0 0 *.5000 *.* LISTEN

我们会发现端口5000处于侦听状态. 运行客户端程序./cli “Hello, this the first message!”, 在引号中的消息就会发送到当前目录下的server.log文件中. 至此, 我们完成了最基本的交互式服务器端.

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

这一天

今天这一天和多年来大多数相同的日子一样,都是自己一个人独自度过。决定对自己好一些,再买一个iPod Classic送给我自己作为这一天的礼物。下午出发到东单从地铁站A口出来,出站口还是有一位盲眼拉二胡的老大爷在乞讨。在出站口不知不觉就向东方新天地走去,进入卖场后稍作停留,然后在里面由地下一层到一层闲逛一圈。接下来转到君悦大酒店边的星巴克,经过招商银行ATM,穿过东单三条和医学院,走到校尉胡同,到达在新东安商场的苹果商店里。这一路的一切一切,对我来说是如此的陌生却又如此的熟悉。走着走着,眼泪禁不住不停的往下滴。

在苹果商店里很快就买完自己要的IPC,接下来习惯性地去旁边的ole超市看看,买了两听啤酒。看着有啤酒晚饭有点想吃烤串,于是返回走到新开路胡同。在胡同里面找到一家路边摊,一边吃着一边喝着。

吃完天色还早,决定去宝金街上的那家Molly Malone爱尔兰酒吧。仍是那时候的习惯,坐在书架对面的沙发上,点一杯大的Hoegaarden。酒还是那么的香馥可口,一边享受,一边抹去眼角的泪珠儿。

仅于此文纪念我最后的最后。

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

UNIX网络编程从零起步(1): 客户端程序

由于项目的需要, 从事Linux网络编程已有两年多的时间. 将来从事什么还未确定, 所以趁着在这个项目即将结束的时候把在网络编程的实践中学到的东西总结一下. 虽然标题是”从零起步”, 但并不是说零基础起步, 而是从最简单的交互式服务器端开始, 逐渐增加功能到复杂的支持并发连接的服务器端.

基于TCP/IP协议的网络编程, 整体基本框架大致是: 服务器端程创建一个socket, 然后将制定的端口号绑定到这个socket上, 接下来侦听这个socket. 当客户端发起连接时, accept返回一个连接socket并通过此连接socket和远端的客户端通讯. 客户端则是创建一个socket, 将socket连接到服务器端, 通过此socket与服务器端通讯.

在我编写项目程序的过程中发现, 由于很多进程都需要开机自动启动运行, 为了监控和追溯设备运行状况, 必须要把每一步的操作都记录在日志文件里. 但是Unix系统提供的syslog并不能很好的满足我的需求, 于是单独写了一个简单的基于网络通讯的日志记录程序. 这个程序功能简单, 没有复杂的业务逻辑, 正好适合当总结网络编程的例子. 网络程序编写之前需要定好通讯协议, 日志记录程序的通讯协议也很简单, 客户端向服务器端发送事先约定好长度的定长字符串, 服务器端接收并把字符串内容写到文件中.

头文件server.h

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
#ifndef _SERVER_H
#define _SERVER_H 1
 
#define PATHSIZE        256
#define HOSTNAMESIZE    256
#define SERVNAMESIZE    16
#define LOGSIZE         4096
#define BACKLOG         10
 
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
 
#include <errno.h>
#include <fcntl.h>
#include <netdb.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/types.h>
 
typedef struct sockaddr SA;
 
int open_logfile(const char *);
ssize_t readn(int, void *, size_t);
int recv_log(int, char *);
int send_log(int, const char *);
int tcp_connect(const char *, const char *, SA *, socklen_t *);
int tcp_listen(const char *, const char *, SA *, socklen_t *);
ssize_t writen(int, const void *, size_t);
#endif

客户端程序client.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
#include "server.h"
 
int
main(int argc, char *argv[])
{
    int logfd;
    char *hostname = (char *) malloc(HOSTNAMESIZE);
    char *servname = (char *) malloc(SERVNAMESIZE);
    char *logbuf = (char *) malloc(LOGSIZE);
 
    if (hostname == NULL || servname == NULL || logbuf == NULL) {
        fprintf(stderr, "malloc error.\n");
        exit(EXIT_FAILURE);
    }
 
    if (argc == 2) {
        snprintf(hostname, HOSTNAMESIZE, "localhost");
        snprintf(servname, SERVNAMESIZE, "5000");
        snprintf(logbuf, LOGSIZE, "%s\n", argv[1]);
    } else if (argc == 4) {
        snprintf(hostname, HOSTNAMESIZE, "%s", argv[1]);
        snprintf(servname, SERVNAMESIZE, "%s", argv[2]);
        snprintf(logbuf, LOGSIZE, "%s\n", argv[3]);
    } else {
        fprintf(stderr, "Usage: \n");
        fprintf(stderr, "%s [hostname] [port] <message> \n", argv[0]);
        exit(EXIT_FAILURE);
    }
 
    if ((logfd = tcp_connect(hostname, servname, NULL, NULL)) < 0) {
        fprintf(stderr, "tcp_connect error.\n");
        exit(EXIT_FAILURE);
    }
 
    send_log(logfd, logbuf);
    close(logfd);
 
    free(logbuf);
    free(hostname);
    free(servname);
 
    exit(EXIT_SUCCESS);
}

目前IPV6已经在国内高校科研单位实验性部署, 让TCP连接与IP协议无关是一个”向前”兼容的好主意. tcp_connect函数创建socket并使用getaddrinfo系统调用创建协议无关连接.
tcp_connect.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
#include "server.h"
 
int
tcp_connect(const char *hostname, const char *servname, SA *sockaddr, socklen_t *addrlen)
{
    struct addrinfo *ailist, *aip;
    struct addrinfo hint;
    int sockfd;
    int ret;
 
    memset(&hint, '\0', sizeof(struct addrinfo));
    hint.ai_family = AF_UNSPEC;
    hint.ai_socktype = SOCK_STREAM;
 
    if ((ret = getaddrinfo(hostname, servname, &hint, &ailist)) != 0) {
        fprintf(stderr, "%s", gai_strerror(ret));
        return -1;
    }
 
    aip = ailist;
    do {
        sockfd = socket(aip->ai_family, aip->ai_socktype, aip->ai_protocol);
        if (sockfd < 0)
            continue;
        if (connect(sockfd, aip->ai_addr, aip->ai_addrlen) == 0) {
            if (addrlen != NULL)
                memcpy(addrlen, &aip->ai_addrlen, sizeof(socklen_t));
            if (sockaddr != NULL)
                memcpy(sockaddr, aip->ai_addr, aip->ai_addrlen);
            break;
        }
        close(sockfd);
    } while ((aip = aip->ai_next) != NULL);
 
    if (aip == NULL) {
        sockfd = -1;
    }
 
    freeaddrinfo(ailist);
 
    return sockfd;
}

发送日志函数send_log.c

1
2
3
4
5
6
7
8
9
10
11
12
#include "server.h"
int
send_log(int sockfd, const char *logbuf)
{
    ssize_t n;
    n = writen(sockfd, logbuf, LOGSIZE);
    if (n != LOGSIZE) {
        fprintf(stderr, "send_log error.\n");
        return -1;
    }
    return 0;
}

使用write系统调用往stream socket写入至少1个字节, 这时如果write的时候所在线程收到signal并调用signal handler, 在信号处理函数返回后, write并不是返回-1而是成功返回已发送字节数; 若往write在写入任何字节之前被信号中断, write返回-1并且设置errno为EINTER. 这种情况下, 我们应该多次调用直到发送完所有要发送的字节数为止. 因此, 我们用writen函数代替直接的write.
writen.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include "server.h"
ssize_t
writen(int fd, const void *vptr, size_t n)
{
    size_t nleft;
    ssize_t nwritten;
    const char *ptr;
 
    ptr = vptr;
    nleft = n; 
    while (nleft > 0) {
        if ((nwritten = write(fd, ptr, nleft)) < 0) {
            if (nwritten < 0 && errno == EINTR)
                nwritten = 0;
            else 
                return -1;
        }
        nleft -= nwritten;
        ptr += nwritten;
    }
 
    return n;
}

编译客户端程序

gcc client.c writen.c tcp_connet.c send_log.c -Wall -O2 -o cli

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

无锁队列的C实现

工作中遇到一个问题要用到队列数据结构. 然而在多线程的环境下为了保持数据一致性enqueue和dequeue操作都需要加锁, 加锁解锁会降低程序的效率, 而且如果设计不小心有可能会出现死锁. 前些天听到过无锁(lock-free)数据结构这个概念, 可惜无锁队列的实现有现成代码的都是用java写成的, 只好自己去山寨一个.

无锁数据结构思想很简单, 利用比较交换(compare and swap, CAS, 旧值与操作地址保存的值比较, 如果相等将新值复制到操作地址中, 返回操作地址保存的值)原子操作, 在enqueue或者dequeue之前, 记录要操作队列元素的值, 如果CAS失败, 说明队列被别的线程修改过, 重复之前的步骤. C语言标准并不提供这种语义的原子操作(据说最新的C++11提供), 所幸的是GCC提供这样的内建(built-in)函数.

使用单链表实现队列反复调用malloc会降低程序效率,我使用循环数组(参见Weiss的Data Structure and Algorithm Analysis in C)实现队列. 毫无例外只要内存资源重复使用无锁队列必然会遇到ABA问题. 假设我们的队列数组只有3个元素, 初始条件是rear=0, front=1;线程A的enqueue操作到了记录rear=0的时候被阻塞; 线程B连续enqueue了3次, 这时候rear=0, front=1, 和初始条件一样; 线程A继续, CAS成功, 然而此时队列已经满, 继续enqueue导致覆盖还未dequeue的数据. 解决ABA问题最简单的办法就是用队列元素的个数当标签, 在一个CAS操作中同时比较front和size两个值. GCC并不支持这种Double CAS操作, 不过可以通过让整形数某些位表示队列元素个数某些位表示front/rear的位置来模拟DCAS. 实现的代码如下:

#include <stdlib.h>
#include <stdint.h>
#include <string.h>
 
typedef struct {
    unsigned capacity;
    uint64_t front;
    uint64_t rear;
    void **array;
} aQueue_t;
 
typedef aQueue_t *aQueue;
 
aQueue
aQueue_init(unsigned size)
{
    aQueue q;
    q = (aQueue) malloc(sizeof(aQueue_t));
    if (q == NULL) exit(EXIT_FAILURE);
    q->array = (void **) malloc(sizeof(void **) * size);
    if (q->array == NULL) exit(EXIT_FAILURE);
    q->capacity = size;
    q->front = 1;
    q->rear = 0;
    return q;
}
 
int
aQueue_isEmpty(aQueue q)
{
    uint64_t front;
    unsigned *size;
    front = __sync_or_and_fetch(&q->front, 0xFFFFFFFF);
    size = (unsigned *)&front;
    return *size == 0;
}
 
int
aQueue_isFull(aQueue q)
{
    uint64_t front;
    unsigned *size;
    front = __sync_or_and_fetch(&q->front, 0xFFFFFFFF);
    size = (unsigned *)&front;
    return *size == q->capacity;
}
 
unsigned
aQueue_succ(unsigned idx, aQueue q)
{
    if (++idx == q->capacity)
        idx = 0;
    return idx;
}
 
void *
de_aQueue(aQueue q)
{
    void *ret;
    uint64_t comp_val, new_val;
    unsigned *size, *front;
    do {
        if (aQueue_isEmpty(q))
            continue;
        else {
            comp_val = q->front;
            size = (unsigned *)&q->front;
            front = size + 1;
            ret = q->array[*front];
            (*size)--;
            *front = aQueue_succ(*front, q);
            memcpy(&new_val, size, sizeof(uint64_t));
        }
    } while (__sync_val_compare_and_swap(&q->front, comp_val, new_val) != old_val);
    return ret;
}
 
void
en_aQueue(void *element, aQueue q)
{
    uint64_t comp_val, new_val;
    unsigned *size, *rear, pos;
    do {
        if (aQueue_isFULL(q))
            continue;
        else {
            comp_val = q->rear;
            size = (unsigned *)&q->rear;
            rear = size + 1;
            pos = *rear;
            (*size)--;
            *rear = aQueue_succ(*rear, q);
            memcpy(&new_val, size, sizeof(uint64_t));
        }
    } while (__sync_val_compare_and_swap(&q->front, comp_val, new_val) != old_val);
    q->array[pos] = element;
}

上面代码用到了uint64_t类型, 有可能只能在64位的操作系统下运行.

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

分手感悟

本以为分手不是一件大不了的事情,更何况我曾经经历过那些件我觉得让我伤痛无比的事情。但是如今面对分手还是觉得脑子一片空白,什么也不想做,什么也不想吃。

爱就是爱,不爱就是不爱,不是一方能够做什么说什么就能改变。这是我一直都认同的说法。当她告诉我“她无法爱上我”的时候,我已经知道无论我如何再次挽留也都没有什么用处。尽管难免不会难受一阵子,我还是很坦然,因为我觉得我已经做了所有我能够做的事情,我对自己能够问心无愧。

年纪大了,不知道还会不会还有没有机会再去爱一个人。有一点我仍然深信不疑,爱情付出才会有回报,付出也让人快乐。

回想从前,让人难过的不是分手,不是她喜欢别的人,而是多年后她的那封信,说“看着你送给我的礼物,我泪流满面”,可是这个时候心已不在。

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

raspberrypi分辨率问题

无聊买了一块树莓派的开发板玩, 用最简单的方法装饰debian wheezy, 能够进入操作系统. 但是通过hdmi连接线接到我的24寸显示器上却只能够以1680×1050的分辨率显示, 看着十分难受. 花了好多时间到网上找答案, 解决方法是在/boot/config.txt这个文件中加入一行:

hdmi_mode=82

重启后系统分辨率就修改成1920×1080. 使用tvsevice -m DMT命令可以列出连接显示器支持的分别率和对应的hdmi_mode编号.

发表在 未分类 | 标签为 , | 留下评论

在DELL PowerEdge R720xd上安装Debian

团组新购置一台R720xd服务器, 我负责安装操作系统. 和网站服务器一样, 我决定还是使用Debian 6 squeeze.

服务器操作系统安装比台式机要繁琐. R720xd主板集成RAID卡, 安装前需要设置BIOS. 服务器自带一块1T企业级SATA硬盘, 默认设置为RAID盘, 并且RAID卡里设置虚拟硬盘(virtual disk). 如果不重新设置BIOS, 安装系统的时候会发现只有20G的空间可用. 删掉虚拟硬盘并且关闭RAID卡选项, 然后就可以正常安装系统.

R720xd自带一块高通NIC5720四口前兆网卡, 安装好系统发现缺少网卡驱动. 到高通的网站上下载驱动, 同时要安装linux-header-{kernel version}的软件包, 编译好后:

modprobe tg3

之后, 操作系统安装完毕.

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

看某报编辑记者们满地打滚

某报新年贺词据说被上级主管部门阉割,于是乎这几天该报社编辑和记者们满地打滚,说要闹成“国际事件”。看着真是又好气又好笑。清末的先行者谭嗣同在被捕之前曾经说过,”各国变法,无不从流血而成,今中国未闻有因变法而流血者,此国之所以不昌。有之,请自嗣同始!”。 这些人别说流血坐牢,连个在体制内的职务都舍不得辞去。他们的那些话能信么?当然不能,信了只能当了他们的炮灰。

记得该报某专栏作者曾经编过一本书叫做《半个世纪前的庄严承诺》,这本书有着很强的现实意义。然而,它的意义与其说是在于号召民众向执掌权柄的某党提出兑现承诺的要求,不如说是警醒国人别被这些野心家们蒙骗,免得自己的儿孙也编个《x个世纪前的庄严承诺》。

发表在 和谐社会 | 留下评论

说说pthread_cond_wait

经常在BBS上看到有新手问pthread_cond_wait的问题, 不理解这个系统调用这么设计的目的. 这一两年来我一直都在做一些多线程守护进程的开发, 就我的工作经验来说说我对这个问题的看法.

首先, 我们考虑一下pthread_cond_wait的使用场景. 在多线程编程的时候, 会遇到这样的需求: 在满足某特定条件时, 某个线程去做一项任务; 而当条件不满足的时候, 这个线程一直阻塞直到条件得到满足; 而条件的改变则是由另外一个线程控制. 显然, 用IPC, 例如FIFO, UNIX套接字, 可以满足这样的要求. 如果没有条件变量, 用线程锁, 看起来也可以”实现”这样的功能. 代码如下:

int flag;
pthread_mutex_t mutex;
 
void *
thr_fn1(void *arg)
{
    for (;;) {
        pthread_mutex_lock(&mutex);
        if (flag != 0) {
            /*do something here*/
        }
        pthread_mutex_unlock(&mutex);    
    }
}
 
void *
thr_fn2(void *arg)
{
    pthread_mutex_lock(&mutex);
    /*set flag value here*/
    flag = xxx; 
    pthread_mutex_unlock(&mutex);
}

然而, 这段代码问题很大. 首先, 采用轮询的办法, 不停的加锁解锁会消耗大量系统自资源. 更致命的是, thr_fn1和thr_fn2之间有racing condition, 导致thr_fn2可能得不到锁, 无法重新设置flag, 进而thr_fn1也不会按照需求去执行一些任务. 幸运的是POSIX提供了条件变量这样的同步机制, 使用条件变量就能够解决这样的问题.

int flag;
pthread_mutex_t mutex;
pthread_cond_t cond;
 
void *
thr_fn1(void *arg)
{
    for (;;) {
        pthread_mutex_lock(&mutex);
        if (flag == 0) {
            pthread_cond_wait(&cond, &mutex);
        }
        /*do something here*/
        pthread_mutex_unlock(&mutex);    
    }
}
 
void *
thr_fn2(void *arg)
{
    pthread_mutex_lock(&mutex);
    /*set flag value here*/
    flag = xxx;
    pthread_cond_signal(&cond); 
    pthread_mutex_unlock(&mutex);
}

thr_fn1执行到pthread_cond_wait时, 首先做的事情是原子的解开mutex锁和阻塞线程等待条件发生, 这样thr_fn1和thr_fn2之间就不存在racing condition. 在thr_fn2调用pthread_cond_signal完成后, pthread_cond_wait返回, 并且在返回前试图获取mutex锁. 最后, thr_fn2解开mutex锁, thr_fn继续往下执行. 因为pthread_cond_signal不存在解锁加锁的问题, 它的参数只有cond; 而pthread_cond_wait需要解开和重新加上某个锁, 所以它除了cond, 还有一个mutex参数. 简而言之, pthread_cond_wait相当于做了pthread_mutex_unlock—cond_wait—pthread_mutex_lock三件事情, 它的原子性保证了condition racing导致结果不可预期的情形不会出现.

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