本博文主要针对UNP一书中的第六章内容来聊聊I/O复用技术以及其在网络编程中的实现

1. I/O复用技术

I/O多路复用是指内核一旦发现进程指定的一个或者多个I/O条件准备就绪,它就通知该进程。I/O复用适用于以下场合:

(1) 当客户处理多个描述符(一般是交互式输入或网络套接字),必须适用I/O复用

(2) 当一个客户处理多个套接字时,这种情况很少见,但也可能出现

(3) 当一个TCP服务器既要处理监听套接字,又要处理已连接套接字,一般就要使用I/O复用

(4) 如果一个服务器既要适用TCP,又要适用UDP,一般就要使用I/O复用

(5) 如果一个服务器要处理多个服务或者多个协议,一般就要使用I/O复用

与多线程和多进程技术相比,I/O复用技术的最大优势就是系统开销小,系统不必创建进程/线程,也不必维护这些进程/进程,从而大大减小了系统的开销。

2. I/O模型

Unix下常见的I/O模型有五种,分别是:阻塞式I/O,非阻塞式I/O,I/O复用,信号驱动式I/O和异步I/O。

Unix下对于一个输入操作,通常包含两个不同的阶段:

(1) 等待数据准备好

(2) 从内核向进程复制数据

例如:对于一次read函数操作来说,数据先会被拷贝到操作系统内核的缓冲区去,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。

再比如对于一次socket流传输来说,首先等待网络上的数据到达,然后复制到内核的某个缓冲区,然后再把内核缓冲区的数据复制到进程缓冲区。

下面就以上述两个阶段来阐述五种I/O模型。

2.1 阻塞式I/O模型

2.1.1 趣解模型

假定一个特定的场景,你的一个好朋友找你借钱,你身上没有充足的现金,于是,你要去银行取钱,银行人多,你只能在那里排队,在这段时间内,你不能离开队伍去干你自己的事情。时间都浪费在排队上面了。这就是典型的阻塞式I/O模型。

2.1.2 网络模型

默认情况下,所有的套接字都时阻塞的,以数据报套接字为例

如上图,我们把recvfrom函数视为系统调用,进程调用recvform函数后就阻塞于此,等待数据报的到达,一直到内核把数据报准备好后,就将数据从内核复制到用户进程,随后用户进程再对这些数据进行处理。

这种模型的好处就是,能够及时获得数据,没有延迟,但是就像上面趣解模型中讲到,对用户来说,这段时间一直要处于等到状态,不能去做其他的事情,在性能方面付出了代价。

2.2 非阻塞式I/O模型

2.2.1 趣解模型

还是去取钱的例子,假设你无法忍受一直在那里排队,而是去旁边的商场逛逛,然后隔一段时间回来看看还有在排队没,有的话再继续去逛逛,直到有一次你回来看到没有人排队了为止。这就是非阻塞式I/O模型。

2.2.2 网络模型

进程把一个套接字设置成非阻塞是在通知内核:当所请求的I/O操作非得把本进程投入睡眠才能完成时,不要把本进程投入睡眠,而是返回一个错误。

如上图所示,前三次询问都返回一个错误,即内核没有数据报准备好,到第四次调用recvform函数时,数据被准备好了,它被复制到应用进程缓存区,于是recvform成功返回,应用进程随后处理数据。

这种模型相对于阻塞式来说,

优点在于:应用进程不必阻塞在recvfrom调用中,而是可以去处理其他事情

缺点在于:如趣解模型中所说,你来回跑银行带来了很大的延时,可能在你来回的路上叫到了你的号。在网络模型中即可以表现在任务完成的响应延迟增大了,隔一段时间轮询一次recvform,数据报可能在两次轮询之间的任意时间内准备好,这将会导致整体数据吞吐量的降低。

2.3 I/O复用模型

2.3.1 趣解模型

现在,银行都会按一个显示屏,上面会显示轮到几号客户了。这个时候,你就不用每次都去跑进去看还有排队没,而是远远的看看显示屏上轮到你没有,如果显示了你的名字,你就去取钱就行了。这就是I/O复用模型。

2.3.2 网络模型

有了I/O复用技术,我们可以调用select或poll函数,阻塞在这两个系统调用中的某一个之上,而不是阻塞在真正的I/O系统调用上。

如上图所示,进程受阻于select调用,等待可能多个套接字中的任一个变为可读。当select返回套接字可读这一条件时,应用进程就调用recvfrom把所读的数据报复制到应用进程缓冲区。

进程阻塞在select,如果进程还有其他的任务的话就能体现到I/O复用技术的好处,那个任务先返回可读条件,就去执行哪个任务。从单一的等待变成多个任务的同时等待。

这种模型较之前的模型来说,可以不必多次轮询内核,而是等到内核的通知。

2.4 信号驱动式I/O模型

2.4.1 趣解模型

你还是不满意银行的服务,虽然不必排队,但你在商场逛的也不放心啊,你还是要盯着显示屏,深怕没有看到显示屏上面你的名字,于是,银行也退出了全新的服务,你去银行取钱的时候,银行目前人多不能及时处理你的业务,而是叫你留下手机号,等到空闲的时候就短信通知你可以去取钱了。这就是信号驱动式I/O模型。

2.4.2 网络模型

我们可以用信号,让内核在描述符就绪时发送SIGIO信号告知我们。

如上图所示,进程建立SIGIO的信号处理程序(就要趣解模型中的留下手机号),并通过sigaction系统调用安装一个信号处理函数,该系统调用将立即返回,进程继续工作,知道数据报准备好后,内核产生一个SIGIO信号,告知应用进程以及准备好,于是就在信号处理程序中调用recvfrom读取数据报,并通知主循环数据已准备好待处理,也可以立即通知主循环让他读取数据报。

这种模型的好处就是,在数据报没有准备好的期间,应用进程不必阻塞,继续执行主循环,只要等待来自信号处理函数的通知即可。

2.5 异步I/O模型

2.5.1 趣解模型

你细细的想了想自己取钱时为了什么,无非时借给你的朋友,银行都退出了网上银行服务,你只需要知道你的好朋友的银行卡号,然后在网银中申请转账,银行后台会给你处理,然后把钱打到你朋友的账户下面,等这些都处理好后,银行会给你发一条短信,告诉你转账成功,这个时候你就可以跟你的好朋友说,钱已经打给你了。这就是异步I/O模型,取钱借钱的繁琐事就交给银行后台给你处理吧。

2.5.2 网络模型

POSIX规范中提供一些函数,这些函数的工作机制是:告知内核启动某个操作,并让内核在整个操作完成后通知我们。

如上图所示,我们调用aio_read函数(POSIX异步I/O函数以aio_或lio_开头),给内核传递描述符,缓冲区指针,缓冲区大小和文件偏移,并告诉内核完成整个操作后通知我们。

不同于信号驱动式I/O模型,信号是在数据已复制到进程缓冲区才产生的。

2.6 各种I/O模型的比较

以一张图来说明五种I/O操作的差异:

同步I/O操作:导致请求进程阻塞,直到I/O操作完成

异步I/O操作:不导致进程阻塞

可知,前四种都属于同步I/O操作慢系统都会阻塞与recvfrom操作,而异步I/O不会。

3. select函数

select函数用于I/O复用,该函数允许进程指示内核等待多个事件中的任何一个发生,并只在有一个或多个事件发生或经历一段指定的事件才唤醒它。

3.1 函数原型

它的函数原型时:


int select(int maxfdp1, fd_set *readset, fd_set *writeset , fd_set *exceptset , const struct timeval *timeout);

对于timeout参数:

(1) timeout==NULL,表示要永远等待下去,直到有一个描述符准备好I/O时才返回

(2) *timeout的值为0,表示不等待,检查描述符就立即返回,这称为轮询。

(2) *timeout的值不为0,表示等待一段固定的时间,再有一个描述符准备好I/O时返回,但是不能超过由该参数制定的时间。

对于readset,writeset和exceptset三个参数:

这三个描述符说明了可读,可写和处于异常条件的描述符集合

对于描述集fd_set结构,提供了如下四个操作函数


#include <sys/select.h>

int FD_ISSET(int fd,fd_set *fdset); //设定描述集中的某个描述符

void FD_CLR(int fd,fd_set *fdset);//关掉描述集中的某个描述符

void FD_SET(int fd,fd_set *fdset);//打开描述集中的某个描述符

void FD_ZERO(fd_set *fdset);//清除集合内所有元素

对于maxfdp1参数:

指定待测试的描述符个数,它的值时待测试的最大描述符编号加1,即从上面三个描述符集中的最大描述符编号加1。

对于返回值:

select返回值有三种情况:

(1) 返回值为-1时,表示出错,如果在指定的描述符一个都没有准备好时捕捉一个信号,则返回-1

(2) 返回0,表示没有描述符准备好,指定的时间就超过了。

(3) 返回正数,表示已经准备好的描述符个数,在这种情况下,三个描述符集中依旧打开的位对应于已准备好的描述符

3.2 使用select函数修改的str_cli函数

#include    "unp.h"

void
str_cli(FILE *fp, int sockfd)
{
    int         maxfdp1;
    fd_set      rset;
    char        sendline[MAXLINE], recvline[MAXLINE];

    FD_ZERO(&rset);
    for ( ; ; ) {
        FD_SET(fileno(fp), &rset);//标准输入描述符
        FD_SET(sockfd, &rset);//socket描述符
        maxfdp1 = max(fileno(fp), sockfd) + 1;//最大描述符编号+1
        Select(maxfdp1, &rset, NULL, NULL, NULL);//调用select,阻塞于此
  //如果返回的套接字可读,就用readline读入回射文本
        if (FD_ISSET(sockfd, &rset)) {  /* socket is readable */
            if (Readline(sockfd, recvline, MAXLINE) == 0)
                err_quit("str_cli: server terminated prematurely");
            Fputs(recvline, stdout);
        }
  //如果标准输入可读,就先用fgets读入一行文本
        if (FD_ISSET(fileno(fp), &rset)) {  /* input is readable */
            if (Fgets(sendline, MAXLINE, fp) == NULL)
                return;     /* all done */
            Writen(sockfd, sendline, strlen(sendline));
        }
    }
}

3.3 批量输入

在上一节提到的str_cli版本中,仍然存在一个问题。假设客户在标准输入中批量输入数据,在输入完最后一个数据后,碰到了EOF,str_cli返回到main函数,main函数随后终止。但是,在这个过程中,标准输入的EOF终止符并不意味着我们也同时完成了从套接字的读入,可能仍有请求在去往服务器的路上,或者仍有应答在返回客户的路上。

原因就处在于此:

    if (Fgets(sendline, MAXLINE, fp) == NULL)
                return;     /* all done */

当碰到EOF终止符的时候,str_cli函数选择了立即返回,而此时,我们更需要的是找到一个条件来判断套接字的读取是否完成。

3.4 shutdown函数

shutdown函数提供了关闭TCP连接其中一半的方法,也正是为了解决上一小节发现的问题。

假设在标准输入碰到EOF终止符时,我们只关闭发送这一端,也就是给服务器发送一个FIN,告诉它我们已经完成了数据发送,但是仍然保持套接字描述打开以便读取。

这点跟close函数有点像,但是考虑到close函数有如下两个限制:

(1) close把描述符的引用计数减1,仅在该计数变为0时才关闭该套接字。但是使用shutdown可以不管引用计数就激发TCP的正常连接终止序列

(2) close终止读和写两个方向的数据传送。shutdown只是关闭单方向的读或写。

其函数原型如下:

int shutdown(int sockfd , int howto);//若成功则返回0,若出错返回-1

关于该函数的第二个参数howto:

(1) SHUT_RD 关闭连接的读这一半,套接字中不再有数据可接收,而且套接字接收缓冲区中的现有数据都被丢弃

(2) SHUT_WR 关闭连接的写这一半,对于TCP套接字来说,这称为半关闭,当前留在套接字发送缓冲区的数据将被发送,后跟TCP正常的连接终止序列。

(3) SHUT_RDWR 关闭读半部和写半部,这与调用shutdown两次等效。

3.5 str_cli函数再修改版

#include    "unp.h"
void
str_cli(FILE *fp, int sockfd)
{
    int         maxfdp1, stdineof;
    fd_set      rset;
    char        buf[MAXLINE];
    int     n;

    stdineof = 0;
    FD_ZERO(&rset);
    for ( ; ; ) {
        if (stdineof == 0)//套接字读取完成标识符
            FD_SET(fileno(fp), &rset);//关闭select描述符集中的标准输入描述符
        FD_SET(sockfd, &rset);
        maxfdp1 = max(fileno(fp), sockfd) + 1;
        Select(maxfdp1, &rset, NULL, NULL, NULL);

        if (FD_ISSET(sockfd, &rset)) {  /* socket is readable */
            if ( (n = Read(sockfd, buf, MAXLINE)) == 0) {
                if (stdineof == 1)
                    return;     /* normal termination */
                else
                    err_quit("str_cli: server terminated prematurely");
            }

            Write(fileno(stdout), buf, n);
        }

        if (FD_ISSET(fileno(fp), &rset)) {  /* input is readable */
            if ( (n = Read(fileno(fp), buf, MAXLINE)) == 0) {//若读取的字节数为0
                stdineof = 1;//表明套接字读取数据完成
                Shutdown(sockfd, SHUT_WR);  /* send FIN *///关闭读这一半
                FD_CLR(fileno(fp), &rset);
                continue;
            }

            Writen(sockfd, buf, n);
        }
    }
}

4. TCP回射服务器程序(采用select函数)

【unix网络编程第三版】阅读笔记(四):TCP客户/服务器实例中我们采用fork生成子进程来处理每个客户的需求。

如今,有了select函数,就不必创建那么多子进程了,避免了为每一个客户创建一个子进程的所有开销,本节就将其改写成任意个客户的单进程版本。

select函数的描述符集中需要存储每个客户的连接套接字。于是我们很容易想到用采用一个数组client[FD_SETSIZE]来保存所有已连接的套接字。

每次有新客户连接的时候,就在client数组中找到第一个可用项来保存该连接套接字。

具体解释见代码注释:

#include    "unp.h"
int
main(int argc, char **argv)
{
    int                 i, maxi, maxfd, listenfd, connfd, sockfd;
    int                 nready, client[FD_SETSIZE];
    ssize_t             n;
    fd_set              rset, allset;
    char                buf[MAXLINE];
    socklen_t           clilen;
    struct sockaddr_in  cliaddr, servaddr;

    listenfd = Socket(AF_INET, SOCK_STREAM, 0);

    bzero(&servaddr, sizeof(servaddr));
    servaddr.sin_family      = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servaddr.sin_port        = htons(SERV_PORT);

    Bind(listenfd, (SA *) &servaddr, sizeof(servaddr));

    Listen(listenfd, LISTENQ);

    maxfd = listenfd;       //初始化maxfd,在传入select函数时需要+1
    maxi = -1;                  //记录client数组中最后一个非-1数所占的序号
    for (i = 0; i < FD_SETSIZE; i++)
        client[i] = -1;     //初始化client数组,为-1表示该项可用
    FD_ZERO(&allset);
    FD_SET(listenfd, &allset);

    for ( ; ; ) {
        rset = allset;  //初始化描述符集
        nready = Select(maxfd+1, &rset, NULL, NULL, NULL);//注意此处为最大描述符编号+1,返回已准备好的描述符个数

        if (FD_ISSET(listenfd, &rset)) {    //检测到有新客户连接
            clilen = sizeof(cliaddr);
            connfd = Accept(listenfd, (SA *) &cliaddr, &clilen);//连接新客户,获得已连接套接字
#ifdef  NOTDEF
            printf("new client: %s, port %d\n",
                    Inet_ntop(AF_INET, &cliaddr.sin_addr, 4, NULL),
                    ntohs(cliaddr.sin_port));
#endif

            for (i = 0; i < FD_SETSIZE; i++)
                if (client[i] < 0) {//找到第一个可用项
                    client[i] = connfd; //存储套接字描述符
                    break;
                }
            if (i == FD_SETSIZE)//限制最大连接个数
                err_quit("too many clients");

            FD_SET(connfd, &allset);    /* add new descriptor to set */
            if (connfd > maxfd)
                maxfd = connfd;     //重置maxfd为最大描述符编号+1
            if (i > maxi)
                maxi = i;               //client数组中最后一个描述符所占的序号

            if (--nready <= 0)
                continue;               //没有已连接套接字了
        }

        for (i = 0; i <= maxi; i++) {   /* check all clients for data */
            if ( (sockfd = client[i]) < 0)
                continue;
            if (FD_ISSET(sockfd, &rset)) {
                if ( (n = Read(sockfd, buf, MAXLINE)) == 0) {
                        /*connection closed by client */
                    Close(sockfd);//直接关闭套接字
                    FD_CLR(sockfd, &allset);
                    client[i] = -1;
                } else
                    Writen(sockfd, buf, n);

                if (--nready <= 0)
                    break;              //没有已连接套接字了
            }
        }
    }
}

5. pselect函数

pselect函数由POSIX发明,是select的变种。

#include <sys/select.h>
int pselect(int maxfdp1,fd_set *restrict readfds,fd_set *restrict writefds,fd_set *restrict exceptfds,const struct timespec *restrict tsptr,const sigset_t *restrict sigmask);

相对于select函数,pselect函数有如下几点不同:

(1) pselect使用timespec结构,新结构的tv_nsec指定纳秒数,而原结构里的tv_usec指定微妙级

(2) pselect增加了第六个参数:一个指向信号掩码的指针。该参数允许程序先禁止递交某些信号,再测试由这些当前被禁止的信号处理函数设置的全局变量,然后调用pselect,告诉它重新设置信号掩码。

(3)pselect的超时值设为了const,保证了调用pselect不会修改此值。

6. poll函数

poll函数的功能与select相似,不过在处理流设备时,它能够提供额外的信息。

#include <poll.h>
int poll(struct pollfd *fdarray,nfds_t nfds,int timeout);//若有就绪描述符就返回其数目,如超时则返回0,若出错就返回-1

对于第一个参数:为指向一个结构数组第一个元素的指针,每个数组元素都是一个pollfd结构,用于指定测试某个给定描述符fd的条件。

struct pollfd {
        int   fd;         /* file descriptor */
        short events;     /* requested events */
        short revents;    /* returned events */
};

要测试的条件由events成员指定,函数在相应的revents成员中返回该描述符的状态。

这里每个描述符都有两个变量,一个为调用值,一个为返回结果,避免了使用值结果参数。

该结构中events和revents成员所用的常值如下表:

该表中,前四个处理输入,中间三个处理输出,最后三个处理异常。

就TCP/UDP而言,如下几种情况引起poll返回特定的revent

(1) 所有正规TCP数据和所有UDP数据都被认为时普通数据

(2) TCP的带外数据被认为时优先级带数据

(3) 当TCP连接的读半部关闭时,也被认为时普通数据,随后的读操作将返回0

(4) TCP连接存在错误既可认为是普通数据,也可认为时错误,无论哪种情况,随后的读操作都会返回-1,并把errno设为合适的值

(5) 在监听套接字上有新的连接可用既可认为时普通数据,也可认为时优先级数据。

(6) 非阻塞式connect的完成被认为是使相应套接字可写

对于第二个参数nfds:表示结构数组中元素的个数

对于第三个参数timeout:指定poll函数返回前等待多长时间。

timeout值 说明
INFINT 永远等待
0 立即返回,不阻塞进程
大于0 等待指定数目的毫秒数

在select函数中,FD_SETSIZE以及每个描述符集中最大描述符数目这些都涉及到固定值。但是在poll函数中分配一个pollfd数组并把该数组中元素的数据通知内核成了调用者的责任,内核不再需要知道这些固定大小的数据类型。

7. TCP回射服务器再修改版

#include    "unp.h"
#include    <limits.h>      /* for OPEN_MAX */

int
main(int argc, char **argv)
{
    int                 i, maxi, listenfd, connfd, sockfd;
    int                 nready;
    ssize_t             n;
    char                buf[MAXLINE];
    socklen_t           clilen;
    struct pollfd       client[OPEN_MAX];
    struct sockaddr_in  cliaddr, servaddr;

    listenfd = Socket(AF_INET, SOCK_STREAM, 0);

    bzero(&servaddr, sizeof(servaddr));
    servaddr.sin_family      = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servaddr.sin_port        = htons(SERV_PORT);

    Bind(listenfd, (SA *) &servaddr, sizeof(servaddr));

    Listen(listenfd, LISTENQ);

    client[0].fd = listenfd;
    client[0].events = POLLRDNORM;
    for (i = 1; i < OPEN_MAX; i++)//由调用者指定OPEN_MAX
        client[i].fd = -1;      //初始化为-1,表示可用
    maxi = 0;               //client数组中已用项的最大序号
    for ( ; ; ) {
        nready = Poll(client, maxi+1, INFTIM);

        if (client[0].revents & POLLRDNORM) {//新客户连接
            clilen = sizeof(cliaddr);
            connfd = Accept(listenfd, (SA *) &cliaddr, &clilen);//返回已连接客户套接字
#ifdef  NOTDEF
            printf("new client: %s\n", Sock_ntop((SA *) &cliaddr, clilen));
#endif

            for (i = 1; i < OPEN_MAX; i++)//与select不同,这里的最大值均由调用者指定
                if (client[i].fd < 0) {//找到第一个可用项
                    client[i].fd = connfd;  //保存已连接套接字描述符
                    break;
                }
            if (i == OPEN_MAX)
                err_quit("too many clients");

            client[i].events = POLLRDNORM;
            if (i > maxi)
                maxi = i;           //更新已用项的最大序号值

            if (--nready <= 0)
                continue;           //没有已连接套接字了
        }

        for (i = 1; i <= maxi; i++) {   //检查client数组中所有项
            if ( (sockfd = client[i].fd) < 0)
                continue;
            //有些实现在一个连接上接收到RST时返回的时POLLERR事件,而其他实现返回的只是POLLRDNORM事件
            if (client[i].revents & (POLLRDNORM | POLLERR)) {//查看返回的revents状态
                if ( (n = read(sockfd, buf, MAXLINE)) < 0) {
                    if (errno == ECONNRESET) {
                            //由用户来关闭该套接字
#ifdef  NOTDEF
                        printf("client[%d] aborted connection\n", i);
#endif
                        Close(sockfd);
                        client[i].fd = -1;
                    } else
                        err_sys("read error");
                } else if (n == 0) {
                        //由用户来关闭该套接字
#ifdef  NOTDEF
                    printf("client[%d] closed connection\n", i);
#endif
                    Close(sockfd);
                    client[i].fd = -1;
                } else
                    Writen(sockfd, buf, n);

                if (--nready <= 0)
                    break;              //没有已连接套接字了
            }
        }
    }
}

【unix网络编程第三版】阅读笔记(五):I/O复用:select和poll函数的更多相关文章

  1. 【unix网络编程第三版】阅读笔记(三):基本套接字编程

    unp第三章主要介绍了基本套接字编程函数.主要有:socket(),bind(),connect(),accept(),listen()等. 本博文也直接进入正题,对这几个函数进行剖析和讲解. 1. ...

  2. 【UNIX网络编程第三版】阅读笔记(一):代码环境搭建

    粗略的阅读过<TCP/IP详解>和<计算机网络(第五版)>后,开始啃这本<UNIX网络编程卷一:套接字联网API>,目前linux下的编程不算太了解,在阅读的过程中 ...

  3. 【unix网络编程第三版】ubuntu端口占用问题

    <unix网络编程>一书中的代码并不是能直接运行,有时候需要结合各方面的知识来解决,大家在这本书的时候,一定要把代码都跑通,不难你会错过很多学习的机会! 1.问题描述 本人在阅读<U ...

  4. 【unix网络编程第三版】阅读笔记(二):套接字编程简介

    unp第二章主要将了TCP和UDP的简介,这些在<TCP/IP详解>和<计算机网络>等书中有很多细致的讲解,可以参考本人的这篇博客[计算机网络 第五版]阅读笔记之五:运输层,这 ...

  5. 【unix网络编程第三版】阅读笔记(四):TCP客户/服务器实例

    本篇博客主要记录一个完整的TCP客户/服务器实例的编写,以及从这个实例中引发的对僵死进程的处理等问题. 1. TCP客户/服务器功能需求 本实例完成以下功能: (1) 客户从标准输入读入一行文本,并写 ...

  6. UNIX 网络编程第三版

    第五章p102: ps -t  pts/6 -o pid,ppid,tty,stat,args,wchan 在我的系统上运行时出现:TTY not found linux发行版为mint17.1 改用 ...

  7. unix网络编程第三版源代码ubuntu下配置的问题解决

    第一步:首先下载本书配套的源码unpv13e.tar.gz 第二步:解压后进入根文件夹有一个README 4 Execute the following from the src/ directory ...

  8. Unix网络编程第三版源码编译

    配置: $ cd Unix-Network-Programming/ $ chmod 755 configure $ ./configure 主要的工作是检查系统是否有源码编译所依赖的各种资源(系统版 ...

  9. unix网络编程第2版(卷1)_第6章_同步_异步

    第6章 I/O复用:select和poll函数 6.1概述 在5.12节中,我们看到TCP客户同时处理两个输入:标准输入和TCP套接口.我们遇到的问题是客户阻塞于(标准输入上的)fgets调用,而服务 ...

随机推荐

  1. Python与C的简单比较(Python3.0)

    Python可以说是目前最火的语言之一了,人工智能的兴起让Python一夜之间变得家喻户晓,Python号称目前最最简单易学的语言,现在有不少高校开始将Python作为大一新生的入门语言.本萌新也刚开 ...

  2. angular 路由的引用

    使用angular路由 遇到的坑. 使用cmd 安装好node.js 安装成功 输入node  -v 能查看版本说明安装成功 搭建angular项目输入命令 npm install  -g  angu ...

  3. C# 虹软SDK视频人脸识别和注册

    一,准备工作 1.Afoge视频参数类 using AForge.Video.DirectShow; using System; using System.Collections.Generic; u ...

  4. Oracle 11g 中SQL性能优化新特性之SQL性能分析器(SQLPA)

    Oracle11g中,真实应用测试选项(the Real Application Testing Option)提供了一个有用的特点,叫SQL性能分析器(SQL Performance Analyze ...

  5. ubuntu部署mipsel64交叉编译环境

    最近找到个不错的交叉工具链,据传能够编译mipsel64的程序,决定试试. 首先当然是安装环境: apt install -y gcc libncursesada3-dev 下载,解压,进入 三部曲: ...

  6. PHP If...Else 语句

    PHP If...Else 语句 条件语句用于根据不同条件执行不同动作. PHP 条件语句 当您编写代码时,您常常需要为不同的判断执行不同的动作.您可以在代码中使用条件语句来完成此任务. 在 PHP ...

  7. Docker的Fig 项目

    在你的应用里面添加一个 fig.yml 文件,并指定一些简单的内容,执行 fig up 它就能帮你快速建立起一个容器 快速搭建基于 Docker 的隔离开发环境 使用 Dockerfile 文件指定你 ...

  8. Docker标准化开发测试和生产环境

    对于大部分企业来说,搭建 PaaS 既没有那个精力,也没那个必要,用 Docker 做个人的 sandbox 用处又小了点. 可以用 Docker 来标准化开发.测试.生产环境. Docker 占用资 ...

  9. Python3 循环

    Python中的循环语句有 for 和 while. Python循环语句的控制结构图如下所示: while 循环 Python中while语句的一般形式: while 判断条件: statement ...

  10. Python 3.3.2 round函数并非"四舍五入"

    对于一些貌似很简单常见的函数,最好还是去读一下Python文档,否则当你被某个BUG折磨得死去活来时,还不知根源所在.尤其是Python这种不断更新的语言.(python 2.7 的round和3.3 ...