所有语言的运行时系统都提供了高抽象层次的I/O操作函数。例如,ANSI C在标准I/O库中提供了诸如printf和scanf等I/O缓冲功能的函数;C++中则重载了<<和>>用来支持读写。在Unix系统中,这些高层次的函数基于Unix的系统I/O函数来实现,多数时候我们都无需直接使用底层的Unix I/O。但学习Unix系统I/O能更好地理解一些系统概念,而且当高层次的函数不适用时我们也能轻松地实现想要的功能,例如访问文件的元数据。
Unix/Linux将各种I/O设备统一而优雅地抽象为文件,在此基础上提供了一套非常简洁的低层次的API接口,也就是Unix I/O。这套API主要由以下五个函数组成:
end-of-file(EOF)
。EOF到底是什么东西?
首先必须明确一点,没有EOF字符这种东西。EOF是一种状态或条件,由操作系统内核去检测是否达到这种条件。当应用程序从read()中读取到0时,就说明达到EOF条件了。对于磁盘file,EOF表示位置k超过了文件大小。对于网络连接file,EOF表示连接的一端关闭了连接,另一端就会检测到EOF。
下面详细学习一下open()和close()函数:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
/** Returns: new file descriptor if OK, ?1 on error */
int open(char *filename, int flags, mode_t mode);
#include <unistd.h>
/** Returns: zero if OK, ?1 on error */
int close(int fd);
flags参数指明想要怎样访问文件,它可以由几个模式组合而成:
mode参数指明新建文件的访问权限,此参数可以省略。要注意的是:每个进程都有一个由umask()函数设置的umask
,open()函数最终创建出的文件的访问权限是mode & ~umask
:
read()和write()至多拷贝n个字节,返回值:-1表示错误,0表示EOF,大于0则表示实际拷贝的字节数。
#include <unistd.h>
/** Returns: number of bytes read if OK, 0 on EOF, ?1 on error */
ssize_t read(int fd, void *buf, size_t n);
/** Returns: number of bytes written if OK, ?1 on error */
ssize_t write(int fd, const void *buf, size_t n);
ssize_t和size_t有什么区别?
size_t被定义为unsigned int,而ssize_t被定义为int。因为read()和write()要返回-1表示发生错误,所以要使用ssize_t作为返回值。
#include <unistd.h>
int main(int argc, char const *argv[])
{
char c;
while(read(STDIN_FILENO, &c, 1) != 0) {
write(STDOUT_FILENO, &c, 1);
}
return 0;
}
在以下几种情况下,read()和write()可能会返回少于n个字节:
而当你从硬盘读取数据时,除非读到末尾触发EOF,你永远也不会碰到返回小于n字节的情况。同样写数据时也从不会碰到这种情况。
可以通过文件名或描述符获得文件的元数据。
#include <unistd.h>
#include <sys/stat.h>
/** Returns: 0 if OK, ?1 on error */
int stat(const char *filename, struct stat *buf);
int fstat(int fd, struct stat *buf);
/* Metadata returned by the stat and fstat functions */
struct stat {
dev_t st_dev; /* Device */
ino_t st_ino; /* inode */
mode_t st_mode; /* Protection and file type */
nlink_t st_nlink; /* Number of hard links */
uid_t st_uid; /* User ID of owner */
gid_t st_gid; /* Group ID of owner */
dev_t st_rdev; /* Device type (if inode device) */
off_t st_size; /* Total size, in bytes */
unsigned long st_blksize; /* Blocksize for filesystem I/O */
unsigned long st_blocks; /* Number of blocks allocated */
time_t st_atime; /* Time of last access */
time_t st_mtime; /* Time of last modification */
time_t st_ctime; /* Time of last change */
};
例如,下面是一个结合了文件打开关闭和元数据读取的小例子,首先新建文件,然后利用fstat()函数查看新创建文件的访问权限,并以类似ls命令的格式打印出来:
// printf, getchar
#include <stdio.h>
// malloc
#include <stdlib.h>
// open, mode_t
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
// close
#include <unistd.h>
mode_t getumask();
char *mode2str(mode_t mode);
int main(int argc, char const *argv[])
{
int fd;
struct stat stat;
/*
* 1.Print default umask.
* The typical default value for the process umask is:
* S_IWGRP | S_IWOTH (octal 022)
*/
printf("umask: %s\n", mode2str(getumask()));
// 2.Create a new file
if ((fd = open("foo.txt",
O_WRONLY | O_CREAT,
S_IRUSR | S_IWUSR)) == -1) {
fprintf(stderr, "Create file failed\n");
exit(1);
}
// 3.Check if new file mode = mode & ~mask
if (fstat(fd, &stat) == 0)
printf("file mode: %s\n", mode2str(stat.st_mode));
else
printf("Get metadata failed\n");
// 4.Close file
close(fd);
return 0;
}
/**
* There is a getumask(), but only specified in glib_c
* , not portable.
* @return current umask
*/
mode_t getumask()
{
mode_t mode = umask(0);
umask(mode);
return mode;
}
/**
* Acted like ‘ls -l‘
* @param mode file mode
* @return human-read
*/
char *mode2str(mode_t mode)
{
char *str = malloc(10 * sizeof(char));
int i = 0;
str[i++] = S_ISREG(mode) ? ‘F‘ : ‘D‘;
str[i++] = mode & S_IRUSR ? ‘R‘ : ‘-‘;
str[i++] = mode & S_IWUSR ? ‘W‘ : ‘-‘;
str[i++] = mode & S_IXUSR ? ‘X‘ : ‘-‘;
str[i++] = mode & S_IRGRP ? ‘R‘ : ‘-‘;
str[i++] = mode & S_IWGRP ? ‘W‘ : ‘-‘;
str[i++] = mode & S_IXGRP ? ‘X‘ : ‘-‘;
str[i++] = mode & S_IROTH ? ‘R‘ : ‘-‘;
str[i++] = mode & S_IRWXO ? ‘W‘ : ‘-‘;
str[i++] = mode & S_IXOTH ? ‘X‘ : ‘-‘;
return str;
}
Unix有很多种共享文件的方式,在学习之前先要了解Unix内核为每个文件维护了哪些数据结构。正是有这些数据结构的存在,才产生了多种多样的共享方式:
下面就看一下最常见的三种情况。
下面是一个小例子,open_twice()尝试重复打开一个文件两次,然后用两次得到的描述符从文件中读取一个字符,结果读到的是’f’,说明v-node表项相同,但file table未发生共享。而inherit_parent_fd()则是打开文件后,fork一个子进程,子进程先读取一个字符,父进程等子进程结束后再读取一个字符,结果读到的是’o’,说明file table发生共享,子进程的读取导致位置k发生偏移。
// printf
#include <stdio.h>
// exit
#include <stdlib.h>
// open, seek
#include <fcntl.h>
// read, write
#include <unistd.h>
// wait
#include <wait.h>
void open_twice(char *filename);
void inherit_parent_fd(char *filename);
int main(int argc, char const *argv[])
{
open_twice("foo.txt");
inherit_parent_fd("foo.txt");
return 0;
}
void open_twice(char *filename)
{
int fd1, fd2;
char c;
fd1 = open(filename, O_RDONLY);
fd2 = open(filename, O_RDONLY);
read(fd1, &c, 1);
read(fd2, &c, 1);
printf("c = %c\n", c);
}
void inherit_parent_fd(char *filename)
{
int fd;
char c;
fd = open(filename, O_RDONLY);
if (fork() == 0) {
read(fd, &c, 1);
exit(0);
}
wait(NULL);
read(fd, &c, 1);
printf("c = %c\n", c);
}
ANSI C定义了一组更多层次的I/O函数,叫做标准I/O库(libc),提供了Unix C外的另一种选择:
标准I/O库将打开的file抽象为stream。对于程序员来说,stream就是指向FILE类型的指针。stream或者说FILE类型,其实= file描述符 + buffer,其目的就是在内部维护一块缓冲区,从而避免频繁调用开销很大的系统调用。此外,每个ANSI C程序启动时都会自动打开三个stream,对应标准输入、输出、错误流:
#include <stdio.h>
extern FILE *stdin; /* Standard input (descriptor 0) */
extern FILE *stdout; /* Standard output (descriptor 1) */
extern FILE *stderr; /* Standard error (descriptor 2) */
大多数C程序员在其整个职业生涯中都只使用标准I/O库,而不会直接使用底层的Unix I/O,这也是推荐的做法。但标准I/O库在处理网络全双工通信时会有些限制:
Unix Shell提供了I/O重定向操作符,例如unix> ls > foo.txt。这个功能是由dup2()函数来完成的。dup2会拷贝oldfd的描述符表entry覆盖到newfd的entry,假如之前newfd是打开状态的,那么dup2会先关闭newfd再开始拷贝。例如,默认stdout对应fd1,假设foo.txt对应fd4,那么dup2(4, 1)会将fd4的描述符表entry覆盖到fd1,所以最终stdout会和fd4一样,都指向foo.txt文件的file table表项。
#include <unistd.h>
/** Returns: nonnegative descriptor if OK, ?1 on error */
int dup2(int oldfd, int newfd);
原文地址:http://blog.csdn.net/dc_726/article/details/45772221