使用应用级并发的应用程序称为并发程序(concurrent program)。现代操作系统提供3种基本的构造并发程序的方法:进程、I/O多路复用和线程。下面将分别予以讨论。

1. 基于进程的并发编程

我们可以利用熟悉的fork、execve及waitpid函数来开发基于进程的并发编程。下面以构造并发服务器为例,在服务器程序中,父进程接受客户端的连接请求,然后创建一个新的子进程为每一个客户端提供服务。

假设服务器正在监听一个监听描述符3上的连接请求,此时刚好有一个客户端1请求连接,所以服务器接受它的请求并返回一个已连接描述符4。在接受连接请求之后,服务器派生一个子进程,这个子进程获得服务器描述符表的完整副本。子进程关闭它副本中的监听描述符3,而父进程关闭它的已连接描述符4的副本,因为父子进程中的描述符都指向同一个文件表项,所以父进程关闭它的以连接描述符的副本是至关重要的。否则,当子进程退出要关闭已连接描述符时会失败,因为在父进程里仍有一个打开的已连接描述符。同样的情况也适用于监听描述符,所以在子进程里要及时的关闭监听描述符。

现在,假设有另一个客户端2发送来连接请求,服务器接受并返回了一个新的已连接描述符5。服务器又会派生一个子进程,这个子进程同样也会获得服务器描述符表的完整副本,它关闭了监听描述符并用已连接描述符5服务客户端,而服务器关闭已连接描述符,并继续等待新的连接请求。

此时服务器派生的两个子进程正在并发地服务于它们连接的客户端。如下图所示:

<img src="https://cdn.jsdelivr.net/gh/Leon1023/leon_pics/img/20200327231845.jpg" alt="微图" style="zoom: 33%;" />

下面给出源代码:

/* * echoserverp.c - A concurrent echo server based on processes*/
/* $begin echoserverpmain */
#include "csapp.h"
void echo(int connfd);void sigchld_handler(int sig) //line:conc:echoserverp:handlerstart
{while (waitpid(-1, 0, WNOHANG) > 0);return;
} //line:conc:echoserverp:handlerendint main(int argc, char **argv)
{int listenfd, connfd;socklen_t clientlen;struct sockaddr_storage clientaddr;if (argc != 2) {fprintf(stderr, "usage: %s <port>\n", argv[0]);exit(0);}Signal(SIGCHLD, sigchld_handler);listenfd = Open_listenfd(argv[1]);while (1) {clientlen = sizeof(struct sockaddr_storage); connfd = Accept(listenfd, (SA *) &clientaddr, &clientlen);if (Fork() == 0) { Close(listenfd); /* Child closes its listening socket */echo(connfd);    /* Child services client */ //line:conc:echoserverp:echofunClose(connfd);   /* Child closes connection with client */ //line:conc:echoserverp:childcloseexit(0);         /* Child exits */}Close(connfd); /* Parent closes connected socket (important!) */ //line:conc:echoserverp:parentclose}
}
/* $end echoserverpmain */
  • 首先,服务器会运行很长时间,所以必须要包括一个SIGCHLD处理程序来回收僵尸进程的资源。且当SIGCHLD处理程序执行时,SIGCHLD信号是被阻塞的,所以处理程序必须准备好回收多个僵死的进程资源。

  • 其次,父子进程必须关闭它们各自的connfd副本。尤其是父进程必须关闭它的已连接描述符,以避免内存泄漏。

  • 最后,因为套接字的文件表表项中的引用计数,直到父子进程的connfd都关闭了,到客户端的连接才会终止。

    利用进程的并发程序优点: 父子进程间共享文件表,但是不共享用户地址空间。这样它们之间就不会相互覆盖彼此的资源。

    利用进程的并发程序缺点: 独立的地址空间使得父子进程间共享状态信息更加困难,为此不得不使用开销很高的显示IPC(进程间通信),这样速度上不会很快。

2. 基于I/O多路复用的并发编程

服务器使用I/O多路复用,借助select函数检测输入事件的发生。当每个已连接描述符准备好读时,服务器就从描述符读和回写一个文本行。下图展示了一个完整的基于I/O多路复用的并发事件驱动服务器代码。

一个pool结构里维护着活动客户端的集合,在调用init_pool初始化池之后,服务器进入无限循环。在循环的每次迭代中,服务器调用select函数来检测两种不同类型的输入事件:a)来自一个新客户端的连接请求;b)一个已连接描述符准备好可以读了。

当一个连接请求到达时,服务器打开链接,并调用add_client函数将该客户端添加到池里。最后,服务器调用check_clients函数,把来自已准备好的已连接描述符的一个文本行回送回去。

/* * echoservers.c - A concurrent echo server based on select*//* $begin echoserversmain */
#include "csapp.h"typedef struct { /* Represents a pool of connected descriptors */ int maxfd;        /* Largest descriptor in read_set */   fd_set read_set;  /* Set of all active descriptors */fd_set ready_set; /* Subset of descriptors ready for reading  */int nready;       /* Number of ready descriptors from select */   int maxi;         /* Highwater index into client array */int clientfd[FD_SETSIZE];    /* Set of active descriptors */rio_t clientrio[FD_SETSIZE]; /* Set of active read buffers */
} pool; void init_pool(int listenfd, pool *p);
void add_client(int connfd, pool *p);
void check_clients(pool *p);int byte_cnt = 0; /* Counts total bytes received by server */int main(int argc, char **argv)
{int listenfd, connfd;socklen_t clientlen;struct sockaddr_storage clientaddr;static pool pool; if (argc != 2) {fprintf(stderr, "usage: %s <port>\n", argv[0]);exit(0);}listenfd = Open_listenfd(argv[1]);init_pool(listenfd, &pool); //line:conc:echoservers:initpoolwhile (1) {/* Wait for listening/connected descriptor(s) to become ready */pool.ready_set = pool.read_set;pool.nready = Select(pool.maxfd+1, &pool.ready_set, NULL, NULL, NULL);/* If listening descriptor ready, add new client to pool */if (FD_ISSET(listenfd, &pool.ready_set)) { //line:conc:echoservers:listenfdreadyclientlen = sizeof(struct sockaddr_storage);connfd = Accept(listenfd, (SA *)&clientaddr, &clientlen); //line:conc:echoservers:acceptadd_client(connfd, &pool); //line:conc:echoservers:addclient}/* Echo a text line from each ready connected descriptor */ check_clients(&pool); //line:conc:echoservers:checkclients}
}
/* $end echoserversmain */
  • init_pool函数初始化客户端池。clientfd数组表示已连接描述符的集合,其中-1表示一个可用的槽位。开始时,已连接描述符集合是空的,而且监听描述符是select读集合中唯一的描述符。
/* $begin init_pool */
void init_pool(int listenfd, pool *p)
{/* Initially, there are no connected descriptors */int i;p->maxi = -1;                   //line:conc:echoservers:beginemptyfor (i=0; i< FD_SETSIZE; i++)  p->clientfd[i] = -1;        //line:conc:echoservers:endempty/* Initially, listenfd is only member of select read set */p->maxfd = listenfd;            //line:conc:echoservers:begininitFD_ZERO(&p->read_set);FD_SET(listenfd, &p->read_set); //line:conc:echoservers:endinit
}
/* $end init_pool */
  • add_client函数添加一个新客户端到活动客户端池中。在clientfd数组中找到一个空槽位后,服务器将这个已连接描述符添加到数组中,并初始化相应的RIO读缓冲区。然后将这个已连接描述符添加到select读集合,并更新该池(maxfd、maxi、)。
/* $begin add_client */
void add_client(int connfd, pool *p)
{int i;p->nready--;for (i = 0; i < FD_SETSIZE; i++)  /* Find an available slot */if (p->clientfd[i] < 0) { /* Add connected descriptor to the pool */p->clientfd[i] = connfd;                 //line:conc:echoservers:beginaddclientRio_readinitb(&p->clientrio[i], connfd); //line:conc:echoservers:endaddclient/* Add the descriptor to descriptor set */FD_SET(connfd, &p->read_set); //line:conc:echoservers:addconnfd/* Update max descriptor and pool highwater mark */if (connfd > p->maxfd) //line:conc:echoservers:beginmaxfdp->maxfd = connfd; //line:conc:echoservers:endmaxfdif (i > p->maxi)       //line:conc:echoservers:beginmaxip->maxi = i;       //line:conc:echoservers:endmaxibreak;}if (i == FD_SETSIZE) /* Couldn't find an empty slot */app_error("add_client error: Too many clients");
}
/* $end add_client */
  • check_clients函数回送文本行。如果客户端关闭了连接,服务器器这端回检测到EOF,然后服务器也关闭连接,并从池中清除掉这个描述符。
/* $begin check_clients */
void check_clients(pool *p)
{int i, connfd, n;char buf[MAXLINE]; rio_t rio;for (i = 0; (i <= p->maxi) && (p->nready > 0); i++) {connfd = p->clientfd[i];rio = p->clientrio[i];/* If the descriptor is ready, echo a text line from it */if ((connfd > 0) && (FD_ISSET(connfd, &p->ready_set))) { p->nready--;if ((n = Rio_readlineb(&rio, buf, MAXLINE)) != 0) {byte_cnt += n; //line:conc:echoservers:beginechoprintf("Server received %d (%d total) bytes on fd %d\n", n, byte_cnt, connfd);Rio_writen(connfd, buf, n); //line:conc:echoservers:endecho}/* EOF detected, remove descriptor from pool */else { Close(connfd); //line:conc:echoservers:closeconnfdFD_CLR(connfd, &p->read_set); //line:conc:echoservers:beginremovep->clientfd[i] = -1;          //line:conc:echoservers:endremove}}}
}
/* $end check_clients */
  • 优点:
  • 因为在单一进程中执行,所以不需要频繁的进程间上下文切换,进而速度较快;
  • 每个逻辑流共享进程的全部地址空间,共享数据较为简单;
  • 单一进程可以方便的利用GDB调试工具。
  • 缺点:
  • 这种事件驱动的程序比起基于进程的程序编码上相对复杂;
  • 如果运行在多核处理器上不能充分利用处理器的并发优势。

3. 基于线程的并发编程

在基于进程的并发编程方法中,我们为每个流使用了单独的进程,内核会自动调度每个进程,而每个进程有自己的独立地址空间,使的共享数据变得困难。在基于I/O调度的并发编程方法中,我们创建自己的逻辑流,利用I/O多路复用来显示地调度流,因为只有一个进程,所有的流共享整个地址空间。

线程(thread)就是运行在上下文中的逻辑流。现代系统运行里允许一个进程里同时运行多个线程,线程由内核自动调度。每个线程有自己的线程上下文(线程ID、栈、栈指针、程序计数器、通用目的寄存器和条件码),所有的线程共享进程的整个虚拟地址空间。

基于线程的并发编程结合了前两种方法的优势,各个线程由内核自动调度,且线程间又共享地址空间里的代码、数据、堆、共享库和打开的文件。

3.1创建线程

线程通过调用pthread_create函数来创建其它线程:

#include <pthread.h>
typedef void *(func)(void * arg);int pthread_create(pthread_t *tid, pthread_attr_t *attr,func *f, void *arg);//成功返回0,出错非0
  • tid: 存储函数创建的线程ID;
  • attr: 新创建线程的属性变量,默认为NULL;
  • f: 线程例程;
  • arg: 线程的输入变量,是创建线程和被创建线程间传递数据的一个途径。

线程可以调用下面的函数可以获得自己的线程ID:

#include <pthread.h>
pthread_t pthread_self(void);

3.2 终止线程

一个线程总是通过以下4种方式之一来终止的:

  1. 当顶层的线程例程返回时,线程会隐式地终止;
  2. 通过调用pthread_exit函数,线程会显示地终止。如果是主线程(每个进程开始生命周期时都是单一线程,这个线程就是“主线程”,主线程调用线程创建函数得到的线程称为“对等线程”)调用的这个函数,那么它会等待所有其它对等线程终止,然后再终止主线程和整个进程,返回值为thread_return
#include <pthread.h>
void pthread_exit(void *thread_return);//从不返回
  1. 某个对等线程调用exit函数,它会终止该进程及其所有线程。
  2. 某个对等线程通过一个“对等线程ID”作为参数调用pthread_cancel函数来终止这个线程。
#include <pthread.h>
int pthread_cancel(pthread_t tid);//成功返回0,出错为非0

3.3 回收线程资源

线程调用pthread_join函数等待其它线程终止。

#include <pthread.h>
int pthread_join(pthread_t tid, void **thread_return);//成功返回0,出错为非0

该函数会阻塞等待线程tid终止,并可以将线程例程返回的通用指针赋值到thread_return指向的位置,然后回收已终止线程占用的所有内存资源。注意:和wait函数不同的是该函数只等待tid指定的单一线程,而不能等待任意一个线程终止。

3.4 分离线程

线程有两种属性:可结合的(joinable)和分离的(detached)。前者是默认的线程属性,它能够被其它线程收回和杀死,在被回收前它的内存资源是不释放的;后者通过函数pthread_detach来设置,此时的线程被称为“分离的线程”,它是不能其它线程回收或杀死的,它的内存资源在其终止时由系统自动回收。

为了避免内存泄漏,每个可结合线程都应该要么被其它线程显示收回,要么通过调用pthread_detach函数分离。

#include <pthread.h>
int pthread_detach(pthread_t pid);//成功返回0,出错为非0

线程可以通过调用pthread_detach(pthread_self());来分离自己。

3.5 初始化线程

pthread_once函数允许初始化与线程例程相关的状态。

#include <pthread.h>
pthread_once_t once_control = PTHREAD_ONCE_INIT;
int pthread_once(pthread_once_t * &once_control,void (*init_routine)(void));

once_control是一个全局变量,它总是被初始化为PTHREAD_ONCE_INIT。当第一次调用函数时,它会调用init_routine函数,它是一个没有返回值和输入值的函数。再接下来对pthread_once的调用不做任何事。一般应用于需要动态初始化多个线程共享的全局变量时。

3.6 基于线程的并发服务器

下面思考如何编写一个基于线程的并发服务器的代码,假设由主线程循环等待客户端连接,一旦请求到来就创建一个对等线程处理该请求,而后返回继续等待其它客户端的连接请求。

  • 主线程如何将已连接描述符传递给对等线程的?

    首先,主线程调用Accept获得已连接描述符connfd;

    其次,主线程再通过已连接描述符的地址作为参数调用线程创建函数Pthread_creat

    最后,让对等线程间接引用这个指针,并将它赋值给一个局部变量。

connfd = Accept(listenfd, (SA *)&clientaddr, &clientlen);
Pthread_creat(&tid, NULL, thread, &connfd);void *thread(void *vargp){int connfd = *((int *)vargp);...
}

注意: 该思路貌似对的,但如此一来会在Accept函数和赋值语句connfd = *((int *)vargp);之间引起竞争。因为若在某一次连接请求到来,Accept返回一个已连接描述符,接着就调用线程创建函数创建新的线程来处理该连接,之后等待下一次连接请求。假设这次线程中的赋值语句还没开始执行前,一个连接请求又到来了,且Accept又再次返回了一个已连接描述符存进了connfd变量中,此时,线程中的赋值语句就会得到一个错误的描述符。

  • 为了解决这个问题,我们必须将accept返回的每个已连接描述符分配到一个动态分配的内存块中去,正如下面代码的第25和26行所示:
/* * echoservert.c - A concurrent echo server using threads*/
/* $begin echoservertmain */
#include "csapp.h"void echo(int connfd);
void *thread(void *vargp);int main(int argc, char **argv)
{int listenfd, *connfdp;socklen_t clientlen;struct sockaddr_storage clientaddr;pthread_t tid; if (argc != 2) {fprintf(stderr, "usage: %s <port>\n", argv[0]);exit(0);}listenfd = Open_listenfd(argv[1]);while (1) {clientlen=sizeof(struct sockaddr_storage);connfdp = Malloc(sizeof(int)); *connfdp = Accept(listenfd, (SA *) &clientaddr, &clientlen); Pthread_create(&tid, NULL, thread, connfdp);}
}/* Thread routine */
void *thread(void *vargp)
{  int connfd = *((int *)vargp);Pthread_detach(pthread_self()); //line:conc:echoservert:detachFree(vargp);                    //line:conc:echoservert:freeecho(connfd);Close(connfd);return NULL;
}
/* $end echoservertmain */

获取更多知识,请点击关注:
嵌入式Linux&ARM
CSDN博客
简书博客
知乎专栏


linux下的并发编程详解相关推荐

  1. Java高并发编程详解系列-Java线程入门

    根据自己学的知识加上从各个网站上收集的资料分享一下关于java高并发编程的知识点.对于代码示例会以Maven工程的形式分享到个人的GitHub上面.   首先介绍一下这个系列的东西是什么,这个系列自己 ...

  2. Java JUC并发编程详解

    Java JUC并发编程详解 1. JUC概述 1.1 JUC简介 1.2 进程与线程 1.2 并发与并行 1.3 用户线程和守护线程 2. Lock接口 2.1 Synchronized 2.2 什 ...

  3. linux下sort命令使用详解---linux将文本文件内容加以排序命令

    转载自:http://www.cnblogs.com/hitwtx/archive/2011/12/03/2274592.html linux下sort命令使用详解---linux将文本文件内容加以排 ...

  4. Linux下的awk用法详解

    Linux下的awk用法详解 一.awk介绍 二.awk的语法 三.awk常见用法 四.awk其他用法 五.awk语言特性 一.awk介绍 1.AWK 是一种处理文本文件的语言,是一个强大的文本分析工 ...

  5. linux输入一个用户看是否在工作,linux下的用户管理详解

    linux下的用户管理详解 useradd 命令详解 添加用户 想要对linux下面的帐号了解的话首先必须要了解的4个配置文件[root@localhost /]# cat /etc/passwd 首 ...

  6. Linux下find命令用法详解

    Linux下find命令用法详解 学神VIP烟火 学神IT教育:XueGod-IT 最负责任的线上直播教育平台 本文作者为VIP学员 烟火 第一部分:根据文件名查找 1.在当前目录查找名称为test的 ...

  7. linux中which命令详解,Linux下which命令使用详解(转)

    我们经常在linux要查找某个文件,但不知道放在哪里了,可以使用下面的一些命令来搜索: which 查看可执行文件的位置. whereis 查看文件的位置. locate 配合数据库查看文件位置. f ...

  8. linux下测试ftp传输,linux下ftp命令使用详解---linux文件传输ftp命令

    linux下ftp命令使用详解---linux文件传输ftp命令 上一篇 / 下一篇  2010-12-18 09:15:35 / 个人分类:Linux ftp(file transfer proto ...

  9. rm命令linux00,linux下rm命令使用详解 | Soo Smart!

    linux下rm命令使用详解---linux删除文件或目录命令 用户可以用rm命令删除不需要的文件.该命令的功能为删除一个目录中的一个或多个文件或目录,它也可以将某个目录及其下的所有文件及子目录均删除 ...

最新文章

  1. SqlServer 数据库 建立子账号
  2. python【数据结构与算法】表达式(前缀中缀后缀表达式)与Two fork tree
  3. java单例设计模式
  4. 电商泛滥的时代,我们的出路在哪里?
  5. Facebook大公开:解决NLG模型落地难题!工业界的新一波春天?
  6. jvm延迟偏向_用于偏向硬币翻转模拟的Python程序
  7. IDA笔记-IDA Pro基本使用
  8. 学计算机毁一生,大学中4大“天坑”级别的专业,学了毁一生,家里没钱不要学...
  9. 史上最全的thymleaf标签
  10. C++11 range-based for loop
  11. linux 硬链接 软链接_Linux中的软链接–完整参考
  12. Anytime项目开发记录3
  13. Linux 分区简介
  14. web端四方支付 只有安卓可以跳转
  15. 做在线交易你必须知道的关于支付的知识
  16. TP5中的No input file specified
  17. Deep Stream Ai落地--初体验
  18. Wireshark系列之6 数据流追踪
  19. HTML CSS基本知识点总结
  20. Java TreeSet详解

热门文章

  1. DSP28335的上手试用LED灯闪烁-第一篇
  2. 营收同比增长12.8% 富途的增长故事为何难以吸引投资者?
  3. VulnHub靶机-Jangow: 1.0.1
  4. 根据老师编号,统计其所授课程的平均成绩及学生人数。
  5. Cannot run program python: CreateProcess error=2, 系统找不到指定的文件。
  6. getPerspectiveTransform通过4对点确认透视变换矩阵的原理分析
  7. 成都产科生产日记(十五)(建档、入院生产、上户、少儿互助金、疫苗、儿保)全...
  8. ios开发描述文件添加新设备
  9. OpenSTF技术知识与组件解读
  10. python中and not是什么意思,python中的not,and, or