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是否可读.

此条目发表在计算机与网络技术分类目录,贴了, , 标签。将固定链接加入收藏夹。

发表评论

电子邮件地址不会被公开。 必填项已用*标注

This site uses Akismet to reduce spam. Learn how your comment data is processed.