`
DiaoCow
  • 浏览: 242746 次
  • 性别: Icon_minigender_1
  • 来自: 南京
社区版块
存档分类
最新评论

【读书笔记】 《Unix环境高级编程》第三章 File I/O

阅读更多
对于unix系统而言,一切皆“文件”,所以我们就可以使用一套基本的IO函数,来对所有的“文件”进行读写操作!现在我们来看下文件操作的几个底层IO

1.open函数用来打开或创建一个文件,其函数原型如下:
int open(const char *pathname, int oflag, ... /* mode_t mode */);

第一个参数pathname用来指定需要打开的文件名;
第二个参数oflag用来指定打开文件的方式以及其他一些附加属性,常用的值有:

O_RDONLY  只读
O_WRONLY  只写
O_RDWR    读写

以上三个属性必须指定一个,但是下列属性是可选的

O_APPEND该参数表示每次写操作都是将内容添加到文件末尾(注意:使用该标记位后,每次调用write函数时,首先将file_table中current file offset字段(文件偏移)设置为文件末尾,然后在进行写操作,并且这一系列操作都是原子操作)
O_CREAT该参数表示当需要打开的文件不存在时,则创建(当指定这个标记位的时候,需要使用第三个参数mode,来指定创建文件的默认权限)
O_EXCL该参数表示如果需要打开的文件已经存在,并且指定了O_CREAT标记,则报错。该参数的作用可以参考下列讨论 http://bbs.csdn.net/topics/350001997
O_TRUNC该参数表示如果打开的文件已经存在,则删除文件中的内容(将文件大小截断为0)


如果打开或创建文件成功,open会返回一个文件描述符(int型值),否则返回-1表示调用失败

2.在较老的版本中,open函数只能用来打开一个已存在的文件,所以当文件不存在时,open函数会报错;若我们希望创建一个新的文件,则必须调用一个creat函数:
int creat(char *pathname, mode_t mode);

该函数等价于: open(pathname, O_WRONLY | O_CREAT | O_TRUNC, mode_t)

creat函数有一个不足,就是对于新创建的文件只能够进行写操作,若是你想对这个刚创建的文件进行读操作,就只能先close掉,然后再open进行读操作,非常繁琐,但是如果我们使用最新的open函数(对于不存在的文件,可以先创建然后再打开),则可以很简单的实现这一功能:open(pathname, O_RDWR | O_CREAT | O_TRUNC, mode_t)

所以,基本上我们可以使用open函数来替代creat函数

3.当我们成功调用open或者是creat函数时,会得到一个文件描述符(用来代表我们需要操作的文件),接着我们就可以拿着这个文件描述符对文件进行读写操作

a.read函数原型如下:
ssize_t read(int fileds, void *buf, size_t nbytes);

第一个参数fileds是文件描述符,第二个和第三个参数用来指定读取数据的存放位置以及每次read函数允许读取数据的最大数量(nbytes),read函数的返回值为此次调用读取到的字节数,若读取失败返回-1;

默认情况下,只有读取完nbytes个字节后,read函数才会返回,但是当遇到以下几种情况时,在未读取足够的字节时(< nbytes),read函数也会返回:
1.如果读取的是一个普通文件,当遇到EOF时,read函数立即返回;
2.如果读取的是一个终端设备(譬如标准输入),当遇到一个换行符时,read函数立即返回;
3.其余情况参考《apue》(自己尚未理解,暂不罗列);

b.write函数原型:
ssize_t write(int fileds, void *buf, size_t nbytes);

该函数返回值表示成功写入的字节数,若返回值等于nbytes表示写操作成功;
该函数比较简单,但是要注意的是,当文件打开标记含有O_APPEND时,每次写操作(调用write函数)首先会将当前的文件的偏移设置为文件默认,然后再写入输出数据;

4.当我们完成读写操作后,需要调用close函数关闭刚才打开的文件描述符(释放资源),close函数原型为:
int close(int fileds);

注意,即使我们没有主动关闭文件描述符,当一个进程终止时,也会关闭所有还未关闭的文件描述符;说了这么多,我们来看一个例子(会用上我们之前说的所有函数):
#include <apue.h>
#include <fcntl.h>
#define BUF_SIZE 4096
#define FILE_MODE 0666 

int main(void) 
{
    int fd, n;
    char buf[BUF_SIZE];

    if ((fd = open("out.tmp", O_WRONLY | O_CREAT | O_TRUNC, FILE_MODE)) < 0) {
        printf("Fail to open out.tmp\n");
    }   

    while ((n = read(STDIN_FILENO, buf, BUF_SIZE)) > 0) {
        if (write(fd, buf, n) != n) {
            printf("Fail to write out.tmp\n");
        }   
    }   

    close(fd);
    
    return 0;
}

这样我们就用最底层文件IO实现了将标准输入内容输出到文件的功能!
ps:一个进程中默认有三个已打开的文件描述符:STDIN_FILENO(标准输入),STDOUT_FILENO(标准输出),STDERR_FILENO(标准错误输出)

5.通常我们是从文件开始处读写数据,但有的时候我们可能需要在文件中间某个位置读写数据,要实现这样的功能,需要使用lseek函数,其函数原型如下:
off_t lseek(int filedes, off_t offset, int wherece);

第二个参数offset表示相对于指定位置wherece的偏移量,wherece有以下3个值:

SEEK_SET 文件起始处
SEEK_CUR 文件当前位置
SEEK_END 文件末尾


--------------------------------------------------------------------------华丽风格线--------------------------------------------------------------------------

unix使用三种数据结构表示一个打开的文件,这三种结构是:
1.file_descripter_table(文件描述符表): 每个进程都维护了一个文件描述符表来操作和管理该进程打开的所有文件,该表的每条记录主要有两个字段:
a.文件描述符
b.文件指针(指向另一个数据结构file_table)

2.file_table(文件表):该表由操作系统维护,用来记录被打开文件的相关信息,其主要字段有:
a.文件标记(譬如我们使用open函数打开文件所指定的标记位)
b.文件偏移
c.v-node指针(指向另一个数据结构v-node_tabe)

3.v-node_table:关于它,只需要知道每一个打开的文件有且仅对应一个v-node即可

这三张表的大体结构我们已经说完,从文字描述中,我们也可以得知,这三表存在很强的关联性,现在我们通过下面这张图来更清晰的看下这几张表的关系:

当我们调用lseek函数时,其实只是移动了相关file_table中的current file offset,而未进行实际的IO操作(如果current file offset的值超过current file size,则更新v-node的current file size)

现在根据上图,我们来思考下,多个进程是如何实现文件享的,譬如A进程打开了文件hello.c,此时假如B进程也打开了hello.c文件 ,那么B进程能否读取hello.c文件并且B进程的读取是否会影响到A进程的读取?

要回答上面的问题,我们先看下面的图片


可以发现,当多个进程打开同一个文件时,只有v-node_table这一张表是共享的,其他都是独立的,这样每个进程就可以独立的记录自己的文件偏移,因此B进程的读取不会影响到A进程!通过这样的表结构组织,我们就实现了文件共享读!

最后我们在看下dup,dup2这两个函数,这两个函数时用来复制文件描述符的,那么复制文件描述符有什么作用呢?我们先看一个linux命令:

echo "hello, world" > out.tmp

该命令用来把"hello, world"从标准输出重定向到out.tmp文件中,那么这个重定向是如何实现的呢? 这里就需要使用dup或者dup2函数,把标准输出的文件指针指向指向out.tmp文件的file table, 如下图:


我们看下相关代码:
#include <apue.h>
#include <fcntl.h>

int main(void) 
{
	int fd, stdout_fd;
	char *str = "hello, world\n";

	if((fd = open("out.tmp", O_WRONLY | O_CREAT | O_TRUNC, 0666)) < 0) {
		printf("Fail to create out.tmp\n");
		exit(1);
	}

	stdout_fd = dup(STDOUT_FILENO);
    dup2(fd, STDOUT_FILENO);
	
	write(STDOUT_FILENO, str, strlen(str));
	dup2(stdout_fd, STDOUT_FILENO);
	write(STDOUT_FILENO, str, strlen(str));
	
	close(fd);

	return 0;
}

执行结果:


可以发现终端只输出了一次"hello, world",而原因是:第一次调用write输出时,标准输出文件描述符被重定向到文件(此时"hello,world"被输出到out.tmp文件),第二次调用write函数时,标注输出文件描述符已经恢原来设置(指向终端),所以此时我们看到在屏幕上输出了"hello,world"





  • 大小: 41.1 KB
  • 大小: 47.2 KB
  • 大小: 27.4 KB
  • 大小: 13.5 KB
分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics