• 高级I/O之异步I/O


    A synchronous I/O operation causes the requesting process to be blocked until that I/O operation completes;

    An asynchronous I/O operation does not cause the requesting process to be blocked;

    使用http://www.cnblogs.com/nufangrensheng/p/3557584.html中说明的select和poll可以实现异步形式的通知。关于描述符的状态,系统并不主动告诉我们任何信息,我们需要进行查询(调用select或poll)。如在信号章节中所述,信号机构提供一种以异步形式通知某种事件已发生的方法。由BSD和系统V派生的所有系统提供了使用一个信号(在系统V中是SIGPOLL,在BSD中是SIGIO)的异步I/O方法,该信号通知进程某个描述符已经发生了所关心的某个事件

    我们已经了解到select和poll对任意描述符都能工作。但是关于异步I/O却有限制。在系统V派生的系统中,异步I/O只对STREAMS设备和STREAMS管道起作用。在BSD派生的系统中,异步I/O只对终端和网络起作用。

    异步I/O的一个限制是每个进程只有一个信号。如果要对几个描述符进行异步I/O,那么在进程接收到该信号时并不知道这一信号对应于哪一个描述符。

    Single UNIX Specification 版本4 将实时扩展(real-time extension)中的通用异步I/O机制(general asynchronous I/O mechanism)添加到了基本规格(base specification)中。该机制解决了老式的异步I/O设施中存在的一些限制。

    在我们学习使用异步I/O的不同方法之前,需要讨论一下使用异步I/O的代价。使用异步I/O会使得我们的设计变得复杂,因为此时需要同时应付许多同时发生的操作。一个简单的解决方法是使用多线程,这使得我们可以用同步模型(synchronous model)来编写程序,而各个线程之间则以异步方式运行。

    当使用POSIX 异步I/O接口时,将会更加复杂:

    • 对于每一个异步操作我们需要考虑三个出错源:一个与操作的提交(the submission of the operation)有关;一个与操作自身的结果有关;另一个与使用的确定异步操作的状态的函数有关。
    • 与传统的接口相比,这些接口本身涉及大量额外的设置和规则处理。

    We can’t really call the non-asynchronous I/O function calls ‘‘synchronous,’’ because
    although they are synchronous with respect to the program flow, they aren’t
    synchronous with respect to the I/O. Recall the discussion of synchronous writes in
    Chapter 3. We call a write ‘‘synchronous’’ if the data we write is persistent when we
    return from the call to the write function. We also can’t differentiate the
    conventional I/O function calls from the asynchronous ones by referring to the
    conventional calls as the ‘‘standard’’ I/O calls, because this confuses them with the
    function calls in the standard I/O library. To avoid confusion, we’ll refer to the
    read and write functions as the ‘‘conventional’’ I/O function calls in this section.

    • 很难从错误中恢复。例如,我们提交了多个异步写并且其中一个出错,此时我们该如何处理?如果这些异步写操作是相互联系的,可能我们需要撤销那些成功的异步写操作。

    1、系统V异步I/O

    在系统V中,异步I/O是STREAMS系统的一部分。它只对STREAMS设备和STREAMS管道起作用。系统V的异步I/O信号是SIGPOLL

    为了对一个STREAMS设备启动异步I/O,需要调用ioctl,它的第二个参数(request)是I_SETSIG。第三个参数是由表14-7中的常量构成的整型值。这些常量在<stropts.h>中定义。

                                                       表14-7 产生SIGPOLL信号的条件

             常量                                                                    说明
    S_INPUT
    S_RDNORM
    S_RDBAND
    S_BANDURG
    S_HIPRI
    非高优先级消息已到达
    普通消息已到达
    非0优先级波段消息已到达
    若此常量和S_RDBAND一起指定,则当一非0优先级波段消息已到达时,产生SIGURG信号而非SIGPOLL
    高优先级消息已到达
    S_OUTPUT
    S_WRNORM
    S_WRBAND
    写队列不再满
    与S_OUTPUT相同
    可发送非0优先级波段消息
    S_MSG
    S_ERROR
    S_HANGUP
    包含SIGPOLL信号的STREAMS信号消息已到达
    M_ERROR消息已到达
    M_HANGUP消息已到达

    表14-7中 “已到达” 的意思是“已到达流首的读队列”。

    除了调用ioctl说明产生SIGPOLL信号的条件以外,还应为该信号建立信号处理程序。回忆表10-1(http://www.cnblogs.com/nufangrensheng/p/3514157.html),对于SIGPOLL的默认动作是终止该进程,所以应当在调用ioctl之前建立信号处理程序。

    2、BSD异步I/O

    在BSB派生的系统中,异步I/O是SIGIO和SIGURG两个信号的组合。前者是通用异步I/O信号,后者则用来通知进程在网络连接上到达了带外的数据http://blog.chinaunix.net/uid-27164517-id-3275870.html)。为了接收SIGIO信号,需执行下列三步:

    (1)调用signal或sigaction为SIGIO信号建立信号处理程序。

    (2)以命令F_SETOWN(见http://www.cnblogs.com/nufangrensheng/p/3500350.html)调用fcntl来设置进程ID和进程组ID,它们将接收对于该描述符的信号。

    (3)以命令F_SETFL调用fcntl设置O_ASYNC文件状态标志,使在该描述符上可以进行异步I/O(见http://www.cnblogs.com/nufangrensheng/p/3500350.html中的表3-3)。

    第(3)步仅能对指向终端或网络的描述符执行,这是BSD异步I/O设施的一个基本限制。

    对于SIGURG信号,只需执行第(1)步和第(2)步。该信号仅对引用支持带外数据的网络连接描述符而产生。

    3、POSIX异步I/O(第三版新增)

    POSIX异步I/O接口给我们提供了一个一致的方法来执行异步I/O,而且无需考虑文件的类型。这些接口采纳自实时草案标准(real-time draft standard)。Single UNIX Specification 版本4将这些接口加入到了基准当中(real-time draft standard--->base specification),所以现在所有的平台都必须支持这些接口。

    这些异步I/O接口使用AIO控制块来描述I/O操作。使用aiocb结构体定义一个AIO控制块。aiocb结构至少包括如下所示的字段(不同的实现可能还会包含额外的字段):

    struct aiocb {
        int              aio_fildes;        /* file descriptor */
        off_t            aio_offset;        /* file offset for I/O */
        volatile void   *aio_buf;           /* buffer for I/O */
        size_t           aio_nbytes;        /* number of bytes to transfer */
        int              aio_reqprio;       /* priority */
        struct sigevent  aio_sigevent;      /* signal information */
        int              aio_lio_opcode;    /* operation for list I/O */
    };

    aio_fildes字段是为读或写文件而打开的文件描述符从aio_offset指定的偏移量处开始读或写对于读,数据被拷贝到由aio_buf指定的缓冲区中。对于写,则是从aio_buf指定的缓冲区中拷贝数据aio_nbytes字段包含了要读或写的字节数

    注意,我们执行异步I/O时必须提供一个明确的偏移量(offset)。异步I/O接口不会影响由操作系统所维持的文件偏移量。这并不会成为问题,只要我们不把同一进程中作用在同一文件上的异步I/O函数和传统的I/O函数相混淆。同时还要注意,如果我们向一个以追加模式打开(O_APPEND)的文件使用异步I/O接口写时,AIO控制块中的aio_offset字段将会被系统忽略。

    其余的字段则与传统I/O函数不相关。应用程序可以根据aio_reqprio提供的优先级对异步I/O请求进行排序。然而,系统对于精确的排序只具有有限的控制权,所以并不一定完全遵从aio_reqprio给出的优先级。aio_lio_opcode字段只适用于基于列表(list-based)的异步I/O。aio_sigevent字段控制如何通知应用程序I/O事件已完成。该字段用sigevent结构描述:

    struct sigevent {
        int            sigev_notify;               /* notify type */
        int            sigev_signo;               /* signal number */
        union sigval     sigev_value;               /* notify argument */
        void (*sigev_notify_function)(union sigval);    /* notify function */
        pthread_attr_t   *sigev_notify_attributes;      /* notify attrs */
    };

    sigev_notify控制通知的类型。它可以取如下三个值:

    SIGEV_NONE            异步I/O请求完成时不通知进程。

    SIGEV_SIGNAL         异步I/O请求完成时产生由sigev_signo字段指定的信号。如果应用程序选择捕获信号,并在建立信号处理程序时指定了SA_SIGINFO标志,则该信号被排队(如果实现支持信号队列)。将si_value字段设置为sigev_value的siginfo结构体传递给信号处理程序(再次声明,如果使用了SA_SIGINFO标志)。

    SIGEV_THREAD        异步I/O请求完成时由sigev_notify_function字段指定的函数将会被调用。并且sigev_value字段作为该函数唯一的参数。该函数在另外一个线程中以分离状态执行,除非sigev_notify_attributes字段设置为一个线程属性结构地址,且该线程属性结构指定了其他的线程属性。

    为了执行异步I/O,我们首先需要初始化一个AIO控制块,然后调用aio_read函数进行异步读或调用aio_write函数进行异步写。

    #include <aio.h>
    int aio_read(struct aiocb *aiocb);
    int aio_write(struct aiocb *aiocb);
    两函数返回值:若成功则返回0,出错则返回-1

    如果异步I/O请求已经被操作系统排队以等待处理,则aio_read和aio_write成功返回。这两个函数的返回值与实际I/O操作的结果并无关系。当I/O操作挂起时,我们必须小心,以确保AIO控制块和数据缓冲区保持稳定;它们的基本内存必须保持有效,而且我们不能重用它们直到I/O操作完成。

    如果想要强制所有挂起的异步写持久性存储(To force all pending asynchronous writes to persistent storage)而无需等待,我们可以设立一个AIO控制块并且调用aio_fsync函数。

    #include <aio.h>
    int aio_fsync(int op, struct aiocb *aiocb);
    返回值:若成功则返回0,出错则返回-1

    AIO控制块中的aio_fildes字段指示了一个文件,该文件的异步写是同步的(当然是调用了aio_fsync之后)。如果op参数被设置为O_DSYNC,那么就如同调用fdatasync。如果op被设置为O_SYNC,那么就如同调用fsync。

    如同aio_read和aio_write函数,aio_fsync也是在操作系统把同步操作被加入任务计划后才成功返回。直到异步sync操作完成,数据才会是持久性的。AIO控制块控制着我们如何被通知,就像aio_read和aio_write函数一样。

    为了确定异步读、写和同步操作的完成状态,我们需要调用aio_error函数。

    #include <aio.h>
    int aio_error(const struct aiocb *aiocb);
    返回值:见下面

    aio_error函数的返回值有四种情况:

    0                        异步操作成功完成。我们需要调用aio_return函数从操作中获取返回值。

    -1                       aio_error调用失败。错误原因设置在errno。

    EINPROGRESS    异步读、写和同步操作在挂起中。

    anything else     其他任何返回值给出了相应的异步操作的错误代码。

    如果异步操作成功,我们可以调用aio_return函数来获取异步操作的返回值。

    #include <aio.h>
    ssize_t aio_return(const struct aiocb *aiocb);
    返回值:见下面

    我们必须在异步操作完成后再调用aio_return函数。如果在异步操作完成之前调用aio_return函数,那么结果是未定义的。另外,还要注意对每一个异步I/O操作,我们只能调用aio_return函数一次。一旦我们调用了aio_return函数,操作系统就可以自由释放包含I/O操作返回值的记录。

    如果aio_return函数自身失败,则会返回-1并且设置errno。否则它会返回异步操作的结果,在这种情况下,异步读、写和同步操作成功返回什么aio_return就返回什么。

    如果我们有其他的处理(processing)要做,而且不希望在执行I/O操作的时候阻塞这些处理,这时我们可以使用异步I/O。当我们完成了这些处理,但是发现还有未完成的异步操作,此时,我们可以调用aio_suspend函数阻塞直到有一个异步I/O操作完成。

    #include <aio.h>
    int aio_suspend(const struct aiocb *const list[], int nent,
                    const struct tiimespec *timeout);
    返回值:若成功则返回0,出错则返回-1

    有三种情况可以导致aio_suspend返回。如果被信号中断,则返回-1,并把errno设置为EINTR。如果在任何I/O操作完成之前,timeout超时,则返回-1,并把errno设置为EAGAIN(we can pass a null pointer for the timeout argument if we want to block without a time limit)。如果有任何一个I/O操作完成,则aio_suspend返回0。如果当我们调用aio_suspend的时候,所有的异步I/O操作全部都已经完成了,那么aio_suspend将无阻塞地返回。

    list参数是一个指针数组(这些指针指向AIO控制块),nent指示该指针数组中指针的个数。该指针数组中的空指针会被跳过,其他的指针则必须指向用来初始化异步I/O操作的AIO控制块。

    当我们挂起了那些我们不再想要完成的异步I/O操作,我们可以使用aio_cancel函数试图取消它们

    #include <aio.h>
    int aio_cancel(int fd, struct aiocb *aiocb);
    返回值:见下面

    其中,参数fd说明了带有未完成的异步I/O操作的文件描述符。如果aiocb为NULL,那么系统将试图取消在该文件上的所有未完成的异步I/O操作。否则,系统试图取消由AIO控制块描述的单个异步I/O操作。这里我们只是说“试图”取消,因为不能保证系统能够取消正在进行中的任何操作。

    aio_cancel函数有如下四种返回值:

    AIO_ALLDONE              在试图取消之前,所有的操作都已完成。

    AIO_CANCELED            所有请求的操作都已经被取消。

    AIO_NOTCANCELED     至少有一个请求的操作无法被取消。

    -1                                 调用aio_cancel失败。相应的错误编号存入errno。

    如果一个异步I/O操作被成功取消了,然后在相应的AIO控制块上调用aio_error函数将会返回错误ECANCELED。如果操作不能被取消,那么aio_cancel不会修改相应的AIO控制块。

    lio_listio函数也作为异步I/O接口之一,虽然它既可以以异步方式使用又可以以同步方式使用。lio_listio函数提交一个用AIO控制块列表描述的I/O请求集

    #include <aio.h>
    int lio_listio(int mode, struct aiocb *restrict const list[restrict],
              int nent, struct sigevent *restrict sigev);
    返回值:若成功则返回0,出错则返回-1

    其中,mode参数决定I/O是否是真正的异步。当把mode设置为LIO_WAIT,由list指定的所有的I/O操作全部完成时lio_listio函数才返回。在这种情况下,参数sigev被忽略。当mode参数设置为LIO_NOWAIT,一旦I/O请求排队后lio_listio函数立即返回(并不等待其完成)。当所有的I/O操作全部完成后,根据sigev参数异步地通知进程。如果我们不想被通知,可以将sigev设置为NULL。注意,单个的AIO控制块在单个操作完成时也可能会使能异步通知。由sigev参数指定的异步通知并不包括这些,仅当所有的I/O操作完成后才会发送由sigev参数指定的异步通知。

    list参数指向一个AIO控制块列表,具体说明了要执行的I/O操作。nent参数具体说明了数组中元素的个数。AIO控制块列表中可以包含空指针;这些空指针会被忽略。

    在每一个AIO控制块中,aio_lio_opcode字段具体说明了操作是读(LIO_READ)、是写(LIO_WRITE)、还是no-op(LIO_NOP)(该操作将被忽略)。A read i-s treated as if the corresponding AIO control block had been passed to the aio_read function. Similarly, a write is treated as if the AIO control bloc-k had been passed to aio_write.

    实现可以限制允许未完成的(悬的,outstanding)异步I/O操作的数量。这些限制是运行时变量,总结如下表:

    未命名

    We can determine the value of AIO_LISTIO_MAX by calling the sysconf function
    with the name argument set to _SC_IO_LISTIO_MAX. Similarly, we can determine the
    value of AIO_MAX by calling sysconf with the name argument set to _SC_AIO_MAX,
    and we can get the value of AIO_PRIO_DELTA_MAX by calling sysconf with its
    argument set to _SC_AIO_PRIO_DELTA_MAX.
    The POSIX asynchronous I/O interfaces were originally introduced to provide realtime
    applications with a way to avoid being blocked while performing I/O operations.

    Now we’ll look at an example of how to use the interfaces.

    实例

    We don’t discuss real-time programming in this text, but because the POSIX
    asynchronous I/O interfaces are now part of the base specification in the Single UNIX
    Specification, we’ll look at how to use them. To compare the asynchronous I/O
    interfaces with their conventional counterparts, we’ll look at the task of translating a file
    from one format to another.
    The program shown in Figure 14.20 translates a file using the ROT-13 algorithm(关于ROT-13算法可参考http://zh.wikipedia.org/wiki/ROT13)
    that the USENET news system, popular in the 1980s, used to obscure text that might be
    offensive or contain spoilers or joke punchlines. The algorithm rotates the characters ’a’
    to ’z’ and ’A’ to ’Z’ by 13 positions, but leaves all other characters unchanged.

    Figure 14.20 Translate a file using ROT-13

    #include "apue.h"
    #include <ctype.h>
    #include <fcntl.h>
    
    #define BSZ    4096
    
    unsigned char buf[BSZ];
    
    unsigned char 
    translate(unsigned char c)
    {
        if(isalpha(c))
        {
            if(c >= 'n')
                c -= 13;
            else if(c >= 'a')
                c += 13;
            else if(c >= 'N')
                c -= 13;
            else 
                c += 13;
        }
        return(c);
    }
    
    int
    main(int argc, char* argv[])
    {
        int ifd, ofd, i, n, nw;
        
        if(argc != 3)
            err_quit("usage: rot13 infile outfile");
        if((ifd = open(argv[1], O_RDONLY)) < 0)
            err_sys("can't open %s", argv[1]);
        if((ofd = open(argv[2], O_RDWR|O_CREAT|O_TRUNC, FILE_MODE)) < 0)
            err_sys("can't create %s", argv[2]);
    
        while((n = read(ifd, buf, BSZ)) > 0)
        {
            for(i = 0; i < n; i++)
                buf[i] = translate(buf[i]);
            if((nw = write(ofd, buf, n)) != n)
            {
                if(nw < 0)
                    err_sys("write failed");
                else
                    err_quit("short write (%d/%d)", nw, n);
            }
        }
        fsync(ofd);
        exit(0);
    }

    The I/O portion of the program is straightforward: we read a block from the input
    file, translate it, and then write the block to the output file. We repeat this until we hit
    the end of file and read returns zero. The program in Figure 14.21 shows how to
    perform the same task using the equivalent asynchronous I/O functions.

    Figure 14.21 Translate a file using ROT-13 and asynchronous I/O

    #include "apue.h"
    #include <ctype.h>
    #include <fcntl.h>
    #include <aio.h>
    #include <errno.h>
    
    #define BSZ    4096
    #define NBUF    8
    
    enum rwop {
        UNUSED = 0,
        READ_PENDING = 1,
        WRITE_PENDING = 2
    };
    
    struct buf {
        enum rwop       op;
        int             last;
        struct aiocb    aiocb;
        unsigned char   data[BSZ];
    };
    
    struct buf bufs[NBUF];
    
    unsigned char 
    translate(unsigned char c)
    {
        if(isalpha(c))
        {
            if(c >= 'n')
                c -= 13;
            else if(c >= 'a')
                c += 13;
            else if(c >= 'N')
                c -= 13;
            else 
                c += 13;
        }
        return(c);
    }
    
    int 
    main(int argc, char* argv[])
    {
        int                    ifd, ofd, i, j, n, err, numop;
        struct stat            sbuf;
        const struct aiocb    *aiolist[NBUF];
        off_t                  off = 0;
    
        if(argc != 3)
            err_quit("usage: rot13 infile outfile");
        if((ifd = open(argv[1], O_RDONLY)) < 0)
            err_sys("can't open %s", argv[1]);
        if((ofd = open(argv[2], O_RDWR|O_CREAT|O_TRUNC, FILE_MODE)) < 0)
            err_sys("can't create %s", argv[2]);
        if(fstat(ifd, &sbuf) < 0)
            err_sys("fstat failed");
    
        /* initialize the buffers */
        for(i = 0; i < NBUF; i++)
        {
            bufs[i].op = UNUSED;
            bufs[i].aiocb.aio_buf = bufs[i].data;
            bufs[i].aiocb.aio_sigevent.sigev_notify = SIGEV_NONE;
            aiolist[i] = NULL; 
        } 
        
        numop = 0;
        for(;;)
        {
            for(i = 0; i < NBUF; i++)
            {
                switch(bufs[i].op)
                {
                    case UNUSED:
                        /*
                        * Read from the input file if more data
                        * remains unread.
                        */
                        if(off < sbuf.st_size)
                        {    
                            bufs[i].op = READ_PENDING;
                            bufs[i].aiocb.aio_fildes = ifd;
                            bufs[i].aiocb.aio_offset = off;
                            off += BSZ;
                            if(off >= sbuf.st_size)
                                bufs[i].last = 1;
                            bufs[i].aiocb.aio_nbytes = BSZ;
                            if(aio_read(&bufs[i].aiocb) < 0)
                                err_sys("aio_read failed");
                            aiolist[i] = &bufs[i].aiocb;
                            numop++;
                        }
                        break;
    
                    case READ_PENDING:
                        if((err = aio_error(&bufs[i].aiocb)) == EINPROGRESS)
                            continue;
                        if(err != 0)
                        {    
                            if(err == -1)
                                err_sys("aio_error failed");
                            else
                                err_exit(err, "read failed");
                        }
                        /*
                        * A read is complete; translate the buffer
                        * and write it. 
                        */
                        if((n = aio_return(&bufs[i].aiocb)) < 0)
                            err_sys("aio_return failed");
                        if(n != BSZ && !bufs[i].last)
                            err_quit("short read (%d/%d)", n, BSZ);
                        for(j = 0; j < n; j++)
                            bufs[i].data[j] = translate(bufs[i].data[j]);
                        bufs[i].op = WRITE_PENDING;
                        bufs[i].aiocb.aio_fildes = ofd;
                        bufs[i].aiocb.aio_nbytes = n;
                        if(aio_write(&bufs[i].aiocb) < 0)
                            err_sys("aio_write failed");
                        /*  return our spot in aiolist */
                        break;
                    
                    case WRITE_PENDING:
                        if((err = aio_error(&bufs[i].aiocb)) == EINPROGRESS)
                            continue;
                        if(err != 0)    
                        {
                            if(err == -1)
                                err_sys("aio_error failed");
                            else
                                err_exit(err, "write failed");
                        }
                        
                        /*
                        * A write is complete; mark the buffer as unused.
                        */
                        if((n = aio_return(&bufs[i].aiocb)) < 0)
                            err_sys("aio_return failed");
                        if(n != bufs[i].aiocb.aio_nbytes)
                            err_quit("short write (%d/%d)", n, BSZ);
                        aiolist[i] = NULL;
                        bufs[i].op = UNUSED;
                        numop--;
                        break;
                }
            }
            if(numop == 0)
            {
                if(off >= sbuf.st_size)
                    break;
            }
            else    
            {
                if(aio_suspend(aiolist, NBUF, NULL) < 0)
                    err_sys("aio_suspend failed");
            }
        }
        
        bufs[0].aiocb.aio_fildes = ofd;
        if(aio_fsync(O_SYNC, &bufs[0].aiocb) < 0)
            err_sys("aio_fsync failed");
        exit(0);
    }

    编译此程序时,需要在编译选项中加上 -lrt,否则会出现“undefined reference to ‘aio_xxx’”这样的错误。(参考自http://hi.baidu.com/catproste2012/item/04eab0ee76afe0d2eb34c914

    Note that we use eight buffers, so we can have up to eight asynchronous I/O
    requests pending. Surprisingly, this might actually reduce performance—if the reads
    are presented to the file system out of order, it can defeat the operating system’s readahead
    algorithm.
    Before we can check the return value of an operation, we need to make sure the
    operation has completed.
    When aio_error returns a value other than EINPROGRESS
    or −1, we know the operation is complete
    . Excluding these values(EINPROGRESS, -1), if the return value is
    anything other than 0, then we know the operation failed
    . Once we’ve checked these
    conditions, it is safe to call aio_return to get the return value of the I/O operation.
    As long as we have work to do, we can submit asynchronous I/O operations.
    When we have an unused AIO control block, we can submit an asynchronous read
    request. When a read completes, we translate the buffer contents and then submit an
    asynchronous write request. When all AIO control blocks are in use, we wait for an
    operation to complete by calling aio_suspend.
    When we write a block to the output file, we retain the same offset at which we read
    the data from the input file. Consequently, the order of the writes doesn’t matter.
    This
    strategy works only because each character in the input file has a corresponding
    character in the output file at the same offset; we neither add nor delete characters in the
    output file.

    We don’t use asynchronous notification in this example, because it is easier to use a
    synchronous programming model. If we had something else to do while the I/O
    operations were in progress, then the additional work could be folded into the for
    loop. If we needed to prevent this additional work from delaying the task of translating
    the file, however, then we might have to structure the code to use some form of
    asynchronous notification. With multiple tasks, we need to prioritize the tasks before
    deciding how the program should be structured.

    本篇博文内容摘自《UNIX环境高级编程》(第二版),仅作个人学习记录所用。关于本书可参考:http://www.apuebook.com/

  • 相关阅读:
    2017-2018-1 20155338 《信息安全系统设计基础》 第三周学习总结
    2017-2018-1 20155338 《信息安全系统设计基础》 第二周课堂测试
    2017-2018-1 20155338 《信息安全系统设计基础》第1周学习总结
    20155338 2016-2017-2 《JAVA程序设计》课程总结
    20155338 《JAVA程序设计》实验五网络编程与安全实验报告
    20155338 2016-2017-2《Java程序设计》实验四Android程序开发实验报告
    20155338 《Java程序设计》实验三(敏捷开发与XP实践)实验报告
    20155338 2016-2017-2 《Java程序设计》第10周学习总结
    【私人向】Java复习笔记
    2017-2018-1 20155316 《信息安全系统设计基础》第2周学习总结
  • 原文地址:https://www.cnblogs.com/nufangrensheng/p/3558505.html
Copyright © 2020-2023  润新知