linux文件io(open、read、write、close)

所有执行io操作的系统调用都是以文件描述符(大于0的整数)来指代打开的文件。文件描述符可以表示诸如管道(pipe)、fifo、socket、终端、设备和普通文件。对于每个进程,文件描述符都自成一套。
下图的3个文件描述默认都会打开(可以说都是继承自shell文件描述符的一个副本)

 文件描述符用途  posix 名称stdio流 
 0 标准输入 STDIN_FILENO stdin
 1 标准输出 STDOUT_FILENO stdout
 2 标准错误 STDERR_FILENO stderr

程序中可以用0、1、2来代表文件描述符也可以用<unistd.h>里的STDIN_FILENO、STDOUT_FILENO、STDERR_FILENO来表示。

下面介绍io操作主要的4个系统调用。

int open(const char *pathname, int flags, /* mode_t mode */);
打开文件并返回文件描述符,flags是掩码参数(下图),mode是权限相关(S_IRUSR | S_IWUSR | S_IXUSR      S_IRGRP | S_IWGRP | S_IXGRP     S_IROTH | S_IWOTH | S_IXOTH)

ssize_t read(int fd, void *buffer, size_t count);
读取最多count个字节到buffer中,返回读取到的字节数,如果独到文件末尾则返回0,出错返回-1。为啥说是最多呢,因为对于普通文件而言,有可能接近了文件结尾,对于其他如socket,fifo等就有其他复杂的因素。

ssize_t write(int fd, void *buffer, size_t count);
从buffer写入最多count个字节到文件中,fd是指代要写入的文件描述符。如果写入成功返回写入的字节数。返回值可能小于count造成部分写,有可能是磁盘满了,也有可能是进程资源的文件大小的限制。

int close(int fd);
关闭打开的文件描述符

off_t lseek(int fd, off_t offset, int whence);
改变文件偏移量

off_t curr = lseek(fd, 0, SEEK_CUR);  //获取当前offset
lseek(fd, 0, SEEK_SET); /*定位到文件的开始*/
lseek(fd, 0, SEEK_END); /*定位到文件结尾的下一个字节*/
lseek(fd, -1, SEEK_END); /* 定位最后一个字节*/
lseek(fd, -10, SEEK_CUR); /* 当前往回10字节 */
lseek(fd, 10000, SEEK_END); /* 距离结尾文件 10001 字节*/

int fcntl(int fd, int cmd, ...);
文件控制操作

//设置非阻塞
int flags = fcntl(fd, F_GETFL);    //获取当前的标志
flags = flags | O_NONBLOCK;    //追加
fcntl(fd, F_SETFL, flags);     //重新设置

//检测文件标志
if (flags & O_SYNC)
    printf("writes are synchronized\n");

//检测权限-略复杂
int accessMode = flags & O_ACCMODE;
if (accessMode == O_WRONLY || accessMode == O_RDWR)
    printf("file is writable\n");


ssize_t pread(int fd, void *buf, size_t count, off_t offset);
ssize_t pwrite(int fd, const void *buf, size_t count, off_t offset);

原子操作,可以手动指定(SEEK_SET)在offset偏移量来读取和写入操作,操作完后会复原当前的偏移量


文件描述符与打开文件

多个文件描述符可以指向同一个打开文件,同一个文件描述符可以指向多个打开文件。内核有3个数据结构。

1.进程级别的文件描述符表
2.系统级别的打开文件表
3.文件系统的i-node表

我们解释下上图,第1列一条数据代表一个文件描述符,第2列一条数据代表依次打开文件比如open,第3列一条数据代表一个文件(这里只代表普通文件)。
进程A的fd1,fd20是2个文件描述符,但是它们指向同一个打开文件23,fd20可能是通过dup,dup2,fcntl复制出来的。
进程A的fd2和进程B的fd2指向同一个打开文件73,则可能是进程B是进程A的子进程,或者是某进程通过unix域套接字将一个打开文件描述符传递个另一个进程。
进程A的fd0指向打开文件0,进程B的fd3指向打开文件86,单却指向同一个inode1976,换言之指向同一个文件,那是因为进程A和进程B都打开了同一文件,同一个进程调用2次open同一个文件也能发生类似情况。

总结:从上可以得出结论除了close-on-exec标志位每个进程的文件描述符所私有,其他的有可能会共用。比如进程A在fd1上修改文件偏移后会保存在打开文件23,那么fd20也会受到影响。


复制文件描述符

int dup(int oldfd);
复制文件描述符oldfd并返回一个新的文件描述符,2个文件描述符指向同一个打开文件,新文件描述符会取整数且最小未使用的。

int dup2(int oldfd, int newfd);
复制文件描述符oldfd并返回一个新的文件描述符 ,编号由newfd决定,如果newfd已经打开,则会先关闭newfd(由于dup2会忽略关闭newfd的错误,所以更为安全的做法是用close手动关闭newfd)。 

int newfd = fcntl(oldfd, F_DUPFD, startfd);
这可以用来复制,文件描述符编号会取大于等于startfd的最小未使用的整数。

int newfd = fcntl(oldfd, F_DUPFD_CLOEXEC, startfd);
这可以用来复制,文件描述符编号会取大于等于startfd的最小未使用的整数并在新的文件描述符上设置close-on-exec标志。  

int dup3(int oldfd, int newfd, int flags);
能实现上面同样的功能,在新的文件描述符上加上close-on-exec标志。


截断文件

int truncate(const char *pathname, off_t length);
对文件必须要有写的权限

int ftruncate(int fd, off_t length);
必须以写的方式先打开文件,不会改变文件的偏移量

/dev/fd

这个目录是一个软连接指向/proc/self,/proc/self也是一个软连接,它指向另一个目录,假设我们当前的进程号为6674,那么实际指向/proc/6674/fd。这个目录里有当前进程所有打开的文件描述符。可以利用这一特性来访问自己已打开的文件描述符,这一特性在shell中尤其有用。

创建临时文件

int mkstemp(char *template);
返回文件描述符,必须手动删除
FILE *tmpfile(void);
打开后自动删除但是文件依然存在于内存,可以操作,在linux如果一个文件被打开了,我们是可以手动删除这个文件的,但是这是只是删除了该文件名,内存中还是保留着该文件,可以对这个文件执行读写操作,当程序结束时,才算真的删除。基于这个特性,如果只是想生成临时文件,打开后就可立马删除,但是后续还可以操作该文件。

//例子
int fd;
char template[] = "/tmp/tmp.XXXXXX";
fd = mkstemp(template);                 //生成临时文件并打开 template=/tmp/tmp.CYDaN7
unlink(template);                       //手动删除

FILE *fp = tmpfile();                   //创建后会自动删除
fclose(fp);                             //自动删除


总结

1.对文件执行追加操作是原子操作,所以当多个进程以追加方式同时打开一个文件不会出现竞争危险。2.本文对linux文件io做了简单的介绍,因为io是编程的基本,所以放在比较靠前来讲,如果有疑问可以给我留言。

上一篇: linux系统调用及错误处理
下一篇: linux进程
作者邮箱: 203328517@qq.com