AIO 简介

Linux 异步 I/O 是 Linux 内核中提供的一个相当新的增强。它是 2.6 版本内核的一个标准特性,但是我们在 2.4 版本内核的补丁中也可以找到它。AIO 背后的基本思想是允许进程发起很多 I/O 操作,而不用阻塞或等待任何操作完成。稍后或在接收到 I/O 操作完成的通知时,进程就可以检索 I/O 操作的结果。

I/O 操作。这个 API 的展示显示了如何使用它。

AIO API

AIO 接口的 API 非常简单,但是它为数据传输提供了必需的功能,并给出了两个不同的通知模型。表 1 给出了 AIO 的接口函数,本节稍后会更详细进行介绍。

表 1. AIO 接口 API

API 函数 说明
aio_read 请求异步读操作
aio_error 检查异步请求的状态
aio_return 获得完成的异步请求的返回状态
aio_write 请求异步写操作
aio_suspend 挂起调用进程,直到一个或多个异步请求已经完成(或失败)
aio_cancel 取消异步 I/O 请求
lio_listio 发起一系列 I/O 操作

每个 API 函数都使用 aiocb 结构开始或检查。这个结构有很多元素,但是清单 1 仅仅给出了需要(或可以)使用的元素。

清单 1. aiocb 结构中相关的域

1

2

3

4

5

6

7

8

9

10

11

12

struct aiocb {

  int aio_fildes;               // File Descriptor

  int aio_lio_opcode;           // Valid only for lio_listio (r/w/nop)

  volatile void *aio_buf;       // Data Buffer

  size_t aio_nbytes;            // Number of Bytes in Data Buffer

  struct sigevent aio_sigevent; // Notification Structure

  /* Internal fields */

  ...

};

sigevent 结构告诉 AIO 在 I/O 操作完成时应该执行什么操作。我们将在 AIO 的展示中对这个结构进行探索。现在我们将展示各个 AIO 的 API 函数是如何工作的,以及我们应该如何使用它们。

aio_read

aio_read 函数请求对一个有效的文件描述符进行异步读操作。这个文件描述符可以表示一个文件、套接字甚至管道。aio_read 函数的原型如下:

1

int aio_read( struct aiocb *aiocbp );

aio_read 函数在请求进行排队之后会立即返回。如果执行成功,返回值就为 0;如果出现错误,返回值就为 -1,并设置 errno 的值。

要执行读操作,应用程序必须对 aiocb 结构进行初始化。下面这个简短的例子就展示了如何填充 aiocb 请求结构,并使用 aio_read 来执行异步读请求(现在暂时忽略通知)操作。它还展示了 aio_error 的用法,不过我们将稍后再作解释。

清单 2. 使用 aio_read 进行异步读操作的例子

#include <aio.h>...int fd, ret;struct aiocb my_aiocb;fd = open( "file.txt", O_RDONLY );if (fd < 0) perror("open");/* Zero out the aiocb structure (recommended) */bzero( (char *)&my_aiocb, sizeof(struct aiocb) );/* Allocate a data buffer for the aiocb request */my_aiocb.aio_buf = malloc(BUFSIZE+1);if (!my_aiocb.aio_buf) perror("malloc");/* Initialize the necessary fields in the aiocb */my_aiocb.aio_fildes = fd;my_aiocb.aio_nbytes = BUFSIZE;my_aiocb.aio_offset = 0;ret = aio_read( &my_aiocb );if (ret < 0) perror("aio_read");while ( aio_error( &my_aiocb ) == EINPROGRESS ) ;if ((ret = aio_return( &my_iocb )) > 0) {/* got ret bytes on the read */} else {/* read failed, consult errno */}

在清单 2 中,在打开要从中读取数据的文件之后,我们就清空了 aiocb 结构,然后分配一个数据缓冲区。并将对这个数据缓冲区的引用放到 aio_buf 中。然后,我们将 aio_nbytes 初始化成缓冲区的大小。并将 aio_offset 设置成 0(该文件中的第一个偏移量)。我们将 aio_fildes 设置为从中读取数据的文件描述符。在设置这些域之后,就调用 aio_read 请求进行读操作。我们然后可以调用 aio_error 来确定 aio_read 的状态。只要状态是 EINPROGRESS,就一直忙碌等待,直到状态发生变化为止。现在,请求可能成功,也可能失败。

使用 AIO 接口来编译程序

我们可以在 aio.h 头文件中找到函数原型和其他需要的符号。在编译使用这种接口的程序时,我们必须使用 POSIX 实时扩展库(librt)。

注意使用这个 API 与标准的库函数从文件中读取内容是非常相似的。除了 aio_read 的一些异步特性之外,另外一个区别是读操作偏移量的设置。在传统的 read 调用中,偏移量是在文件描述符上下文中进行维护的。对于每个读操作来说,偏移量都需要进行更新,这样后续的读操作才能对下一块数据进行寻址。对于异步 I/O 操作来说这是不可能的,因为我们可以同时执行很多读请求,因此必须为每个特定的读请求都指定偏移量。

aio_error

aio_error 函数被用来确定请求的状态。其原型如下:

1

int aio_error( struct aiocb *aiocbp );

这个函数可以返回以下内容:

  • EINPROGRESS,说明请求尚未完成
  • ECANCELLED,说明请求被应用程序取消了
  • -1,说明发生了错误,具体错误原因可以查阅 errno

aio_return

异步 I/O 和标准块 I/O 之间的另外一个区别是我们不能立即访问这个函数的返回状态,因为我们并没有阻塞在 read 调用上。在标准的 read 调用中,返回状态是在该函数返回时提供的。但是在异步 I/O 中,我们要使用 aio_return 函数。这个函数的原型如下:

1

ssize_t aio_return( struct aiocb *aiocbp );

只有在 aio_error 调用确定请求已经完成(可能成功,也可能发生了错误)之后,才会调用这个函数。aio_return 的返回值就等价于同步情况中 read 或 write 系统调用的返回值(所传输的字节数,如果发生错误,返回值就为 -1)。

aio_write

aio_write 函数用来请求一个异步写操作。其函数原型如下:

1

int aio_write( struct aiocb *aiocbp );

aio_write 函数会立即返回,说明请求已经进行排队(成功时返回值为 0,失败时返回值为 -1,并相应地设置 errno)。

这与 read 系统调用类似,但是有一点不一样的行为需要注意。回想一下对于 read 调用来说,要使用的偏移量是非常重要的。然而,对于 write 来说,这个偏移量只有在没有设置 O_APPEND 选项的文件上下文中才会非常重要。如果设置了 O_APPEND,那么这个偏移量就会被忽略,数据都会被附加到文件的末尾。否则,aio_offset 域就确定了数据在要写入的文件中的偏移量。

aio_suspend

我们可以使用 aio_suspend 函数来挂起(或阻塞)调用进程,直到异步请求完成为止,此时会产生一个信号,或者发生其他超时操作。调用者提供了一个 aiocb 引用列表,其中任何一个完成都会导致 aio_suspend 返回。 aio_suspend 的函数原型如下:

1

int aio_suspend( const struct aiocb *const cblist[], int n, const struct timespec *timeout );

aio_suspend 的使用非常简单。我们要提供一个 aiocb 引用列表。如果任何一个完成了,这个调用就会返回 0。否则就会返回 -1,说明发生了错误。请参看清单 3。

清单 3. 使用 aio_suspend 函数阻塞异步 I/O

struct aioct *cblist[MAX_LIST]/* Clear the list. */
bzero( (char *)cblist, sizeof(cblist) );/* Load one or more references into the list */
cblist[0] = &my_aiocb;ret = aio_read( &my_aiocb );ret = aio_suspend( cblist, MAX_LIST, NULL );

注意,aio_suspend 的第二个参数是 cblist 中元素的个数,而不是 aiocb 引用的个数。cblist 中任何 NULL 元素都会被 aio_suspend 忽略。

如果为 aio_suspend 提供了超时,而超时情况的确发生了,那么它就会返回 -1errno 中会包含 EAGAIN

aio_cancel

aio_cancel 函数允许我们取消对某个文件描述符执行的一个或所有 I/O 请求。其原型如下:

1

int aio_cancel( int fd, struct aiocb *aiocbp );

要取消一个请求,我们需要提供文件描述符和 aiocb 引用。如果这个请求被成功取消了,那么这个函数就会返回 AIO_CANCELED。如果请求完成了,这个函数就会返回 AIO_NOTCANCELED

要取消对某个给定文件描述符的所有请求,我们需要提供这个文件的描述符,以及一个对 aiocbp 的 NULL 引用。如果所有的请求都取消了,这个函数就会返回 AIO_CANCELED;如果至少有一个请求没有被取消,那么这个函数就会返回 AIO_NOT_CANCELED;如果没有一个请求可以被取消,那么这个函数就会返回 AIO_ALLDONE。我们然后可以使用 aio_error 来验证每个 AIO 请求。如果这个请求已经被取消了,那么 aio_error 就会返回 -1,并且 errno 会被设置为 ECANCELED

lio_listio

最后,AIO 提供了一种方法使用 lio_listio API 函数同时发起多个传输。这个函数非常重要,因为这意味着我们可以在一个系统调用(一次内核上下文切换)中启动大量的 I/O 操作。从性能的角度来看,这非常重要,因此值得我们花点时间探索一下。lio_listio API 函数的原型如下:

1

2

int lio_listio( int mode, struct aiocb *list[], int nent,

                   struct sigevent *sig );

mode 参数可以是 LIO_WAIT 或 LIO_NOWAITLIO_WAIT 会阻塞这个调用,直到所有的 I/O 都完成为止。在操作进行排队之后,LIO_NOWAIT 就会返回。list 是一个 aiocb 引用的列表,最大元素的个数是由 nent 定义的。注意 list 的元素可以为 NULLlio_listio 会将其忽略。sigevent 引用定义了在所有 I/O 操作都完成时产生信号的方法。

对于 lio_listio 的请求与传统的 read 或 write 请求在必须指定的操作方面稍有不同,如清单 4 所示。

清单 4. 使用 lio_listio 函数发起一系列请求

struct aiocb aiocb1, aiocb2;
struct aiocb *list[MAX_LIST];.../* Prepare the first aiocb */
aiocb1.aio_fildes = fd;
aiocb1.aio_buf = malloc( BUFSIZE+1 );
aiocb1.aio_nbytes = BUFSIZE;
aiocb1.aio_offset = next_offset;
aiocb1.aio_lio_opcode = LIO_READ;...bzero( (char *)list, sizeof(list) );
list[0] = &aiocb1;
list[1] = &aiocb2;ret = lio_listio( LIO_WAIT, list, MAX_LIST, NULL );

对于读操作来说,aio_lio_opcode 域的值为 LIO_READ。对于写操作来说,我们要使用 LIO_WRITE,不过 LIO_NOP 对于不执行操作来说也是有效的。

AIO 通知

现在我们已经看过了可用的 AIO 函数,本节将深入介绍对异步通知可以使用的方法。我们将通过信号和函数回调来探索异步函数的通知机制。

使用信号进行异步通知

使用信号进行进程间通信(IPC)是 UNIX 中的一种传统机制,AIO 也可以支持这种机制。在这种范例中,应用程序需要定义信号处理程序,在产生指定的信号时就会调用这个处理程序。应用程序然后配置一个异步请求将在请求完成时产生一个信号。作为信号上下文的一部分,特定的 aiocb 请求被提供用来记录多个可能会出现的请求。清单 5 展示了这种通知方法。

清单 5. 使用信号作为 AIO 请求的通知

void setup_io( ... )
{int fd;struct sigaction sig_act;struct aiocb my_aiocb;.../* Set up the signal handler */sigemptyset(&sig_act.sa_mask);sig_act.sa_flags = SA_SIGINFO;sig_act.sa_sigaction = aio_completion_handler;/* Set up the AIO request */bzero( (char *)&my_aiocb, sizeof(struct aiocb) );my_aiocb.aio_fildes = fd;my_aiocb.aio_buf = malloc(BUF_SIZE+1);my_aiocb.aio_nbytes = BUF_SIZE;my_aiocb.aio_offset = next_offset;/* Link the AIO request with the Signal Handler */my_aiocb.aio_sigevent.sigev_notify = SIGEV_SIGNAL;my_aiocb.aio_sigevent.sigev_signo = SIGIO;my_aiocb.aio_sigevent.sigev_value.sival_ptr = &my_aiocb;/* Map the Signal to the Signal Handler */ret = sigaction( SIGIO, &sig_act, NULL );...ret = aio_read( &my_aiocb );}void aio_completion_handler( int signo, siginfo_t *info, void *context )
{struct aiocb *req;/* Ensure it's our signal */if (info->si_signo == SIGIO) {req = (struct aiocb *)info->si_value.sival_ptr;/* Did the request complete? */if (aio_error( req ) == 0) {/* Request completed successfully, get the return status */ret = aio_return( req );}}return;
}

在清单 5 中,我们在 aio_completion_handler 函数中设置信号处理程序来捕获 SIGIO 信号。然后初始化 aio_sigevent 结构产生 SIGIO 信号来进行通知(这是通过 sigev_notify 中的 SIGEV_SIGNAL 定义来指定的)。当读操作完成时,信号处理程序就从该信号的 si_value 结构中提取出 aiocb,并检查错误状态和返回状态来确定 I/O 操作是否完成。

对于性能来说,这个处理程序也是通过请求下一次异步传输而继续进行 I/O 操作的理想地方。采用这种方式,在一次数据传输完成时,我们就可以立即开始下一次数据传输操作。

使用回调函数进行异步通知

另外一种通知方式是系统回调函数。这种机制不会为通知而产生一个信号,而是会调用用户空间的一个函数来实现通知功能。我们在 sigevent 结构中设置了对 aiocb 的引用,从而可以惟一标识正在完成的特定请求。请参看清单 6。

清单 6. 对 AIO 请求使用线程回调通知

void setup_io( ... )
{int fd;struct aiocb my_aiocb;.../* Set up the AIO request */bzero( (char *)&my_aiocb, sizeof(struct aiocb) );my_aiocb.aio_fildes = fd;my_aiocb.aio_buf = malloc(BUF_SIZE+1);my_aiocb.aio_nbytes = BUF_SIZE;my_aiocb.aio_offset = next_offset;/* Link the AIO request with a thread callback */my_aiocb.aio_sigevent.sigev_notify = SIGEV_THREAD;my_aiocb.aio_sigevent.notify_function = aio_completion_handler;my_aiocb.aio_sigevent.notify_attributes = NULL;my_aiocb.aio_sigevent.sigev_value.sival_ptr = &my_aiocb;...ret = aio_read( &my_aiocb );}void aio_completion_handler( sigval_t sigval )
{struct aiocb *req;req = (struct aiocb *)sigval.sival_ptr;/* Did the request complete? */if (aio_error( req ) == 0) {/* Request completed successfully, get the return status */ret = aio_return( req );}return;
}

在清单 6 中,在创建自己的 aiocb 请求之后,我们使用 SIGEV_THREAD 请求了一个线程回调函数来作为通知方法。然后我们将指定特定的通知处理程序,并将要传输的上下文加载到处理程序中(在这种情况中,是个对 aiocb 请求自己的引用)。在这个处理程序中,我们简单地引用到达的 sigval 指针并使用 AIO 函数来验证请求已经完成。

对 AIO 进行系统优化

proc 文件系统包含了两个虚拟文件,它们可以用来对异步 I/O 的性能进行优化:

  • /proc/sys/fs/aio-nr 文件提供了系统范围异步 I/O 请求现在的数目。
  • /proc/sys/fs/aio-max-nr 文件是所允许的并发请求的最大个数。最大个数通常是 64KB,这对于大部分应用程序来说都已经足够了。

结束语

使用异步 I/O 可以帮助我们构建 I/O 速度更快、效率更高的应用程序。如果我们的应用程序可以对处理和 I/O 操作重叠进行,那么 AIO 就可以帮助我们构建可以更高效地使用可用 CPU 资源的应用程序。尽管这种 I/O 模型与在大部分 Linux 应用程序中使用的传统阻塞模式都不同,但是异步通知模型在概念上来说却非常简单,可以简化我们的设计。

实例:

#include "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)
{
    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 not open %s", argv[1]);
    if ((ofd = open(argv[2], O_RDWR|O_CREAT|O_TRUNC, FILE_MODE)) < 0)
        err_sys("can not create %s", argv[2]);
    if (fstat(ifd, &sbuf) < 0)
        err_sys("fstat failed");

//初始化NBUF
    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:
                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");
                }
                
                //读取完成;转换缓冲区并写入
                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");
                //保留我们在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");
                }
                
                //写入完成:将缓冲区标记为未使用
                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");
    
    return 0;
}

编译: gcc sample.c -lrt

利用异步I/O复制文件及详解相关推荐

  1. Linux中利用grep命令如何检索文件内容详解

    前言 Linux系统中搜索.查找文件中的内容,一般最常用的是grep命令,另外还有egrep命令,同时vi命令也支持文件内容检索.下面来一起看看Linux利用grep命令检索文件内容的详细介绍. 方法 ...

  2. php js跨域上传文件,Jquery实现跨域异步上传文件步骤详解

    这次给大家带来Jquery实现跨域异步上传文件步骤详解,Jquery实现跨域异步上传文件的注意事项有哪些,下面就是实战案例,一起来看一下. 先说明白 这个跨域异步上传功能我们借助了Jquery.for ...

  3. 43. Systemd的Unit配置详解,unit文件位置,优先级,unit类型,unit文件字段详解,Unit/Service/Install字段,添加mysql服务等例子

    Systemd的Unit配置详解,unit文件位置和优先级,unit文件类型,unit文件字段详解,[Unit]字段,[Service]字段,[Install]字段,添加服务,创建.service 文 ...

  4. mysql回滚用法_Mysql误操作后利用binlog2sql快速回滚的方法详解

    前言 在日常工作或者学习中,操作数据库时候难免会因为"大意"而误操作,需要快速恢复的话通过备份来恢复是不太可能的,下面这篇文章主要给大家介绍关于Mysql误操作后利用binlog2 ...

  5. linux文件权限详解

    linux文件权限详解 一.文件和目录权限概述 在linux中的每一个文件或目录都包含有访问权限,这些访问权限决定了谁能访问和如何访问这些文件和目录. 通过设定权限可以从以下三种访问方式限制访问权限: ...

  6. 每天一个linux命令(2):文件权限详解

    Linux文件权限详解 文件和目录权限概述 在linux中的每一个文件或目录都包含有访问权限,这些访问权限决定了谁能访问和如何访问这些文件和目录. 通过设定权限可以从以下三种访问方式限制访问权限:只允 ...

  7. ASP中利用OWC控件实现图表功能详解[zz]

    ASP中利用OWC控件实现图表功能详解 在ASP中利用OWC(Office Web Components)控件可轻松实现各种图表功能,如饼图,簇状柱型图,折线图等. 在下面的代码中我详细的给出了饼图, ...

  8. PHP之PHP文件引用详解

    HP的文件引用涉及到四个函数: 文件引用 1.include() 2.include_once() 3.require() 4.require_once() 这四个函数常常会给PHP初学者造成困扰,总 ...

  9. Linux 文件权限详解 含义和修改和安全

    Linux文件权限详解 文件和目录权限概述 在linux中的每一个文件或目录都包含有访问权限,这些访问权限决定了谁能访问和如何访问这些文件和目录. 第一次接触Linux 的时候,对于文件权限方面的知识 ...

最新文章

  1. Unix数据中心主宠儿
  2. awd的批量脚本 pwn_AWD攻防之web入门篇
  3. typedef的用法
  4. 积分和人民币比率_通过比率路由到旧版和现代应用程序–通过Spring Cloud的Netflix Zuul...
  5. DecimalFormat的几种用法!关于字符串的使用
  6. Linux系统配置成简单的路由器
  7. 下一代 Windows 将至,是全新的 Windows 11 还是 Windows 10 的延续?
  8. Windows下用vs2017编译和配置libcurl库(手把手教,适合新人)
  9. SQL注入漏洞入门(操作实现)
  10. git 安装_Windows系统Git安装教程(详解Git安装过程)
  11. Android Studio查看MD5
  12. u盘启动盘制作工具多个linux,多系统U盘启动盘制作工具
  13. 踩了个DNS解析的坑,但我还是没想通
  14. java 中文分割_在java中只分割中文字符
  15. 假程序员启示录:房价
  16. 哈尔滨五星酒店马桶刷洗杯子事件舆情监测报告
  17. UE5笔记【六】流明引擎Lumen简介;Lumen处理发光物体。
  18. 学习手册--没必要装模做样,只有你自己最清楚
  19. 【代码审计】--- php代码审计方法
  20. depot_tools更新失败

热门文章

  1. 你可能也看过这个生物学家转行后创造的价值 120 亿美元的动画
  2. 和在c语言,?和:在C语言中的详解
  3. php mysql 值是否丰在_php 查询数据库表 判断 某值是否存在
  4. python gevent asyncio_python用from gevent import monkey; monkey.patch_all()之后报ssl等错误
  5. python读取txt文件每一行存为列表,从txt文件中读取一定数量的行,并以python方式转换为list...
  6. Tampermonkey笔记-脚本的搭建和基本使用
  7. Java笔记-Spring Boot Webservice中对指定namespace及localpart进行拦截
  8. MySQL笔记-解决...mysql.sock (13)(两种情况会产生此问题)
  9. Qt笔记-windows下拷贝文件夹中所有文件
  10. C++笔记-设置cout输出数据的宽度和填充