博客
关于我
Linux 文件 IO
阅读量:581 次
发布时间:2019-03-11

本文共 31503 字,大约阅读时间需要 105 分钟。

参考:

C 和 C++ 文件操作详解:

标准IO与文件IO 的区别:

参考 APUE 整理。如有疑问,可以直接看 APUE 。。。   linux 文件IO

IO文件操作时最常用的也最基本的内容。linux文件系统是由两层结构构建:第一层是虚拟文件系统(VFS),第二层是各种不同的具体文件系统。VFS是吧、把各种具体的文件系统的公共部分抽取出来,形成一个抽象层,是系统内核的一部分。它位于用户程序和具体的文件系统中间。它对用户程序提供了标准的文件系统的调用接口,对具体的文件系统,它通过一系列的对不同文件系统公用的函数指针来实际调用具体的文件系统函数,完成实际的各有的操作。任何使用文件系统的程序必须经过这层接口来使用它。通过这种方式,VFS就对用于屏蔽了底层文件系统的实现细节和差异。

通过 cat /proc/filesystems命令可以查看系统支持哪些文件系统。

先来一张总结图

1. 引言

        先说明可用的文件 I/O 函数——打开文件、读文件、写文件等等。大多数UNIX文件 I/O只需用到5个函数: open、 read、 write、 lseek 以及 close。然后说明不同缓存器长度对 read 和 write 函数的影响 。

        本文所说明的函数经常被称之为不带缓存的 I/O(unbuffered I/O)。标准 I/O函数,即库函数的 I/O,是带缓存的I/O。不带缓冲的I/O,直接调用系统调用,速度快,如函数open(), read(), write()等。而带缓冲的I/O,在系统调用前采用一定的策略,速度慢,比不带缓冲的I/O安全,如fopen(), fread()、 fwrite()等。

        术语——不带缓存指的是每个 read 和 write都调用内核中的一个系统调用。这些不带缓存的 I/O 函数不是 ANSI C的组成部分,但是是 POSIX .1和XPG3的组成部分。

        只要涉及在多个进程间共享资源,原子操作的概念就变成非常重要。我们将通过文件 I/O 和传送给 open 函数的参数来讨论此概念。并进一步讨论在多个进程间如何共享文件,并涉及内核的有关数据结构。在讨论了这些特征后,将说明 dup、 fcntl 和 ioctl 函数。

        其实,上面所说的可以分为 文件I/O(即系统调用) 和 标准 I/O(库函数)。

  • 文件I/O:文件I/O称之为不带缓存的IO(unbuffered I/O)。不带缓存指的是每个read,write都调用内核中的一个系统调用。也就是一般所说的低级I/O——操作系统提供的基本IO服务,与os绑定,特定于linix或unix平台。
  • 标准I/O:标准I/O是ANSI C建立的一个标准I/O模型,是一个标准函数包和stdio.h头文件中的定义,具有一定的可移植性。标准I/O库处理很多细节。例如缓存分配,以优化长度执行I/O等。标准的I/O提供了三种类型的缓存。
    (1)全缓存:当填满标准I/O缓存后才进行实际的I/O操作。
    (2)行缓存:当输入或输出中遇到新行符时,标准I/O库执行I/O操作。
    (3)不带缓存:stderr就是了。

二者的区别

        文件I/O 又称为低级磁盘I/O,遵循POSIX相关标准。任何兼容POSIX标准的操作系统上都支持文件I/O。标准I/O被称为高级磁盘I/O,遵循ANSI C相关标准。只要开发环境中有标准I/O库,标准I/O就可以使用。(Linux 中使用的是GLIBC,它是标准C库的超集。不仅包含ANSI C中定义的函数,还包括POSIX标准中定义的函数。因此,Linux 下既可以使用标准I/O,也可以使用文件I/O)。

        通过文件I/O读写文件时,每次操作都会执行相关系统调用。这样处理的好处是直接读写实际文件,坏处是频繁的系统调用会增加系统开销,标准I/O可以看成是在文件I/O的基础上封装了缓冲机制。先读写缓冲区,必要时再访问实际文件,从而减少了系统调用的次数。
        文件I/O中用文件描述符表现一个打开的文件,可以访问不同类型的文件如普通文件、设备文件和管道文件等。而标准I/O中用FILE(流)表示一个打开的文件,通常只用来访问普通文件。

使用的一些函数

2. 文件描述符

        对于内核而言,所有打开的文件(Linux 中一切皆文件)都由一个文件描述符标识。文件描述符是一个非负整数。当打开一个现存文件或创建一个新文件时,内核向进程返回一个文件描述符。当读、写一个文件时,用 open 或 creat 返回的文件描述符标识该文件,将其作为参数传送给read 或 write

        按照惯例, UNIX shell使文件描述符0与进程的标准输入相结合,文件描述符1与标准输出相结合,文件描述符2与标准出错输出相结合。这是UNIX shell以及很多应用程序使用的惯例,而与内核无关。尽管如此,如果不遵照这种惯例,那么很多UNIX应用程序就不能工作。

        在 
POSIX.1
应用程序中,
0
1、
被代换成 符号常数 
STDIN_FILENO
STDOUT_FILENO 
和 
STDERR_FILENO
。这些常数都定义在头文件
< unistd.h >
中。

        文件描述符的范围是 0 ~ OPEN_MAX(见表2-7)。早期的 UNIX版本采用的上限值是19 (允许每个进程打开 20 个文件),现在很多系统则将其增加至63

3. 文件 I/O 相关系统调用

APUE 中 关于文件 I/O 的几个基础系统调用:open()、creat()、close()、lseek()、read()、write()。

上面的函数都可以直接通过 man 帮助查看。man 查看的内容包括:

man可以查看一下内容:1.一般命令(shell命令)2.系统调用(open write等直接陷入内核的函数)3.子函数(C函数库等不直接陷入内核的函数)4.特殊文件(/dev/zero等linux系统中有特殊用途的文件)5.文件格式(linux系统的配置文件格式 host.conf)6.游戏7.宏和地方传统定义(本地配置)8.维护命令(tcpdump等用来观察linux系统运行情况的命令)

open函数

open函数:调用它可以打开或者创建一个文件。(man 2 open

注意:open 创建文件不在同一目录时,如果目录不存在则创建失败。所以创建的文件不在同一目录时,必须先创建目录,然后在创建文件。

NAME       open, openat, creat - open and possibly create a fileSYNOPSIS       #include 
#include
#include
int open(const char *pathname, int flags); int open(const char *pathname, int flags, mode_t mode); int creat(const char *pathname, mode_t mode); int openat(int dirfd, const char *pathname, int flags); int openat(int dirfd, const char *pathname, int flags, mode_t mode);返回值:成功返回新分配的文件描述符,出错返回-1并设置errno参数解析: pathname是要打开或者创建的文件名。flags 参数有一系列常数值可供选择,可以同时选择多个常数用按位或运算符连接起来,所以这些常数的宏定义都以O_开头,表示or。flags 文件打开时候的选项,有三个选项是必选的!必选项:以下三个常数中必须指定一个,且仅允许指定一个。 O_RDONLY以只读方式打开文件。 O_WRONLY以只写方式打开文件。 O_RDWR以读、写方式打开文件。flags 可选选项:可选项可以同时指定0个或多个,和必选项按位或起来作为flags参数。可选项有很多,这里只介绍一部分,其它选项可参考open(2)的Man Page:O_APPEND 以追加方式打开文件,每次写时都写在文件末尾。O_CREAT 如果文件不存在,则创建一个,存在则打开它。O_EXCL 与O_CREAT一起使用时,如果文件已经存在则返回出错。O_TRUNC 以只写或读写方式打开时,把文件截断为0O_DSYNC 每次write时,等待数据写到磁盘上。O_RSYNC 每次读时,等待相同部分先写到磁盘上。O_NONBLOCK 对于设备文件,以O_NONBLOCK方式打开可以做非阻塞I/O(Nonblock I/O)O_SYNC 每次write时,等到数据写到磁盘上并接更新文件属性。SYNC选项都会影响降低性能,有时候也取决于文件系统的实现。mode 只有创建文件时才使用此参数,指定文件的访问权限。模式有: S_IRWX[UGO] 可读 可写 可执行 S_IR[USR GRP OTH] 可读 S_IW[USR GRP OTH] 可写 S_IX[USR GRP OTH] 可执行 S_ISUID 设置用户ID S_ISGID 设置组IDU->user G->group O->others

由 open 返回的文件描述符一定是最小的未用描述符数字。

在早期的 UNIX 版本中, open 的第二个参数只能是 0、1 或 2。没有办法打开一个尚未存在的文件,因此需要另一个系统调用 creat 以创建新文件。现在 open 函数提供了选择项 O_CREAT 和 O_TRUNC,于是也就不再需要 creat 函数了。

示例代码1:

#include 
#include
#include
#include
int main(void){ int fd; char buf[64]; int ret = 0; fd = open("./file.txt", O_RDONLY); if (fd == -1) { printf("open file error\n"); exit(1); } printf("---open ok---\n"); while((ret = read(fd, buf, sizeof(buf)))) { write(1, buf, ret); } close(fd); return 0;}

示例代码2(命令行参数实现简单的cp命令):

/* *./mycp src dst 命令行参数实现简单的cp命令 */#include 
#include
#include
#include
char buf[1024];int main(int argc, char *argv[]){ int src, dst; int n; src = open(argv[1], O_RDONLY); //只读打开源文件 if(src < 0) { perror("open src error"); exit(1); } //只写方式打开,覆盖原文件内容,不存在则创建,rw-r--r-- dst = open(argv[2], O_WRONLY|O_TRUNC|O_CREAT, 0644); if(src < 0) { perror("open dst error"); exit(1); } while((n = read(src, buf, 1024))) { if(n < 0) { perror("read src error"); exit(1); } write(dst, buf, n); //不应写出1024, 读多少写多少 } close(src); close(dst); return 0;}

示例代码3:

#include 
#include
#include
#include
#define N 1204int main(int argc, char *argv[]){ int fd, fd_out; int n; char buf[N]; fd = open("src.txt", O_RDONLY); if(fd < 0) { perror("open src.txt error"); exit(1); } fd_out = open("des.txt", O_WRONLY|O_CREAT|O_TRUNC, 0644); if(fd < 0) { perror("open des.txt error"); exit(1); } while((n = read(fd, buf, N))) { if(n < 0) { perror("read error"); exit(1); } write(fd_out, buf, n); } close(fd); close(fd_out); return 0;}

示例代码:使用库函数 fopen

#include 
#include
int main(void){ FILE *fp, *fp_out; int n; fp = fopen("src.txt", "r"); if(fp == NULL) { perror("fopen error"); exit(1); } fp_out = fopen("des.cp", "w"); if(fp == NULL) { perror("fopen error"); exit(1); } while((n = fgetc(fp)) != EOF) { fputc(n, fp_out); } fclose(fp); fclose(fp_out); return 0;}

示例代码:打开错误

#include 
//read write#include
//open close O_WRONLY O_RDONLY O_CREAT O_RDWR#include
//exit#include
#include
//perror#include
int main(void){ int fd;#if 1 //打开文件不存在 fd = open("test", O_RDONLY | O_CREAT); if(fd < 0) { printf("errno = %d\n", errno); //perror("open test error"); printf("open test error: %s\n" , strerror(errno)); //printf("open test error\n"); exit(1); }#elif 0 //打开的文件没有对应权限(以只写方式打开一个只有读权限的文件) fd = open("test", O_WRONLY); //O_RDWR也是错误的 if(fd < 0) { printf("errno = %d\n", errno); perror("open test error"); //printf("open test error\n"); exit(1); }#endif#if 0 //以写方式打开一个目录 fd = open("testdir", O_RDWR); //O_WRONLY也是错的 if(fd < 0) { perror("open testdir error"); exit(1); }#endif return 0;}

注意open函数与C标准I/O库的fopen函数有些细微的区别:

  • 以可写的方式fopen一个文件时,如果文件不存在会自动创建,而open一个文件时必须明确指定O_CREAT才会创建文件,否则文件不存在就出错返回。
  • 以w或w+方式fopen一个文件时,如果文件已存在就截断为0字节,而open一个文件时必须明确指定O_TRUNC才会截断文件,否则直接在原来的数据上改写。
  • 第三个参数mode指定文件权限,可以用八进制数表示,比如0644表示-rw-r-r–,也可以用S_IRUSR、S_IWUSR等宏定义按位或起来表示,详见open(2)的Man Page。要注意的是,文件权限由open的mode参数和当前进程的umask掩码共同决定。

补充说明一下Shell的umask命令。Shell进程的umask掩码可以用umask命令查看:(反掩码 的数字是要去掉的权限

$ umask0002

用touch命令创建一个文件时,创建权限是0666,而touch进程继承了Shell进程的umask掩码,所以最终的文件权限是0666&∼022=0644。

同样道理,用gcc编译生成一个可执行文件时,创建权限是0777,而最终的文件权限是:0777 & ∼022 = 0755。

我们看到的都是被umask掩码修改之后的权限,那么如何证明touch或gcc创建文件的权限本来应该是0666和0777呢?我们可以把Shell进程的umask改成0,再重复上述实验。

最大打开文件个数

查看当前系统允许打开最大文件个数:cat /proc/sys/fs/file-max

修改默认设置最大打开文件个数为4096:ulimit -n 4096

creat函数

creat 以只写方式创建一个文件,若文件已经存在,则把它截断为0

#include 
int creat(const char *pathname, mode_t mode) // 返回:若成功为只写打开的文件描述符,若出错为- 1参数解析:pathname 要创建的文件名称mode 跟open的第三个参数相同,可读,可写,可执行 。如果失败 ,返回值为-1creat 函数 等同于 open (pathname, O_WRONLY | O_CREAT | O_TRUNC, mode)在早期的 UNIX 版本中, open 的第二个参数只能是 0、1 或 2。没有办法打开一个尚未存在的文件,因此需要另一个系统调用 creat 以创建新文件。现在 open 函数提供了选择项 O_CREAT 和 O_TRUNC,于是也就不再需要 creat 函数了。

creat 的一个不足之处是它以只写方式打开所创建的文件。在提供 open的新版本之前,如果要创建一个临时文件,并要先写该文件,然后又读该文件,则必须先调用 creat, clo se,然后再调用 open。现在则可用下列方式调用open:open(pathname, O_RDWR|O_CREAT|O_TRUNC, m o d e) ;

close函数

close 关闭已经打开的文件,并释放文件描述符

#include 
int close(int fd) // 返回值:成功返回0,出错返回-1并设置errno参数解析:fd 文件描述符,由 open 或者 creat 返回的非负整数。当一个进程结束时,操作系统会自动释放该进程打开的所有文件。但还是推荐用close来关闭文件。lsof命令可以查看进程打开了那些文件。

        参数fd是要关闭的文件描述符。需要说明的是,当一个进程终止时,内核对该进程所有尚未关闭的文件描述符调用close关闭,所以即使用户程序不调用close,在终止时内核也会自动关闭它打开的所有文件。但是对于一个长年累月运行的程序(比如网络服务器),打开的文件描述符一定要记得关闭,否则随着打开的文件越来越多,会占用大量文件描述符和系统资源。

        由open返回的文件描述符一定是该进程尚未使用的最小描述符。由于程序启动时自动打开文件描述符0、1、2,因此第一次调用open打开文件通常会返回描述符3,再调用open就会返回4。可以利用这一点在标准输入、标准输出或标准错误输出上打开一个新文件,实现重定向的功能。例如,首先调用close关闭文件描述符1,然后调用open打开一个常规文件,则一定会返回文件描述符1,这时候标准输出就不再是终端,而是一个常规文件了,再调用printf就不会打印到屏幕上,而是写到这个文件中了。后面要讲的dup2函数提供了另外一种办法在指定的文件描述符上打开文件。

/*本程序演示了,在一个函数中打开一个文件,然后在另一个函数中关闭文件。可以看到fd相关数据结构是由内核维护的,所以如果你打开了一个文件而没有关闭,那些数据结构就会占用内存。需要说明的是,当一个进程终止时,内核对该进程所有尚未关闭的文件描述符调用close关闭,所以即使用户程序不调用close,在终止时内核也会自动关闭它打开的所有文件。*/#include 
#include
#include
#include
int openFile(void){ int fd=-1; fd = open("./test.txt", O_RDONLY); if(fd>0) printf("open file fd : %d\n", fd); else printf("open file fail\n"); return fd; }int closeFile(int fd){ int retVal; retVal=close(fd); if(retVal<0) printf("close file fail\n"); else printf("close file sueccess\n"); return retVal;}void main(void){ int fd=openFile(); closeFile(fd);}
程序运行截图:

lseek函数

Linux文件空洞与稀疏文件:

lseek 用来定位当前文件偏移量,既你对文件操作从文件的那一部分开始。

每个打开文件都有一个与其相关联的“当前文件位移量”。它是一个非负整数,用以度量从文件开始处计算的字节数。通常,读、写操作都从当前文件位移量处开始,并使位移量增加所读或写的字节数。按系统默认,当打开一个文件时,除非指定 O_APPEND 选择项,否则该位移量被设置为0。可以调用 lseek 显式地定位一个打开文件。

#include 
off_t lseek(int fd, off_t offset, int whence); // 如果失败,返回值为-1,成功返回移动后的文件偏移量。参数解析:fd 文件描述符。offset 必须与whence一同解析 whence为 SEEK_SET, 则offset从文件的开头算起。 whence为 SEEK_CUR, 则offset从当前位置算起,既新偏移量为当前偏移量加上offset whence为 SEEK_END, 则offset从文件末尾算起。可以通过lseek、write来快速创建一个大文件。

每个打开的文件都记录着当前读写位置,打开文件时读写位置是0,表示文件开头,通常读写多少个字节就会将读写位置往后移多少个字节。但是有一个例外,如果以O_APPEND方式打开,每次写操作都会在文件末尾追加数据,然后将读写位置移到新的文件末尾。lseek和标准I/O库的fseek函数类似,可以移动当前读写位置(或者叫偏移量)。

参数offset和whence的含义和fseek函数完全相同。只不过第一个参数换成了文件描述符。和fseek一样,偏移量允许超过文件末尾,这种情况下对该文件的下一次写操作将延长文件,中间空洞的部分读出来都是0。若lseek成功执行,则返回新的偏移量,因此可用以下方法确定一个打开文件的当前偏移量:

off_t currpos;

currpos = lseek(fd, 0, SEEK_CUR);

这种方法也可用来确定文件或设备是否可以设置偏移量,常规文件都可以设置偏移量,而设备一般是不可以设置偏移量的。如果设备不支持lseek,则lseek返回-1,并将errno设置为ESPIPE。注意fseek和lseek在返回值上有细微的差别,fseek成功时返回0失败时返回-1,要返回当前偏移量需调用ftell,而lseek成功时返回当前偏移量失败时返回-1。

lseek 仅将当前的文件位移量记录在内核内,它并不引起任何 I/O操作。然后,该位移量用于下一个读或写操作。文件位移量可以大于文件的当前长度,在这种情况下,对该文件的下一次写将延长该文件,并在文件中构成一个空调,这一点是允许的。位于文件中但没有写过的字节都被读为 0。

示例代码:

新建一个文件 lseek.txt ,文件内容:It's a test for lseek。然后新建一个 lseek.c 文件,内容如下:

#include 
#include
#include
#include
#include
int main(void){ int fd, n; char msg[] = "It's a test for lseek\n"; char ch; fd = open("lseek.txt", O_RDWR|O_CREAT|O_TRUNC, 0644); if(fd < 0) { perror("open lseek.txt error"); exit(1); } write(fd, msg, strlen(msg)); lseek(fd, 0, SEEK_SET); while((n = read(fd, &ch, 1))) { if(n < 0) { perror("read error"); exit(1); } write(STDOUT_FILENO, &ch, n); //putchar(ch); //printf("%c", ch); } close(fd); return 0;}

使用 lseek 得出文件大小

#include 
#include
#include
#include
int main(void){ int fd; fd = open("lseek.txt", O_RDWR | O_CREAT, 0644); if (fd < 0) { perror("open error"); exit(1); }#if 0 int ret = lseek(fd, 99, SEEK_SET); if (ret < 0) { perror("lseek error"); exit(1); } write(fd, "a", 1);#endif int ret = lseek(fd, 0, SEEK_END); if (ret < 0) { perror("lseek error"); exit(1); } printf("the lenth of lseek.txt is %d\n", ret); close(fd); return 0;}

示例代码:

#include 
#include
#include
#include
#include
int main(void){ int fd; fd = open("lseek.txt", O_RDONLY); if(fd < 0){ perror("open lseek.txt error"); exit(1); } int len = lseek(fd, 0, SEEK_END); if(len == -1){ perror("lseek error"); exit(1); } printf("len of msg = %d\n", len); off_t cur = lseek(fd, -10, SEEK_END); if(cur == -1){ perror("lseek error"); exit(1); } printf("--------| %ld\n", cur); close(fd); return 0;}

read函数

read 从打开的设备或文件偏移量处读入指定大小的数据。

#include 
ssize_t read(int fd, void *buf, size_t count);返回值:成功返回读取的字节数,出错返回-1并设置errno,如果在调read之前已到达文件末尾,则这次read返回0参数解析 fd 文件描述符 ,有open返回。buf 读入文件内容存放的内存首地址。count 要读取的字节数。实际读入的字节数可能会小于要求读入的字节数。比如文件只有所剩的字节数小于你要读入的字节数,读取fifo文件和网络套接字时都可能出现这种情况

        参数count是请求读取的字节数,读上来的数据保存在缓冲区buf中,同时文件的当前读写位置向后移。注意这个读写位置和使用C标准I/O库时的读写位置有可能不同,这个读写位置是记在内核中的,而使用C标准I/O库时的读写位置是用户空间I/O缓冲区中的位置。比如用fgetc读一个字节,fgetc有可能从内核中预读1024个字节到I/O缓冲区中,再返回第一个字节,这时该文件在内核中记录的读写位置是1024,而在FILE结构体中记录的读写位置是1。注意返回值类型是ssize_t,表示有符号的size_t,这样既可以返回正的字节数、0(表示到达文件末尾)也可以返回负值-1(表示出错)。read函数返回时,返回值说明了buf中前多少个字节是刚读上来的。有些情况下,实际读到的字节数(返回值)会小于请求读的字节数count,

        例如:读常规文件时,在读到count个字节之前已到达文件末尾。例如,距文件末尾还有30个字节而请求读100个字节,则read返回30,下次read将返回0。从终端设备读,通常以行为单位,读到换行符就返回了。从网络读,根据不同的传输层协议和内核缓存机制,返回值可能小于请求的字节数,后面socket编程部分会详细讲解。

write函数

write向打开的设备或文件中写入一定字节的数据。

#include 
ssize_t write(int fd, const void *buf, size_t count); // 返回值:成功返回写入的字节数,出错返回-1并设置errno失败返回-1,成功返回实际写入的字节数。当磁盘满或者文件到达上限时可能写入失败。一般从当前文件偏移量出写入,但如果打开时使用了O_APPEND,那么无论当前文件偏移量在哪里,都会移动到文件末尾写入。

写常规文件时,write的返回值通常等于请求写的字节数count,而向终端设备或网络写则不一定

阻塞和非阻塞

        读常规文件是不会阻塞的,不管读多少字节,read一定会在有限的时间内返回。从终端设备或网络读则不一定,如果从终端输入的数据没有换行符,调用read读终端设备就会阻塞,如果网络上没有接收到数据包,调用read从网络读就会阻塞,至于会阻塞多长时间也是不确定的,如果一直没有数据到达就一直阻塞在那里。同样,写常规文件是不会阻塞的,而向终端设备或网络写则不一定。

        现在明确一下阻塞(Block)这个概念。当进程调用一个阻塞的系统函数时,该进程被置于睡眠(Sleep)状态,这时内核调度其它进程运行,直到该进程等待的事件发生了(比如网络上接收到数据包,或者调用sleep指定的睡眠时间到了)它才有可能继续运行。与睡眠状态相对的是运行(Running)状态,在Linux内核中,处于运行状态的进程分为两种情况:
        正在被调度执行。CPU处于该进程的上下文环境中,程序计数器(eip)里保存着该进程的指令地址,通用寄存器里保存着该进程运算过程的中间结果,正在执行该进程的指令,正在读写该进程的地址空间。
        就绪状态。该进程不需要等待什么事件发生,随时都可以执行,但CPU暂时还在执行另一个进程,所以该进程在一个就绪队列中等待被内核调度。系统中可能同时有多个就绪的进程,那么该调度谁执行呢?内核的调度算法是基于优先级和时间片的,而且会根据每个进程的运行情况动态调整它的优先级和时间片,让每个进程都能比较公平地得到机会执行,同时要兼顾用户体验,不能让和用户交互的进程响应太慢。

下面这个小程序从终端读数据再写回终端。

阻塞读终端

#include 
#include
int main(void){ char buf[10]; int n; n = read(STDIN_FILENO, buf, 10); if (n < 0) { perror("read STDIN_FILENO\n"); exit(1); } write(STDOUT_FILENO, buf, n); return 0;}

第一次执行a.out的结果很正常,而第二次执行的过程有点特殊。

现在分析一下:Shell进程创建a.out进程,a.out进程开始执行,而Shell进程睡眠等待a.out进程退出。a.out调用read时睡眠等待,直到终端设备输入了换行符才从read返回,read只读走10个字符,剩下的字符仍然保存在内核的终端设备输入缓冲区中。a.out进程打印并退出,这时Shell进程恢复运行,Shell继续从终端读取用户输入的命令,于是读走了终端设备输入缓冲区中剩下的字符d和换行符,把它当成一条命令解释执行,结果发现执行不了,没有 abcd 这个命令。如果在open一个设备时指定了O_NONBLOCK标志,read/write就不会阻塞。以read为例,如果设备暂时没有数据可读就返回-1,同时置errno为EWOULDBLOCK(或者EAGAIN,这两个宏定义的值相同),表示本来应该阻塞在这里(would block,虚拟语气),事实上并没有阻塞而是直接返回错误,调用者应该试着再读一次(again)。这种行为方式称为轮询(Poll),调用者只是查询一下,而不是阻塞在这里死等,这样可以同时监视多个设备:

while(1) {    非阻塞read(设备1);    if(设备1有数据到达)        处理数据;    非阻塞read(设备2);    if(设备2有数据到达)        处理数据;...}

如果read(设备1)是阻塞的,那么只要设备1没有数据到达就会一直阻塞在设备1的read调用上,即使设备2有数据到达也不能处理,使用非阻塞I/O就可以避免设备2得不到及时处

理。非阻塞I/O有一个缺点,如果所有设备都一直没有数据到达,调用者需要反复查询做无用功,如果阻塞在那里,操作系统可以调度别的进程执行,就不会做无用功了。在使用非阻塞I/O时,通常不会在一个while循环中一直不停地查询(这称为Tight Loop),而是每延迟等待一会儿来查询一下,以免做太多无用功,在延迟等待的时候可以调度其它进程执行。

while(1) {    非阻塞read(设备1);    if(设备1有数据到达)        处理数据;    非阻塞read(设备2);    if(设备2有数据到达)        处理数据;    ...    sleep(n);}

这样做的问题是,设备1有数据到达时可能不能及时处理,最长需延迟n秒才能处理,而且反复查询还是做了很多无用功。以后要学习的select(2)函数可以阻塞地同时监视多个设备,还可以设定阻塞等待的超时时间,从而圆满地解决了这个问题。

以下是一个非阻塞I/O的例子。目前我们学过的可能引起阻塞的设备只有终端,所以我们用终端来做这个实验。程序开始执行时在0、1、2文件描述符上自动打开的文件就是终端,但是没有O_NONBLOCK标志。所以就像例 28.2 “阻塞读终端”一样,读标准输入是阻塞的。我们可以重新打开一遍设备文件/dev/tty(表示当前终端),在打开时指定O_NONBLOCK标志。

非阻塞读终端示例代码:

#include 
#include
#include
#include
#include
#include
#define MSG_TRY "try again\n"int main(void){ char buf[10]; int fd, n; fd = open("/dev/tty", O_RDONLY|O_NONBLOCK); //使用O_NONBLOCK标志设置非阻塞读终端 if(fd < 0) { perror("open /dev/tty"); exit(1); }tryagain: n = read(fd, buf, 10); if(n < 0) { //由于open时指定了O_NONBLOCK标志,read读设备,没有数据到达返回-1,同时将errno设置为EAGAIN或EWOULDBLOCK if(errno != EAGAIN) { //也可以是 if(error != EWOULDBLOCK)两个宏值相同 perror("read /dev/tty"); exit(1); } sleep(3); write(STDOUT_FILENO, MSG_TRY, strlen(MSG_TRY)); goto tryagain; } write(STDOUT_FILENO, buf, n); close(fd); return 0;}

非阻塞I/O实现等待超时的例子。既保证了超时退出的逻辑又保证了有数据到达时处理延迟较小。

#include 
#include
#include
#include
#include
#include
#define MSG_TRY "try again\n"#define MSG_TIMEOUT "time out\n"int main(void){ char buf[10]; int fd, n, i; fd = open("/dev/tty", O_RDONLY|O_NONBLOCK); if(fd < 0) { perror("open /dev/tty"); exit(1); } printf("open /dev/tty ok... %d\n", fd); for(i = 0; i < 5; i++) { n = read(fd, buf, 10); if(n > 0) { //说明读到了东西 break; } if(errno != EAGAIN) { //EWOULDBLK perror("read /dev/tty"); exit(1); } sleep(3); write(STDOUT_FILENO, MSG_TRY, strlen(MSG_TRY)); } if(i == 5) { write(STDOUT_FILENO, MSG_TRY, strlen(MSG_TIMEOUT)); } else { write(STDOUT_FILENO, buf, n); } close(fd); return 0;}

内核用于所有 I/O 的数据结构

内核使用了三种数据结构,来实现I/O 

  1. 每个进程在进程表中都有一个记录项,每个记录项中有一张打开文件描述符表,可将其视为一个矢量,每个描述符占用一项。与每个 文 件描述符相关联的是:
    (a) 文件描述符标志。
    (b) 指向一个文件表项的指针。
  2. 内核为所有打开文件维持一张文件表。每个文件表项包含:
    (a) 文件状态标志(读、写、增写、同步等)。
    (b) 当前文件位移量。
    (c) 指向该文件v节点表项的指针。
  3. 每个打开文件(或设备)都有一个v节点结构。v节点包含了文件类型和对此文件进行各种操作的函数的指针信息。对于大多数文件, v节点还包含了该文件的i节点(索引节点)。例如, i节点包含了文件的所有者、文件长度、文件所在的设备、指向文件在盘上所使用的实际数据块的指针等等

如图显示了进程的三张表之间的关系。该进程有两个不同的打开文件:一个文件打开为标准输入(文件描述符0),另一个打开为标准输出(文件描述符为 1)。

打开文件的内核数据结构

两个独立进程各自打开了同一文件,它们拥有各自的文件表项,但共享v节点表。

如图所示,假定第一个进程使该文件在文件描述符 3上打开,而另一个进程则使此文件在文件描述符 4上打开。打开此文件的每个进程都得到一个文件表项,但对一个给定的文件只有一个 v节点表项。每个进程都有自己的文件表项的一个理由是:这种安排使每个进程都有它自己的对该文件的当前位移量

两个独立进程各自打开同一个文件

原子操作

假定 A、B 两个进程以 O_APPEND 方式打开同一个文件。A 进程去写该文件,假设此时文件偏移量为1000,B进程同时去写该文件,此时由于A进程未写完,则B进程得到的文件偏移量仍为1000。最后B进程的内容可能会覆盖掉A进程写的内容。pread , pwrite是原子读写操作。相当于先把文件偏移量定位到offset,然后在进行读写。这都是一步完成,不存在竞争问题。

#include 
ssize_t pread(int filedes, void *buf, size_t nbytes, off_t offset) ssize_t pwrite(int filedes, const void *buf, size_t nbytes, off_t offset) 返回值跟read和write一样。offset为文件偏移量。

调用 pread 相当于 顺序调用 lseek 和 read ,但是 pread 又与这种顺序调用有下列重要区别:

  • 调用 pread 时,无法中断其定位和读操作
  • 不能更新文件指针

调用 pwrite 相当于 顺序调用 lseek 和 write ,但但也与他们有上述类似的区别。

dup 和 dup2 函数

dup/dup2用来复制一个已经存在的文件描述符

#include 
int dup(int filedes) ; int dup2(int filedes, int filedes2) ; 失败返回-1,成功返回新文件描述符。filedes2是新文件描述符,如果已经打开则先关闭它。ssize_t pread(int filedes, void *buf, size_t nbytes, off_t offset);共享文件表项。

dup 和 dup2 的使用

  • dup 返回的新文件描述符一定是当前可用文件描述符中的最小数值。
  • dup2 则可以用 filedes2 参数指定新描述符的数值。如果 filedes2 已经打开,则先将其关闭。如若filedes 等于 filedes2,则 dup2 返回 filedes2,而不关闭它。

这些函数返回的新文件描述符与参数 filedes 共享同一个文件表项。如图所示:

        在此图中,我们假定进程执行了:newfd = dup(1) 。当此函数开始执行时,假定下一个可用的描述符是 3 (这是非常有可能的,因为 0, 1和2由 shell 打开)。因为两个描述符指向同一文件表项,所以它们共享同一文件状态标志 (读、写、添写等)以及同一当前文件位移量。

        每个文件描述符都有它自己的一套文件描述符标志。正如我们将在下一节中说明的那样,新描述符的执行时关闭( close-on-exec )文件描述符标志总是由 dup 函数清除。
        复制一个描述符的另一种方法是使用 fcntl 函数,下一节将对该函数进行说明。

实际上,调用:dup( filedes ) ; 等效于:fcntl (filedes , F_DUPFD, 0);而调用:dup2(filedes, filedes2);等效于:        close ( filedes2 ) ;        fcntl(filedes, F_DUPFD, filedes2);

在上面的第二种情况下,dup2并不完全等同于close加上fcntl。它们之间的区别是:

  1.  dup2是一个原子操作,而close及fcntl则包括两个函数调用。有可能在close和fcntl之间插入执行信号捕获函数,它可能修改文件描述符。(第10章将说明信号。)
  2.  在dup2和fcntl之间有某些不同的errno。

示例代码:

#include 
#include
#include
#include
#include
int main(void){ int fd, save_fd; char msg[] = "It's a test!\n"; fd = open("file1", O_RDWR|O_CREAT, 0644); if(fd < 0) { perror("open error"); exit(1); } printf("------>fd = %d\n", fd); //新打开的文件描述符是3,里面保存指向feil1文件的指针 save_fd = dup(STDOUT_FILENO); //把文件描述符1所保存的stdout指针复制给文件描述符save_fd printf("save_fd = %d\n", save_fd); //save_fd是文件描述符4,里面保存指向stdout的文件指针 write(save_fd, msg, strlen(msg)); //向save_fd写,既是向stdout写,会写到屏幕 //将fd(3)保存的指向file1的文件指针复制给STDOUT_FILENO(1),并覆盖1原来保存的文件指针 int ret = dup2(fd, STDOUT_FILENO); //结果是fd指向file1文件,STDOUT_FILENO(1)也指向file1文件 printf(" -------> m = %d\n", ret); //printf默认对应文件描述符1,但是现在1指向file1文件 close(fd); //fd(3)被关闭 puts(msg); return 0;}

dup2 示例代码:

#include 
#include
#include
#include
#include
int main(void){ int fd, save_fd; char msg[] = "It's just a test for dup2!\n"; fd = open("test", O_RDWR|O_CREAT|O_TRUNC, 0644); //
if(fd < 0) { perror("open error"); exit(1); } save_fd = dup(STDOUT_FILENO); //STDOUT_FILENO
printf("save_fd = %d\n", save_fd);#if 0 dup2(STDOUT_FILENO, fd); write(fd, msg, strlen(msg));#else dup2(fd, STDOUT_FILENO); puts(msg); write(fd, msg, strlen(msg));//???#endif close(fd); return 0;}

示例代码:

/* *如果使用dup2给一个文件制定了两个描述符的时候 *一个文件描述符关闭,依然能使用dup2的新文件描述符对该文件读写 */#include 
#include
#include
#include
#include
int main(void){ int fd, fd2; char *str = "use fd write in\n"; char *str2 = "use ====fd2==== write\n"; fd = open("test", O_WRONLY|O_TRUNC|O_CREAT, 0644); if(fd < 0) { perror("open test error"); exit(1); } fd2 = open("test", O_WRONLY); dup2(fd, fd2); write(fd, str, strlen(str)); close(fd); printf("----------------done close(fd)--------------\n"); int ret = write(fd2, str2, strlen(str2)); if(ret == -1) { perror("write fd2 error"); exit(1); } close(fd2); return 0;}

示例代码:

#include 
#include
#include
#include
#include
int main(void){ int fd; char *str = "hello dup2\n"; //write(STDOUT_FILENO, str, strlen(str)); fd = open("test", O_WRONLY|O_TRUNC|O_CREAT, 0644); if(fd < 0) { perror("open test1 error"); exit(1); } //dup2(STDOUT_FILENO, fd); dup2(fd, STDOUT_FILENO); close(fd); //做文件关闭之前同样的事。 //int n = write(STDOUT_FILENO, str, strlen(str)); int n = write(fd, str, strlen(str)); printf("--------|%d\n", n); return 0;}

代码:

/*    编程程序,要求程序执行效果等同于命令 cat file1 - file2 > out 执行效果。     注:Linux 系统下, Ctrl+d 可输出一个文件结束标记 EOF。*/#include 
#include
#include
#include
#include
void sys_err(int fd, char *err_name){ if(fd < 0) { perror(err_name); exit(1); }}int main(void){ int fd_f1, fd_f2, fd_out; int ret; char buf[1024]; fd_f1 = open("file1", O_RDONLY); sys_err(fd_f1, "open file1 error"); fd_f2 = open("file2", O_RDONLY); sys_err(fd_f2, "open file2 error"); fd_out = open("out", O_WRONLY|O_TRUNC|O_CREAT, 0644); sys_err(fd_out, "open out error"); dup2(fd_out, STDOUT_FILENO); while ((ret = read(fd_f1, buf, sizeof(buf)))) { write(fd_out, buf, ret); } while ((ret = read(STDIN_FILENO, buf, sizeof(buf)))) { write(fd_out, buf, ret); } while ((ret = read(fd_f2, buf, sizeof(buf)))) { write(fd_out, buf, ret); } close(fd_f1); close(fd_f2); close(fd_out); return 0;}

fcntl 函数

fcntl 可以改变一个已打开的文件的属性,可以重新设置读、写、追加、非阻塞等标志(这些标志称为File Status Flag),而不必重新open文件

#include 
#include
int fcntl(int fd, int cmd, ... /* arg */ );int fcntl(int fd, int cmd);int fcntl(int fd, int cmd, long arg);int fcntl(int fd, int cmd, struct flock *lock);获取和设置文件的访问控制属性 参数解析: 第一个为已经打开的文件描述符 第二个为要对文件描述采取的动作 F_DUPFD 复制一个文件描述,返回值为新描述符。 F_GETFD/F_SETFD 目前只有FD_CLOEXEC一个,set时候会用到第三个参数。 F_GETFL / F_SETFL 得到或者设置目前的文件描述符属性,返回值为当前属性。设置时使用第三个参数。在本节的各实例中,第三个参数总是一个整数,与上面所示函数原型中的注释部分相对应。但在说明记录锁时,第三个参数则是指向一个结构的指针。fcntl函数有五种功能: •复制一个现存的描述符(cmd=F_DUPFD)。 •获得/设置文件描述符标记(cmd=F_GETFD或F_SETFD)。 •获得/设置文件状态标志(cmd=F_GETFL或F_SETFL)。 •获得/设置异步I/O有权(cmd=F_GETOWN或F_SETOWN)。 •获得/设置记录锁(cmd=F_GETLK,F_SETLK或F_SETLKW)。我们先说明这十种命令值中的前七种(后三种都与记录锁有关,当讲解记录锁时说明)将涉及与进程表项中各文件描述符相关联的文件描述符标志,以及每个文件表项中的文件状态标志,

这个函数和open一样,也是用可变参数实现的,可变参数的类型和个数取决于前面的cmd参数。下面的例子使用F_GETFL和F_SETFL这两种fcntl命令改变STDIN_FILENO的属性,加上O_NONBLOCK选项,实现和 “非阻塞读终端” 同样的功能。

简单使用:

1、获取文件的flags,即open函数的第二个参数:    flags = fcntl(fd,F_GETFL,0);2、设置文件的flags:    fcntl(fd,F_SETFL,flags);3、增加文件的某个flags,比如文件是阻塞的,想设置成非阻塞:    flags = fcntl(fd,F_GETFL,0);    flags |= O_NONBLOCK;    fcntl(fd,F_SETFL,flags);4、取消文件的某个flags,比如文件是非阻塞的,想设置成为阻塞:    flags = fcntl(fd,F_GETFL,0);    flags &= ~O_NONBLOCK;    fcntl(fd,F_SETFL,flags);

用fcntl改变File Status Flag

#include 
#include
#include
#include
#include
#define N 1024int main(void){ int flags, n; char buf[N]; flags = fcntl(STDIN_FILENO, F_GETFL); flags |= O_NONBLOCK; fcntl(STDIN_FILENO, F_SETFL, flags); again: n = read(STDIN_FILENO, buf, N); if(n == -1) { if(errno == EWOULDBLOCK) // errno == EAGAIN(非阻塞读终端) { printf("no data...\n"); sleep(3); goto again; } else { perror("read error"); exit(1); } } write(STDOUT_FILENO, buf, n); return 0;}

或者 

#include 
#include
#include
#include
#include
#include
#define MSG_TRY "try again\n"int main(void){ char buf[10]; int flags, n; flags = fcntl(STDIN_FILENO, F_GETFL); if(flags == -1) { perror("fcntl error"); exit(1); } flags |= O_NONBLOCK; int ret = fcntl(STDIN_FILENO, F_SETFL, flags); if(ret == -1) { perror("fcntl error"); exit(1); }tryagain: n = read(STDIN_FILENO, buf, 10); if(n < 0) { if(errno != EAGAIN) { perror("read /dev/tty"); exit(1); } sleep(3); write(STDOUT_FILENO, MSG_TRY, strlen(MSG_TRY)); goto tryagain; } write(STDOUT_FILENO, buf, n); return 0;}

示例程序2:

//获取和设置文件flags举例#include 
#include
#include
#include
#include
char buf[500000];int main(int argc,char *argv[]){ int ntowrite,nwrite; const char *ptr ; int flags; ntowrite = read(STDIN_FILENO,buf,sizeof(buf)); if(ntowrite <0) { perror("read STDIN_FILENO fail:"); exit(1); } fprintf(stderr, "read %d bytes\n", ntowrite); if((flags = fcntl(STDOUT_FILENO,F_GETFL,0))==-1) { perror("fcntl F_GETFL fail:"); exit(1); } flags |= O_NONBLOCK; if(fcntl(STDOUT_FILENO,F_SETFL,flags)==-1) { perror("fcntl F_SETFL fail:"); exit(1); } ptr = buf; while(ntowrite > 0) { nwrite = write(STDOUT_FILENO,ptr,ntowrite); if(nwrite == -1) { perror("write file fail:"); } if(nwrite > 0) { ptr += nwrite; ntowrite -= nwrite; } } flags &= ~O_NONBLOCK; if(fcntl(STDOUT_FILENO,F_SETFL,flags)==-1) { perror("fcntl F_SETFL fail2:"); } return 0;}

sync函数

#include 
int fsync(int filedes) //把指定文件的数据和属性写入到磁盘。 int fdatasync(int filedes) //把指定文件的数据部分写到磁盘。 void sync(void) //把修改部分排入磁盘写队列,但并不意味着已经写入磁盘。

ioctl 函数

ioctl 函数是 I/O 操作的杂物箱。不能用本章中其他函数表示的 I/O 操作通常都能用 ioctl 表示。终端 I/O是 ioctl 的最大使用方面

#include 
int ioctl(int fd, unsigned long request, ...); // 获取和设置文件特有的物理属性fd 是某个设备的文件描述符。request 是ioctl的命令,可变参数取决于request,通常是一个指向变量或结构体的指针。若出错则返回-1,若成功则返回其他值,返回值也是取决于request。

ioctl用于向设备发控制和配置命令,有些命令也需要读写一些数据,但这些数据是不能用read/write读写的,称为Out-of-band数据。也就是说,read/write读写的数据是in-band数据,是I/O操作的主体,而ioctl命令传送的是控制信息,其中的数据是辅助的数据。例如,在串口线上收发数据通过read/write操作,而串口的波特率、校验位、停止位通过ioctl设置,A/D转换的结果通过read读取,而A/D转换的精度和工作频率通过ioctl设置。

以下程序使用TIOCGWINSZ命令获得终端设备的窗口大小。

#include 
#include
#include
#include
int main(void){ struct winsize size; if (isatty(STDOUT_FILENO) == 0) exit(1); if(ioctl(STDOUT_FILENO, TIOCGWINSZ, &size)<0) { perror("ioctl TIOCGWINSZ error"); exit(1); } printf("%d rows, %d columns\n", size.ws_row, size.ws_col); return 0;}

/dev/fd

        比较新的系统都提供名为 /dev/fd 的目录,其目录项是名为 0、1、2等的文件。打开文件 /dev/fd/n 等效于复制描述符n (假定描述符n是打开的)。

在函数中调用:

        fd = open("/dev/fd/0", mode);

大多数系统忽略所指定的 mode,而另外一些则要求 mode 是所涉及的文件 (在这里则是标准输入)原先打开时所使用的 mode 的子集。因为上面的打开等效于:
        fd = dup(0);
描述符 0 和 fd 共享同一文件表项(见图3 - 3 )。例如,若描述符0被只读打开,那么我们也只对 fd 进行读操作。即使系统忽略打开方式,并且下列调用成功:
        fd = open("/dev/fd/0", O_RDWR);
我们仍然不能对 fd 进行写操作。

        我们也可以用/dev/fd作为路径名参数调用creat,或调用open,并同时指定O_CREAT。这就允许调用creat的程序,如果路径名参数是/dev/fd/1等仍能工作。

某些系统提供路径名/dev/stdin,/dev/stdout和/dev/stderr。这些等效于/dev/fd/0,/dev/fd/1和/dev/fd/2。
        /dev/fd文件主要由shell使用,这允许程序以对待其他路径名一样的方式使用路径名参数来处理标准输入和标准输出。例如,cat(1)程序将命令行中的一个单独的-特别解释为一个输入文件名,该文件指的是标准输入。例如:
        filterfile2|catfile1-file3|lpr
首先cat读file1,接着读其标准输入(也就是filterfile2命令的输出),然后读file3,如若支持/dev/fd,则可以删除cat对-的特殊处理,于是我们就可键入下列命令行:
        filterfile2|catfile1/dev/fd/0file3|lpr
在命令行中用-作为一个参数特指标准输入或标准输出已由很多程序采用。但是这会带来一些问题,例如若用-指定第一个文件,那么它看来就像开始了另一个命令行的选择项。/dev/fd则提高了文件名参数的一致性,也更加清晰。

最后些一个测试程序,希望可以用到里面大多数函数,用于测试其功能。这个程序功能是打开一个文件,在里面写入hello world,然后调用dup函数复制一个文件描述符,随后调用lseek将偏移量设置到hello之后,最后读出文件内容world打印到终端显示。代码如下所示

#include 
#include
#include
#include
#include
int main(void) { int fd, fdd, ret; char str[]="hello world!"; char buf[10]; fd = open("file", O_RDWR|O_CREAT|O_TRUNC, 755); if(fd < 0){ perror("open error"); exit(1); } ret = write(fd, str, sizeof(str)); if(ret != sizeof(str)){ perror("write error"); exit(1); } fdd = dup(fd); if(ret == -1){ perror("dup error"); exit(1); } lseek(fdd, 6, SEEK_SET); memset(buf,0,sizeof(buf)); ret = read(fdd, buf, sizeof(buf)); if(ret < 0){ perror("read error"); exit(1); } printf("%s/n",buf); return 0; }

统计一个目录下普通文件个数:

/*    编程统计指定目录下普通文件个数。     包括其子目录下的普通文件.    将文件总数打印至屏幕。*/#include 
#include
#include
#include
#include
#include
int count(char *root){ DIR *dp; struct dirent *item; int n = 0; char path[1024]; dp = opendir(root); //打开目录 if (dp == NULL) { perror("----opendir error"); exit(1); } //遍历每一个目录项,NULL表示读完 while ((item = readdir(dp))) { struct stat statbuf; //排除.目录和..目录 if (strcmp(item->d_name, ".") == 0 || strcmp(item->d_name, "..") == 0) continue; //将子目录和当前工作目录拼接成一个完整文件访问路径 sprintf(path, "%s/%s", root, item->d_name); /*取文件的属性, lstat防止穿透*/ if (lstat(path, &statbuf) == -1) { perror("lstat error"); exit(1); } if (S_ISREG(statbuf.st_mode)) { n++; } else if (S_ISDIR(statbuf.st_mode)) { n += count(path);//递归调用该函数 } } closedir(dp); return n;}int main(int argc, char *argv[]){ int total = 0; if (argc == 1) { total = count("."); printf("There are %d files in ./\n", total); } else if (argv[1]) { total = count(argv[1]); printf("There are %d files in %s\n", total, argv[1]); } return 0;}

你可能感兴趣的文章
Mysql 数据类型一日期
查看>>
MySQL 数据类型和属性
查看>>
mysql 敲错命令 想取消怎么办?
查看>>
Mysql 整形列的字节与存储范围
查看>>
mysql 断电数据损坏,无法启动
查看>>
MySQL 日期时间类型的选择
查看>>
Mysql 时间操作(当天,昨天,7天,30天,半年,全年,季度)
查看>>
MySQL 是如何加锁的?
查看>>
MySQL 是怎样运行的 - InnoDB数据页结构
查看>>
mysql 更新子表_mysql 在update中实现子查询的方式
查看>>
MySQL 有什么优点?
查看>>
mysql 权限整理记录
查看>>
mysql 权限登录问题:ERROR 1045 (28000): Access denied for user ‘root‘@‘localhost‘ (using password: YES)
查看>>
MYSQL 查看最大连接数和修改最大连接数
查看>>
MySQL 查看有哪些表
查看>>
mysql 查看锁_阿里/美团/字节面试官必问的Mysql锁机制,你真的明白吗
查看>>
MySql 查询以逗号分隔的字符串的方法(正则)
查看>>
MySQL 查询优化:提速查询效率的13大秘籍(避免使用SELECT 、分页查询的优化、合理使用连接、子查询的优化)(上)
查看>>
mysql 查询,正数降序排序,负数升序排序
查看>>
MySQL 树形结构 根据指定节点 获取其下属的所有子节点(包含路径上的枝干节点和叶子节点)...
查看>>