UNIX网络编程(UNP) 第二十六章学习笔记

概述

传统unix中,如果一个进程需要另一个实体完成某事,就会fork一个子进程然后让其执行。

但是fork调用存在着以下几个问题:

  • fork是昂贵的。fork要将父进程的内存映像复制到子进程,并在子进程复制所有描述符。即便现在使用了“写时复制”技术,fork依然是昂贵的

  • fork返回之后,父子进程之间的通信需要进程间通信(IPC)。调用fork之前父进程向子进程传递信息是容易的,但是子进程向父进程返回信息确很困难

线程有助于解决这些问题,因为

  • 线程创建成本较为低廉,比进程的创建可能快10-100倍
  • 线程共享相同的全局内存,使得线程之间易于共享信息

基本线程函数:创建和终止

pthread_create函数

当程序由exec启动执行时候,称为初始线程或者主线程的单个线程就创建了。其他都用pthread_create函数创建。

int pthread_create(pthread_t *tid,
		const pthread_attr_t * attr,
		void *(*func)(void * ),
		void * arg);

当线程创建成功的时候tid返回线程ID(pthraed_t);我们可以通过attr设置诸如线程优先级、初始栈大小、是否是守护线程等,一般可以用NULL表示使用默认值;func表示要该线程执行的函数,线程会调用该函数,然后通过显式的调用pthread_exit或者隐式的终止(通过return),函数的调用参数由arg指定,如果要传入多个数值,需要封装成struct然后再传入。

线程如果创建成功,函数返回0,否则返回正值表示具体错误(而不是返回-1并且设置errno)

pthread_join函数

我们可以用join来等待一个给定线程的终止,类似于waitpid。为此我们需要制定等待线程的tid(而不能等待任意一个线程结束)

int pthread_join(pthread_t *tid, void ** status)

如果status非空,那么所等待线程的返回值就会存入status指定的位置

pthread_self函数

线程可以调用该函数获知自己的tid(类似于进程中的getpid)

pthread_t pthread_self(void);
pthread_detach函数

一个线程要么是可汇合的(joinable,默认值),要么是脱离的(detached),一个可汇合的线程结束时候,线程ID和退出状态会留存到另一个线程对它调用pthread_join。脱离的线程终止的时候,所有相关的资源都被释放,不能等待终止。

int pthread_detach(pthread_t);

本函数一般是由想让自己脱离的线程调用,如以下语句

pthread_detach(pthread_self());
pthread_exit函数

让一个线程终止的方法之一就是调用pthread_exit函数

void pthread_exit(void * status);

如果该线程未曾脱离,其线程ID和退出状态会一直留存到调用进程内的某个其他线程对它调用pthread_join。

status不能指向局部于调用线程的对象,因为线程终止时候对象也消失。

线程终止的其他方法有:

  1. 启动线程的函数返回。其返回值就是线程的终止状态
  2. main返回或者任何线程调用了exit,进程终止,所有线程也随之终止。

使用线程的str_cli函数

#include "../unp.h"
#include <pthread.h>
void *copyto(void *);
static int sockfd;
static FILE *fp;

void str_cli(FILE *fp_arg, int sockfd_arg)
{
    char recvline[MAXLINE];
    pthread_t tid;
    sockfd = sockfd_arg;
    fp = fp_arg;
    pthread_create(&tid, NULL, copyto, NULL);
}
void *copyto(void *arg)
{
    char sendline[MAXLINE];
    while (Fgets(sendline, MAXLINE, fp) != NULL)
        Writen(sockfd, sendline, strlen(sendline));
    Shutdown(sockfd, SHUT_WR);
    return (NULL);
}

使用线程的TCP回射服务器程序

#include "../unp.h"
#include <pthread.h>
static void *doit(void *);
int main(int argc, char **argv)
{
    int listenfd, connfd;
    pthread_t tid;
    socklen_t addrlen, len;
    struct sockaddr *cliaddr;

    if (argc == 2)
        listenfd = Tcp_listen(NULL, argv[1], &addrlen);
    else if (argc == 3)
        listenfd = Tcp_listen(argv[1], argv[2], &addrlen);
    else
        err_quit("usage: tcpserv01 [ <host> ] <service or port>");
    cliaddr = Malloc(addrlen);

    for (;;)
    {
        len = addrlen;
        connfd = Accept(listenfd, cliaddr, &len);
        pthread_create(&tid, NULL, &doit, (void *)connfd);//这里直接就是一个连接一个线程
    }
}
static void *doit(void *arg)
{
    pthread_detach(pthread_self());
    str_echo((int)arg);
    Close((int)arg);
    return NULL;
}

注意在上述调用pthread_create中我们传入了整数类型connfd,ANSI C并不能保证这样做有用,只有在指针大小和整数类型大小一致的系统上才能起作用,不过绝大多数UNIX都符合这个特征。

那么问题来了?为什么我们不直接传入connfd地址呢?答案是不同的线程因此会共享该地址,从而互相干扰。

除此之外我们可以提供一个更好的方法

#include "../unp.h"
#include <pthread.h>
static void *doit(void *);
int main(int argc, char **argv)
{
    int listenfd, *iptr;
    pthread_t tid;
    socklen_t addrlen, len;
    struct sockaddr *cliaddr;

    if (argc == 2)
        listenfd = Tcp_listen(NULL, argv[1], &addrlen);
    else if (argc == 3)
        listenfd = Tcp_listen(argv[1], argv[2], &addrlen);
    else
        err_quit("usage: tcpserv01 [ <host> ] <service or port>");
    cliaddr = Malloc(addrlen);

    for (;;)
    {
        len = addrlen;
        iptr=Malloc(sizeof(int));
        *iptr = Accept(listenfd, cliaddr, &len);
        pthread_create(&tid, NULL, &doit, iptr);
    }
}
static void *doit(void *arg)
{   
    int connfd;
    connfd=*((int*)arg);
    free(arg);

    pthread_detach(pthread_self());
    str_echo(connfd);
    Close(connfd);
    return NULL;
}

注意二者的差异,此时我们通过调用了malloc来分配空间,从而避免互相干扰

线程安全函数

在上面我们使用到了malloc和free函数,这两个函数是不可重入的。这意味着当主线程处于函数内部执行流程的时候,信号处理函数如果调用这两个函数之一,会可能造成严重后果。那么我们怎么可以调用这两个函数呢?posix规定这两个函数和其他很多函数必须是线程安全的。

不必线程安全的版本 必须线程安全的版本 注释
asctime asctime_r
ctermid 参数非空时线程安全
ctime ctime_r
getc_unlocked
getchar_unlocked
getgrid getgrid_r
getgrnam getgrnam_r
getlogin getlogin_r
getpwnam getpwnam_r
getpwuid getpwuid_r
gmtime gmtime_r
localtime localtime_r
putc_unlocked
putchar_unlocked
rand rand_r
readdir readdir_r
strtok strtok_r
tmpnam 参数非空时线程安全
ttyname ttyname_r
gethostXXX
getnetXXX
getprotoXXX
getservXXX
inet_ntoa

期中ctermid和tmpnam线程安全条件是,调用者为返回结果预先分配空间,并把指向该控件的指针作为参数传递给函数。

线程特定数据

当我们使用线程的时候,有时候会碰到因为有函数使用静态变量而引起的错误,因为静态变量无法为不同的线程保存各自的值。对此的解决办法有以下几个:

  • 使用线程特定数据。优点在于调用顺序不用变动,所有变动都体现在库函数而不是应用程序。缺点在于较为复杂,而且转换成了只能在支持现成的系统上工作的函数

  • 改变调用顺序,如调用者吧readline的所有调用参数封装在一个结构,并在该结构中存入该静态变量

    typedef struct{
    	int read_fd;
      	char *read_ptr;
        size_t read_maxlen;
        int rl_cnt;
        char *rl_bufptr;
        char r1_buf[MAXLINE];
    }
    void readline_rinit(int ,void *,size_t,Rline*);
    ssize_t readline_r(Rline *);
    ssizse_t Readline_r(Rline*);
    

    这样的话无论是否支持线程,函数都可以使用。但是要修改所有用到相关函数的应用程序

  • 改变接口结构,避免使用静态变量。sss

我们不妨先试图使用线程特定数据。每个系统都支持优先数量的线程特定数据元素(POSIX要求不小于128/进程)。系统为每个进程维护一个我们称之为Key结构的结构数组,每个Key包含有标志和析构函数指针。

Key中的标志指示该数组元素是否正在使用,所有的标志都初始化为不在使用。当线程调用pthread_key_create创建一个新的线程特定数据元素时候,系统会搜索Key结构数组找到第一个不在使用的元素,然后返回数组索引。

除了进程范围的Key结构数组外,系统还会在进程内维护每个线程的多条消息。这些特定于线程的信息我们称之为Pthread结构,其部分内容是我们称之为pkey数组的一个128元素的指针数组,pkey数组的所有元素都被初始化为空指针,这些指针是和进程内的128个可能的“键”相关联的。

可能比较混淆,实际上来说,我们可以认为每个线程都自带有一定的线程特定数据元素数组,然后函数可以声明得到一个key数组中的index,拿着这个index去初始化每个线程中pkey[index],从而确保每个线程都有自己的空间。

一个使用线程特定数据来维护线程各自状态的readline函数的运行逻辑会像下面一样:

  1. 进程被启动,多个线程被创建
  2. 其中一个线程(假设是0)是首个调用readline函数的线程,readline函数会调用pthread_key_create获取一个未用的元素,假定是1(readline用pthread_once函数确保pthread_key_create只会被调用一次)
  3. readline调用pthread_getspecific获取本线程的pkey[1]值,返回的会是一个空指针。readline于是调用malloc分配内存去,初始化内存区,然后调用pthread_setspecific设置pkey[1]为刚分配的内存区
  4. 另一个线程(假定是N)调用了readline,同样试图调用pthread_once初始化键,不过因为线程0已经调用过了,所以会不再被调用
  5. readline调用pthread_getspecific发现同样为空指针,于是同样用malloc初始化

当线程终止时候,我们在调用pthread_key_create时候传入的析构函数将会启动,从而让我们有机会释放内存

一个典型的关于pthread_key_create和pthread_once使用的例子大概如下图

pthread_key_t r1_key;
pthread_once_t r1_once=PTHREAD_ONCE_INIT;

void readline_destructor(void *ptr)
{
    free(ptr);
}

void readline_once(void)
{
    pthread_key_create(&r1_key,readline_destructor);
}

ssize_t readline(...)
{
    ...
    pthread_once(&r1_once,readline_once);
    
    if ( (ptr=pthread_getspecific(r1_key))==NULL){
        ptr=Malloc(...);
        pthread_setspecific(r1_key,ptr);
        //初始化内存
    }
}
例子:使用线程特定数据的readline函数
#include "../unp.h"
#include <pthread.h>
static pthread_key_t r1_key;
static pthread_once_t r1_once = PTHREAD_ONCE_INIT;

static void readline_destructor(void *ptr)
{
    free(ptr);
}
static void readline_once(void)
{
    pthread_key_create(&r1_key, readline_destructor);
}
typedef struct
{
    int r1_cnt;
    char *r1_bufptr;
    char r1_buf[MAXLINE];
} Rline;

static ssize_t my_read(Rline *tsd, int fd, char *ptr)
{
    if (tsd->r1_cnt <= 0)
    {
    again:
        if ((tsd->r1_cnt = read(fd, tsd->r1_buf, MAXLINE)) < 0)
        {
            if (errno == EINTR)
                goto again;
            return -1;
        }
        else if (tsd->r1_cnt == 0)
            return 0;
        tsd->r1_bufptr = tsd->r1_buf;
    }
    tsd->r1_cnt--;
    *ptr = *tsd->r1_bufptr++;
    return (1);
}
ssize_t readline(int fd, void *vptr, size_t maxlen)
{
    size_t n, rc;
    char c, *ptr;
    Rline *tsd;

    pthread_once(&r1_once, readline_once);
    if ((tsd = pthread_getspecific(r1_key)) == NULL)
    {
        tsd = Calloc(1, sizeof(Rline));
    }
    ptr = vptr;
    for (n = 1; n < maxlen; n++)
    {
        if ((rc = my_read(tsd, fd, &c)) == 1)
        {
            *ptr++ = c;
            if (c == '\n')
                break;
        }
        else if (rc == 0)
        {
            *ptr = 0;
            return (n - 1);
        }
        else
            return -1;
    }
    *ptr = 0;
    return n;
}

Web客户与同时连接

我们可以用线程代替16章中的非阻塞connect,允许让他们使用默认的阻塞模式。

#include "../unp.h"
#include <pthread.h>
#define MAXFILES 20
#define SERV "80"

struct file
{
    char *f_name;
    char *f_host;
    int f_fd;
    int f_flags;
    pthread_t f_tid;
} file[MAXFILES];
#define F_CONNECTING 1
#define F_READING 2
#define F_DONE 4

#define GET_CMD "GET %s HTTP/1.0\r\n\r\n"
int nconn, nfiles, nlefttoconn, nlefttoread;

void *do_get_read(void *);
void home_page(const char *, const char *);
void write_get_cmd(struct file *);

int main(int argc, char **argv)
{
    int i, n, maxnconn;
    pthread_t tid;
    struct file *fptr;
    if (argc < 5)
        err_quit("usage: web <#conns> <IPaddr> <homepage> file1 ...");
    maxnconn = atoi(argv[1]);

    nfiles = min(argv - 4, MAXFILES);
    for (i = 0; i < nfiles; i++)
    {
        file[i].f_name = argv[i + 4];
        file[i].f_host = argv[2];
        file[i].f_flags = 0;
    }
    printf("nfiles = %d \n", nfiles);
    home_page(argv[2], argv[3]);
    nlefttoread = nlefttoconn = nfiles;
    nconn = 0;
    while (nlefttoread > 0)
    {
        while (nconn < maxnconn && nlefttoconn > 0)
        {
            for (i = 0; i < nfiles; i++)
                if (file[i].f_flags == 0)
                    break;
            if (i == nfiles)
                err_quit("nlefttoconn= %d but nothing found", nlefttoconn);

            file[i].f_flags = F_CONNECTING;
            pthread_create(&tid, NULL, &do_get_read, &file[i]);
            file[i].f_tid = tid;
            nconn++;
            nlefttoconn--;
        }
        //注意thr_join是Solaris下的<thread.h>,其作用类似于waitpid
        //可以等待任意一个线程结束,但是posix并没有该函数(所以该函数不能跑)
        if ((n = thr_join(0,&tid, (void **)&fptr)) != 0)
            errno = n, err_sys("thread join error");

        nconn--;
        nlefttoread--;
        printf("thread id %d for %s done\n", tid, fptr->f_name);
    }
    exit(0);
}
void *do_get_read(void *vptr)
{
    int fd, n;
    char line[MAXLINE];
    struct file *fptr;

    fptr = (struct file *)vptr;
    fd = Tcp_connect(fptr->f_host, SERV);
    fptr->f_fd = fd;
    printf("do_get_read for %s,fd %d,thread %d\n", fptr->f_name, fd, fptr->f_tid);
    write_get_cmd(fptr);

    for(;;){
        if ( (n=Read(fd,line,MAXLINE))==0)
            break;
        printf("readk %d bytes from %s\n",n,fptr->f_name);
    }
    printf("end-of-file on %s\n",fptr->f_name);
    Close(fd);
    fptr->f_flags=F_DONE;
    return(fptr);
}

互斥锁

在线程中我们可以使用互斥锁来保护共享变量,这使用到了pthread_mutex_t和pthread_mutex_lock以及pthread_mutex_unlock

int pthread_mutex_lock(pthread_mutex_t *);
int pthread_mutex_unlock(pthread_mutex_t *);

如果该变量是静态分配的,那么我们就要初始化为常值PTHREAD_MUTEX_INITIALIZER,如果我们在共享内存中分配一个互斥锁,那么就要调用pthread_mutex_init函数在运行时初始化

pthread_mutext_t counter_mutex=PTHEAD_MUTEX_INITIALIZER;
int main(int argv,char ** argv){
	...
	pthread_mutex_lock(&counter_mutex);
	...
	pthread_mutex_unlock(&counter_mutex);
}

条件变量

unix同样提供了API来实现条件变量

int pthread_cond_wait(pthread_cond_t * cptr,
		pthread_mutex_t *mptr);
int pthread_cond_signal(pthread_cond_t *);

一个常见的使用模式是

int ndone;
pthread_mutex_t ndone_mutex=PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t ndone_cond=PTHREAD_COND_INITIALIZER;

pthread_mutex_lock(&ndone_mutex);
ndone++;
pthread_cond_signal(&ndone_cond);
pthread_mutex_unlock(&ndone_mmutex);

pthread_mutex_lock(&ndone_mutex);
while(ndone==0)
    pthread_cond_wait(&ndone_cond,&ndone_mutex);
...
pthread_mutex_unlock(&ndone_mutex);
    	

除此之外,我们如果需要唤醒多个线程或者有时间限制的等待,我们可以用下面的函数

int pthread_cond_broadcast(pthread_cond_t *cptr);

int pthread_cond_timedwait(
		pthread_cond_t * cptr, pthread_mutex_t * mptr,
		const struct timespec * abstime);

pthread_cond_timewait允许线程设置一个阻塞事件的限制,如果发生了超时,就会设置ETIME错误,abstime设置的是绝对时间,从1970年1月1日你UTC时间以来的秒数和纳秒数,通常调用过程是调用gettimeofday获取当前时间的timeval结构,复制到timespec结构中,再加上自己期望的时间限制。优点在于如果该函数过早返回(可能因为捕获了信号),那么不必改动timespec结构参数就可以再次调用该函数

struct timeval tv;
struct timespec ts;
if (gettimeofday(&tv,NULL)<0)
	err_sys("gettimeofday error");
ts.tv_sec=tv.tv_sec+5;//设置为5s之后
ts.tv_nsec=tv.tv_usec*1000//从微秒到纳秒
pthread_cond_timedwait(...,&ts);

Web客户与同时连接(续)

在之前我们书写的版本中,我们使用到了solaris的thr_join从而失去了可移植性,因此我们可以对此做出一些更改。

#include "../unp.h"
#include <pthread.h>
#define MAXFILES 20
#define SERV "80"

struct file
{
    char *f_name;
    char *f_host;
    int f_fd;
    int f_flags;
    pthread_t f_tid;
} file[MAXFILES];
#define F_CONNECTING 1
#define F_READING 2
#define F_DONE 4
#define F_JOINED 8

#define GET_CMD "GET %s HTTP/1.0\r\n\r\n"
int nconn, nfiles, nlefttoconn, nlefttoread;

int ndone;
pthread_mutex_t ndone_mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t ndone_cond = PTHREAD_COND_INITIALIZER;

void *do_get_read(void *);
void home_page(const char *, const char *);
void write_get_cmd(struct file *);

int main(int argc, char **argv)
{
    int i, n, maxnconn;
    pthread_t tid;
    struct file *fptr;
    if (argc < 5)
        err_quit("usage: web <#conns> <IPaddr> <homepage> file1 ...");
    maxnconn = atoi(argv[1]);

    nfiles = min(argv - 4, MAXFILES);
    for (i = 0; i < nfiles; i++)
    {
        file[i].f_name = argv[i + 4];
        file[i].f_host = argv[2];
        file[i].f_flags = 0;
    }
    printf("nfiles = %d \n", nfiles);
    home_page(argv[2], argv[3]);
    nlefttoread = nlefttoconn = nfiles;
    nconn = 0;
    while (nlefttoread > 0)
    {
        while (nconn < maxnconn && nlefttoconn > 0)
        {
            for (i = 0; i < nfiles; i++)
                if (file[i].f_flags == 0)
                    break;
            if (i == nfiles)
                err_quit("nlefttoconn= %d but nothing found", nlefttoconn);

            file[i].f_flags = F_CONNECTING;
            pthread_create(&tid, NULL, &do_get_read, &file[i]);
            file[i].f_tid = tid;
            nconn++;
            nlefttoconn--;
        }
        //这里利用增加条件变量的办法,如果发现有线程完成(线程会调用signal通知),就轮询查找
        //哪个线程是完成的
        pthread_mutex_lock(&ndone_mutex);
        while (ndone == 0)
            pthread_cond_wait(&ndone_cond, &ndone_mutex);
        for (i = 0; i < nfiles; i++)
        {
            if (file[i].f_flags && F_DONE)
            {
                pthread_join(file[i].f_tid, (void **)&fptr);

                if (&file[i] != fptr)
                    err_quit("file[i] != fptr");

                fptr->f_flags = F_JOINED;
                nconn--;
                nlefttoread--;
                ndone--;
                printf("thread id %d for %s done\n", tid, fptr->f_name);
            }
        }
        pthread_mutex_unlock(&ndone_mutex);
    }
    exit(0);
}
void *do_get_read(void *vptr)
{
    int fd, n;
    char line[MAXLINE];
    struct file *fptr;

    fptr = (struct file *)vptr;
    fd = Tcp_connect(fptr->f_host, SERV);
    fptr->f_fd = fd;
    printf("do_get_read for %s,fd %d,thread %d\n", fptr->f_name, fd, fptr->f_tid);
    write_get_cmd(fptr);

    for (;;)
    {
        if ((n = Read(fd, line, MAXLINE)) == 0)
            break;
        printf("readk %d bytes from %s\n", n, fptr->f_name);
    }
    printf("end-of-file on %s\n", fptr->f_name);
    Close(fd);

    pthread_mutex_lock((&ndone_mutex));
    fptr->f_flags = F_DONE;
    ndone++;
    pthread_cond_signal(&ndone_cond);
    pthread_mutex_unlock(&ndone_mutex);

    return (fptr);
}
发布了31 篇原创文章 · 获赞 32 · 访问量 737

猜你喜欢

转载自blog.csdn.net/a348752377/article/details/104200008