【Linux】基础IO函数详解

1.函数open和openat

调用open或openat函数可以打开或创建一个文件。

#include <fcntl.h>
int open(const char *path, int ofag, ... /* mode_t mode */);

int openat (int fd, const char *path, int oflag, ... /* mode_t mode */);

我们将最后一个参数写为...,ISO C用这种方法表明余下的参数的数量及其类型是可变的对于open函数而言,仅当创建新文件时才使用最后这个参数(稍后将对此进行说明)。在函数型中将此参数放置在注释中。


path参数是要打开或创建文件的名字。

oflag参数可用来说明此函数的多个选项。用下列一个或多个常量进行“或”运算构成oflag参数(这些常量在头文件<fcntl.h>中定义)。

  • O_RDONLY:只读打开。
  • O_WRONLY:只写打开。
  • O_RDWR:读、写打开。

大多数实现将ORDONLY定义为0,O_WRONLY定义为1,O_RDWR定义为2,以与早身的程序兼容。这3个大家都熟悉,我们在初阶基础IO的时候讲过

  • O EXEC:只执行打开。
  • O_SEARCH:只搜索打开(应用于目录)。

O_SEARCH常量的目的在于在目录打开时验证它的搜索权限。对目录的文件描述符的后续操作就不需要再次检查对该目录的搜索权限。很多操作系统目前都没有支持O_SEARCH,包括我的也没有支持

注意在这上面5个常量中必须指定一个且只能指定一个。

下列常量则是可选的。

O_APPEND:每次写时都追加到文件的尾端。

O_CLOEXEC:把FD_CLOEXEC常量设置为文件描述符标志

O_CREAT:若此文件不存在则创建它。使用此选项时,open函数需同时说明第3个参数mode(openat函数需说明第4个参数mode),用mode指定该新义件的访问权限位。

O_DIRECTORY:如果path引用的不是目录,则出错。

O_EXCL:如果同时指定了O_CREAT,而文件已经存在,则出错。用此可以测试一个文件是否存在,如果不存在,则创建此文件,这使测试和创建两者成为一个原子操作。

O_NOCTTY:如果path引用的是终端设备,则不将该设备分配作为此进程的控制终端。

O_NOFOLLOW如果path 引用的是一个符号链接,则出错。

O_NONBLOCK:如果path引用的是一个FIFO、一个块特殊文件或一个字符特殊文件,则此选项为文件的本次打开操作和后续的1/O操作设置非阻塞方式。

较早的System V引入了O_NDELAY(不延迟)标志,它与O_NONBLOCK(不阻塞)选项类似,但它的读操作返回值具有二义性。

        如果不能从管道、FIFO或设备读得数据,则不延迟选项使read返回0,这与表示已读到文件尾端的返回值0冲突。

        基于SVR4的系统仍支持这种语义的不延迟选项,但是新的应用程序应当使用不阻塞选项代替之。

O_SYNC:使每次write等待物理IO操作完成,包括由该write 操作引起的文件属性更新所需的I/O。

O_TRUNC:如果此文件存在,而且为只写或读-写成功打开,则将其长度截断为0。

O_TTY_INIT:如果打开一个还未打开的终端设备,设置非标准termios参数值,使其符合Single UNIX Specification。

下面两个标志也是可选的。它们是Single UNIX Specification(以及POSIX.1)中同步输入和输出选项的一部分。

O_DSYNC:使每次write要等待物理I/O操作完成,但是如果该写操作并不影响读取刚写入的数据,则不需等待文件属性被更新。

O_DSYNC和O_SYNC标志有微妙的区别。

  • 仅当文件属性需要更新以反映文件数据变化(例如,更新文件大小以反映文件中包含了更多的数据)时,O_DSYNC标志才影响文件属性。
  • 而设置O_SYNC 标志后,数据和属性总是同步更新。
  1. 当文件用O_DSYN标志打开,在重写其现有的部分内容时,文件时间属性不会同步更新。
  2. 与此相反,如果文件是用O_SYNC标志打开,那么对该文件的每一次write都将在write返回前更新文件时间,这与是否改写现有字节或追加写文件无关。

O_RSYNC:使每一个以文件描述符作为参数进行的read操作等待,直至所有对文件同一部分挂起的写操作都完成。

  • Solaris 10 支持所有这 3个标志。
  • FreeBSD(和 Mac OS X)设置了另外一个标志(O_FSYNC),它与标志O_SYNC的作用相同。因为这两个标志是等效的,它们定义的标志具有相同的值。
  • FreeBSD8.0不支持O_DSYNC或O_RSYNC标志。
  • MacOSX并不支持O_RSYNC,但却定义了O_DSYNC,处理O_DSYNC与处理O_SYNC相同。
  • Linux3.2.0定义了O_DSYNC,但处理O_RSYNC与处理O_SYNC相同.

由open和openat函数返回的文件描述符一定是最小的未用描述符数值。这一点被某些应用程序用来在标准输入、标准输出或标准错误上打开新的文件。

例如,一个应用程序可以先关闭标准输出(通常是文件描述符1),然后打开另一个文件,执行打开操作前就能了解到该文件一定会在文件描述符1上打开。在说明dup2函数时,可以了解到有更好的方法来保证在一个给定的描述符上打开一个文件。

fd参数把open和openat函数区分开,共有3种可能性。

  1. path 参数指定的是绝对路径名,在这种情况下,fd 参数被忽略,openat 函数就相当于open函数。
  2. path 参数指定的是相对路径名,fd参数指出了相对路径名在文件系统中的开始地址。jd参数是通过打开相对路径名所在的目录来获取。
  3. path参数指定了相对路径名,fd参数具有特殊值AT_FDCWD。在这种情况下,路径初当前工作目录中获取,openat 函数在操作上与open函数类似。

openat 函数是POSIX.1最新版本中新增的一类函数之一,希望解决两个问题。

        第一,让线程可以使用相对路径名打开目录中的文件,而不再只能打开当前工作目录。同一进程中的所有线程共享相同的当前工作目录,因此很难让同一进程的多个不同线程在育一时间工作在不同的目录中。

        第二,可以避免time-of-check-to-time-of-use (TOCTTOU)错误.

TOCTTOU 错误的基本思想是:如果有两个基于文件的函数调用,其中第二个调用依赖于算一个调用的结果,那么程序是脆弱的。

        因为两个调用并不是原子操作,在两个函数调用之间文体可能改变了,这样也就造成了第一个调用的结果就不再有效,使得程序最终的结果是错误的。文件系统命名空间中的TOCTTOU错误通常处理的就是那些颠覆文件系统权限的小把戏,这些小把戏通过骗取特权程序降低特权文件的权限控制或者让特权文件打开一个安全漏洞等方式进行,Wei 和Pu[2005]在UNIX文件系统接口中讨论了TOCTTOU的缺陷。

文件名和路径名截断

如果NAMEMAX是14,而我们却试图在当前目录中创建一个文件名包含15个字符的新文件,此时会发生什么呢?

按照传统,早期的SystemV版本(如SVR2)允许这种使用方法,但总是将文件名截断为14个字符,而且不给出任何信息,而BSD类的系统则返回出错状态,并将errno设置ENAMETOOLONG。

无声无息地截断文件名会引起问题,而且它不仅仅影响到创建新文件

如果NAMEMAX是14,而存在一个文件名恰好就是14个字符的文件,那么以路径名作为其参数的任一函数(open、stat等)都无法确定该文件的原始名是什么。其原因是这些函数无法判断该文件名是否被截断过。

在POSIX.1中,常量POSIXNO_TRUNC决定是要截断过长的文件名或路径名,还是返回一个出错。

正如我们已经见过的,根据文件系统的类型,此值可以变化。我们可以用fpathconf或pathconf来查询目录具体支持何种行为,到底是截断过长的文件名还是返回出错。

是否返回一个出错值在很大程度上是历史形成的。例如。基于SVR4 的系统对传统的System V文件系统(S5)并不出错,但是它对BSD风格的文件系统(UFS)则出错。作为另一个例子(参见图2-20),Solaris对UFS返回出错,对与DOS 兼容的文件系统PCES则不返回出错,其原因是DOS会无声无息地截断不匹配8.3格式的文件名。BSD类系统和Linux总是会返回出错。

若_POSIXNO_TRUNC有效,则在整个路径名超过PATHMAX,或路径名中的任一文件名超过NAMEMAX时,出错返回,并将errno设置为ENAMETOOLONG

大多数的现代文件系统支持文件名的最大长度可以为255。因为文件名通常比这个限制要短,因此对大多数应用程序来说这个限制还未出现什么问题。

2.lseek函数

2.1.文件偏移量

Linux文件偏移量是指文件中当前读取或写入操作的位置。

每个打开文件都有一个与其相关联的“当前文件偏移量”(curent fileoffsct)。它通常是一个非负整数,用以度量从文件开始处计算的字节数(本节稍后将对“非负”这一修饰词的某些例外进行说明)。

每个打开的文件都有一个与之关联的文件偏移量,用于跟踪下一次读取或写入。

通常,读、写操作都从当前文件偏移量处开始,并使偏移量增加所读写的字节数。按系统默认的情况,初始文件偏移量通常为0,表示从文件开头开始,当打开一个文件时,除非指定OAPPEND选项,否则就从文件开头开始进行读写.

可以调用lseek显式地为一个打开文件设置偏移量。

返回值:

  • 若成功,返回新的文件偏移量;
  • 若出错,返回-1

对参数offset 的解释与参数whence的值有关。

  1. 若whence 是SEEK_SET,则将该文件的偏移量设置为距文件开始处offset个字节。
  2. 若whence 是SEEK_CUR,则将该文件的偏移量设置为其当前值加offset,offset 可为正或负。
  3. 若whence是SEEK_END,则将该文件的偏移量设置为文件长度加offset,offset可正可负。

若lseek成功执行,则返回新的文件偏移量,为此可以用下列方式确定打开文件的当前偏移量:

off_t currpos;
currpos = lseek(fd, 0, SEEK_CUR);

这种方法也可用来确定所涉及的文件是否可以设置偏移量。如果文件描述符指向的是一个管道、FIFO或网络套接字,则1seek返回-1,并将errno设置为ESPIPE。

3个符号常量 SEBK_SET、SEDK_CUR 和 SEEK_END 是在 System V中引入的。

在 System V之前,whence 被指定为0(绝对偏移量)、1(相对于当前位置的偏移量)或2(相对文件尾端的餐移量)。很多软件仍然把这些数字直接写在代码里。

        在lseek中的字符1表示长整型。在引入off_t数据类型之前,offset参数和返回值是长量型的。lseek是在UNIXV7中引入的,当时C语言中增加了长整型(在UNIXV6中,用函数see和tel1提供类似功能)。

所示的程序用于测试对其标准输入能否设置偏移量。

通常,文件的当前偏移量应当是一个非负整数,但是,某些设备也可能允许负的偏移量。但对于普通文件,其偏移量必须是非负值。因为偏移量可能是负值,所以在比较lseek的返回值时应当谨慎,不要测试它是否小于0,而要测试它是否等于-1。

在Intelx86处理器上运行的FreeBSD的设备/dev/kmem支持负的偏移量。
        因为偏移量(off_t)是带符号数据类型,所以文件的最大长度会减少一半。例如,若off_t是32位整型,则文件最大长度是2^{31}-1字节。

lseek仅将当前的文件偏移量记录在内核中,它并不引起任何I/O操作。然后,该偏移量用于下一个读或写操作。

        文件偏移量可以大于文件的当前长度,在这种情况下,对该文件的下一次写将加长该文件,并在文件中构成一个空洞,这一点是允许的。位于文件中但没有写过的字节都被读为0。

        文件中的空洞并不要求在磁盘上占用存储区。具体处理方式与文件系统的实现有关,当定位到超出文件尾端之后写时,对于新写的数据需要分配磁盘块,但是对于原文件尾端和新开始写位置之间的部分不需要分配磁盘块

2.2. lseek移动文件读写位置示例

我们使用read()和write()函数来实现向一个文件中写入内容并把写入内容打印到屏幕的功能。

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>

#define BUF_MAX 512 /*buf缓冲区最大值*/

/*向中文件写入数据并把写入内容打印到标准输出*/
int main(int argc, char* argv[])
{
	if(argc < 2)
	{
		printf("not fount file name");
		return -1;	
	}
	int fd = open(argv[1], O_RDWR | O_CREAT);
	write(fd, "hello linux...", 15);
	char buf[20];
	memset(buf, 0, sizeof(buf));
	int read_size = read(fd, buf, sizeof(buf));
	if(read_size > 0)
	{
		write(STDOUT_FILENO, buf, read_size);	/*STDIN_FILENO STDERR_FILENO*/
	}
	close(fd);
	return 0;
}

我们知道,在C语言中,字符串都是以 ‘\0’ 结尾的,比如 “hello linux…” 加上结束符共15字节。

write(fd, "hello linux...", 15);

我们来测试下程序,首先明确一点,字符串会写入相应文件,但是不会打印在屏幕中,这个后面分析。这里先看一下结束符 ‘\0’ 是如何显示的。

 

可以看到,确实不会打屏,且文件内容已写入。我们通过vim编辑器打开1.txt文件。

可以看到一个 ‘^@’ 字符,这个就是我们多写入的 ‘\0’ 字符,如果我们把写入字节数15改为14,就没有这个字符了。

下面我们通过上面的案例来分析lseek函数的用法,上面案例测试中说到,字符串已经写入了相应文件,但是并没有打印在屏幕中。这是因为,我们用write()函数写入文件之后,这时候读写位置就指在写完后的那个位置,也就是字符串的后面,这样我们在使用read()函数去读的时候就相当于从写入字符串的后面去读的,所以啥也没读到。这时候,就可以使用lseek()函数来移动读写位置,我们只需在上面代码中加一句话即可。

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>

#define BUF_MAX 512 /*buf缓冲区最大值*/

/*向中文件写入数据并把写入内容打印到标准输出*/
int main(int argc, char* argv[])
{
	if(argc < 2)
	{
		printf("not fount file name");
		return -1;	
	}
	int fd = open(argv[1], O_RDWR | O_CREAT);
	write(fd, "hello linux...", 15);
    /*读写位置在末尾*/
    /*把读写位置移动到文件首部*/
    lseek(fd, 0, SEEK_SET);
	char buf[20];
	memset(buf, 0, sizeof(buf));
	int read_size = read(fd, buf, sizeof(buf));
	if(read_size > 0)
	{
		write(STDOUT_FILENO, buf, read_size);	/*STDIN_FILENO STDERR_FILENO*/
	}
	close(fd);
	return 0;
}

再测试一下,就发现可以正常打屏了。 

2.3. lseek计算文件大小

利用lseek()函数执行成功时的返回值可以来计算一个文件所占字节的大小。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int main(int argc, char* argv[])
{
	if(argc < 2)
	{
		printf("not found filename\n");
		return -1;	
	}
	int fd = open(argv[1], O_RDONLY);
	int size = lseek(fd, 0, SEEK_END);
	printf("file size: %d\n", size);
	close(fd);
	return 0;
}

 

2.4. lseek拓展文件大小

        我们知道lseek()函数有三个参数,在前面的案例中,都把第二个参数偏移量offset设置为0来处理的,这样第三个参数就不用加偏移量了,相当于whence位置都是相对于文件首部来计算的。如果我们使用第二个参数offset,并把位置whence设置为文件尾,就相当于在文件尾再偏移offset个字节,这就达到了扩展文件大小的目的。

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>

int main(int argc, char* argv[])
{
	if(argc < 2)
	{
		perror("not found filename: ");
		return -1;	
	}
	int fd = open(argv[1], O_WRONLY);
	lseek(fd, 10, SEEK_END);
	close(fd);
	return 0;
}

 通过对比我们发现,文件大小并未增加。

这是因为通过lseek()扩展了文件的大小之后,如果我们没有对该文件进行写操作,那么这个扩展的内容默认是不会保存的,所以文件大小不会改变。

所以,在扩展后,至少要对文件写一次才能保存,我们对上面程序增加一个写操作,然后进行测试。 

int main(int argc, char* argv[])
{
	if(argc < 2)
	{
		perror("not found filename: ");
		return -1;	
	}
	int fd = open(argv[1], O_WRONLY);
	lseek(fd, 10, SEEK_END);
    write(fd, "a", 1);
	close(fd);
	return 0;
}

我们运行后发现,文件大小从0变成了11,扩展了11个字节,而我们程序中仅指定扩展了10个字节,

这是因为我们扩展完后又写入了一个字节a,通过前面的分析我们知道,在lseek()函数执行完毕后,读写位置应该是在文件尾部,这时再写入一个字符就相当于在文件尾部,也就是第11个字节出写入了一个a,保存后最终大小为11字节。我们可以使用vim打开文件查看一下。

可以看到10个 ‘^@’ 字符,第11个字符为写入的 ‘a’ 。 

3.read函数

调用read函数从打开文件中读数据。

如read成功,则返回读到的字节数。如已到达文件的尾端,则返回0。

有多种情况可使实际读到的字节数少于要求读的字节数:

  • 读普通文件时,在读到要求字节数之前已到达了文件尾端。例如,若在到达文件尾端之前有30字节,而要求读100字节,则read返回30。下一次再调用read时,它将返回0(文件尾端)。
  • 当从终端设备读时,通常一次最多读一行(第18章将介绍如何改变这一点)。
  • 当从网络读时,网络中的缓冲机制可能造成返回值小于所要求读的字节数。
  • 当从管道或FIFO读时,如若管道包含的字节少于所需的数量,那么read将只返回实际可用的字节数。
  • 当从某些面向记录的设备(如磁带)读时,一次最多返回一个记录。
  • 当一信号造成中断,而已经读了部分数据量时。

读操作从文件的当前偏移量处开始,在成功返回之前,该偏移量将增加实际读到的字节数。

POSIX.1从几个方面对read函数的原型做了更改。经典的原型定义是:

int read(int fd, char *buf, unsigned nbytes);
  • 首先,为了与ISOC一致,第2个参数由char*改为void*。在ISOC中,类型void*用于表示通用指针。
  • 其次,返回值必须是一个带符号整型(ssize_t),以保证能够返回正整数字节数、0(表示文件尾端)或-1(出错)。
  • 最后,第3个参数在历史上是一个无符号整型,这允许一个16位的实现一次读或写的数据可以多达65534字节。在1990 POSIX.1标准中,引入了新的基本系统数据类型ssize_t以提供带符号的返回值,不带符号的size_t 则用于第3个参数(见2.5.2节中的SSIZE MAX常量)。

当read函数调用时,如果文件没有数据可读,则会进入阻塞模式。也就是说,程序会暂停执行,直到读取到数据或者出现错误为止。

在这种情况下,read函数会等待(阻塞)直到满足以下任意条件之一:

  1. 在非阻塞模式下,数据尚未准备好,则返回错误代码EAGAIN或EWOULDBLOCK。
  2. 文件指针已到达文件末尾,此时返回0。
  3. 出现错误,返回-1。

如果希望使用非阻塞模式来读取数据,可以使用fcntl函数修改文件描述符的属性。

read时fd中的数据如果小于要读取的数据,就会引起阻塞。(关于read的阻塞情况评论区有朋友有不同意见,笔者查阅资料后作如下补充。)

以下情况read不会引起阻塞:

  • (1)常规文件不会阻塞,不管读到多少数据都会返回;
  • (2)从终端读不一定阻塞:如果从终端输入的数据没有换行符,调用read读终端设备会阻塞,其他情况下不阻塞;
  • (3)从网络设备读不一定阻塞:如果网络上没有接收到数据包,调用read会阻塞,除此之外读取的数值小于count也可能不阻塞。

下面是一个简单的使用 

#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>

#define BUF_SIZE 1024

int main()
{
    int fd = open("file.txt", O_RDONLY);
    if (fd == -1)
    {
        perror("open error");
        return -1;
    }

    char buf[BUF_SIZE];
    ssize_t bytesRead = read(fd, buf, BUF_SIZE);
    if (bytesRead == -1)
    {
        perror("read error");
        close(fd);
        return -1;
    }

    printf("Read %ld bytes: %s\n", bytesRead, buf);

    close(fd);
    return 0;
}

4. write函数

调用write函数向打开文件写数据。

返回值:若成功,返回已写的字节数;若出错,返回-1

        其返回值通常与参数count的值相同,否则表示出错。write出错的一个常见原因是磁盘已经写满,或者超过了一个给定进程的文件长度限制。
        对于普通文件,写操作从文件的当前偏移量处开始

如果在打开该文件时,指定了O_APPEND选项,则在每次写操作之前,将文件偏移量设置在文件的当前结尾处。在一次成功写之后件偏移量增加实际写的字节数。

(1)write()函数返回值一般无0,只有当如下情况发生时才会返回0:write(fp, p1+len, (strlen(p1)-len))中第三参数为0,此时write()什么也不做,只返回0。man手册给出的write()返回值的说明如下:

RETURN VALUE
       On success, the number of bytes written is returned (zero indicates nothing was written).  It is not  an  error  if this  number is smaller than the number of bytes requested; this may happen for example because the disk device was filled.  See also NOTES.

(2)write()函数从buf写数据到fd中时,若buf中数据无法一次性读完,那么第二次读buf中数据时,其读位置指针(也就是第二个参数buf)不会自动移动,需要程序员来控制,而不是简单的将buf首地址填入第二参数即可。如可按如下格式实现读位置移动:write(fp, p1+len, (strlen(p1)-len))。 这样write第二次循环时便会从p1+len处写数据到fp, 之后的也一样。由此类推,直至(strlen(p1)-len)变为0。

(3)在write一次可以写的最大数据范围内(貌似是BUFSIZ ,8192),第三参数count大小最好为buf中数据的大小,以免出现错误。(经过笔者再次试验,write一次能够写入的并不只有8192这么多,笔者尝试一次写入81920000,结果也是可以,看来其一次最大写入数据并不是8192,但内核中确实有BUFSIZ这个参数,具体指什么还有待研究)

下面是一个write函数的使用例子 

#include <string.h>
#include <stdio.h>
#include <fcntl.h>
int main()
{
  char *p1 = "This is a c test code";
  volatile int len = 0;
 
  int fp = open("/home/test.txt", O_RDWR|O_CREAT);
  for(;;)
  {
     int n;
 
     if((n=write(fp, p1+len, (strlen(p1)-len)))== 0)   //if((n=write(fp, p1+len, 3)) == 0) 
     {                                                 //strlen(p1) = 21
         printf("n = %d \n", n);
         break;
     }
     len+=n;
  }
  return 0;
}

 此程序中的字符串"This is a c test code"有21个字符,经笔者亲自试验,若write时每次写3个字节,虽然可以将p1中数据写到fp中,但文件test.txt中会带有很多乱码。唯一正确的做法还是将第三参数设为(strlen(p1) - len,这样当write到p1末尾时(strlen(p1) - len将会变为0,此时符合附加说明(1)中所说情况,write返回0, write结束。 
 

  • 29
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值