⾛进C标准库(2)——stdio.h中的fopen函数
其他的库⽂件看起来没有什么实现层⾯的知识可以探究的,所以,直接来看stdio.h。
1.茶余饭后的杂谈,有趣的历史
在过去的⼏⼗年中,独⽴于设备的输⼊输出模型得到了飞速的发展,标准C从这个改善的模型中获益颇丰。
输⼊输出模块
在20世纪60年代早期,FORTRAN IV被认为是独⽴于机器的语⾔。但是如果不作任何改动,根本不可能在各种计算机体系结构中移动FORTRAN IV程序。可移植性的主要障碍是输⼊输出领域。在FORTRAN IV中,可以对FORTRAN IV代码中间的I/O语句中对正在通信的设备进⾏命名。CARD 和 INPUT TAPE就不⼀样。
之后,逐渐发展到使⽤逻辑单元号(LUN)来代替具体的设备名,从⽽在可执⾏的⼆进制卡⽚之前加⼊控制卡⽚,从⽽指定某个特殊的运⾏过程那些设备与特定的LUN相对应。这时候,独⽴于设备的I/O时代来临了。
设备独⽴的进⼀步改善得益于标准外围交换程序(peripheral interchange program,PIP)的进步。该程序允许指定源设备与⽬标设备的任意成,然后尽⼒执⾏两个设备之间的拷贝操作。
进⼊UNIX。UNIX对所有⽂本流采⽤了标准内部形式,⽂本的每⼀⾏以换⾏符终⽌。这正是程序读⼊⽂本时所期望的,也是程序输出所产⽣的。假如这样的约定不能满⾜和UNIX机器相连的处理⽂本的外围设备的需求,可以在系统的对外接⼝有些修改,不必修改任何内部代码。UNIX提供了两种机制来修正“对外接⼝”的⽂本流。⾸先的是⼀个通⽤的映射函数,它可以⽤任意的⽂本处理设备⼯作。可以⽤系统调⽤ioctl 来设置或者测试⼀个具体设备的的各种参数。另⼀个修正⽂本流的机制是修改直接控制该设备的专门软件。对于每⼀个UNIX可能需要控制的设备来说,⽤户必须添加⼀个常驻UNIX的设备管理器。
当第⼀个C编译器在UNIX平台上运⾏时,C语⾔就⾃然地继承了它的宿主操作系统简单的I/O模型。除了⽂本流的统⼀表⽰,还有其他⼀些优点。很久以前使⽤的LUNs在最近⼏⼗年也慢慢地演变为称为⽂件描述符或句柄的⾮常⼩的正整数。操作系统负责分发⽂件描述符。并且把所有的⽂件控制信息存储在⾃⼰的专⽤内存中,⽽不是让⽤户去分配和维持⽂件和记录控制块以加重负担。
为了简化多数程序的运⾏管理,UNIX shell分配给每个运⾏的程序3个标准⽂件描述符,这些就是现在普通使⽤的标准输⼊、标准输出和标准错误流。(⽂本流)
UNIX不会阻⽌向任意打开的⽂件写⼊任意的⼆进制编码,或者从⼀个⾜够⼤的地⽅把它们丝毫不变地读
取出来。(⼆进制流)
所以,UNIX消除了⽂本流(与⼈通信)和⼆进制流(与机器通信)之间的区别。
在相同的实现下,从⼀个⼆进制流读⼊的数据应该和之前写⼊到这个liu的数据相同,⽽⽂本流则不是。
PS:流是⼀个操作系统层⾯的⾼度抽象的概念,它隐藏了I/O设备具体的实质,⽽将所有的I/O带来的数据变化看做输⼊的流⼊和流出,这样,在操作系统层⾯为程序将各种I/O设备模拟成流的样式,已经使这时的I/O模块独⽴⽽抽象了。可以看到,I/O模型发展的过程,就是其逐渐抽象统⼀的过程,这⼀点与语⾔的发展的历程是相似的。
X3J11委员会在1983年开始召开会议为C起草ANSI标准。⾮UNIX系统的C⼚商和那些UNIX⽤户之间争论了很长时间,因为UNIX⽤户不能理解I/O为什么要这么⿇烦(显然,UNIX的⽂件结构和设备的管理机制保证了I/O模块的简洁性,这是相对于其他操作系统的优点)。这是⼀个很有教育意义的过程,这些争论的⼀个重要的副产品就是更清楚地阐明了C⽀持的I/O模块。
最终,委员会经过讨论整洁的重要性和向下兼容的重要性之后,决定抛弃UNIX风格的原语。(主要平衡代码效率和代码简洁性)
2.不识庐⼭真⾯露,包含的内容
类型:
FILE    它是⼀个对象类型,可以记录控制流需要的所有信息,包括它的⽂件定位符、指向相关缓冲(如果有的话)的指针、记录是否发⽣了读/写错误的错误提⽰符和记录⽂件⼿否结束的⽂件结束符(⽤来控制流的FILE对象的地址可能很重要,不必使⽤FILE对象的副本来代替原始的对象进⾏服务。)
库中的函数分两类:
1.针对任意流的操作;
2.指定特定问⽂件流的操作;
两者分别⼜有读写、⽂件定位、缓冲区控制等操作,可以完成对流的全⽅位操作,只要你能想到。
3.不畏浮云遮望眼,看实现吧
有两种设计决策对<stdio.h>的实现⾮常关键:
数据结构FILE的内容
与操作系统相互作⽤以执⾏实际输⼊/输出的低级原语
类型FILE:
------------------------------------------------------------------------------------------------------------------------------------------------------------
(此处纯为个⼈理解,不为原书内容)
不管是⼆进制流还是⽂本流,C都是将⽂件当做连续的字节流在处理。该字节流的信息以及⽂件对应的⽂件描述符等都是需要存储在FILE类型中的内容。
typedef struct _iobuf {
char *_ptr;fopen函数失败
int _cnt;
char *_base;
int _flag;
int _file;
int _charbuf;
int _bufsiz;
char *_tmpfname;
}FILE;
虽然不知道这些个变量是什么意思,但看⼀下⼀些函数的实现可以勉强猜⼀猜。
1.FOPEN
1 FILE * __cdecl _tfsopen (
2const _TSCHAR *file,
3const _TSCHAR *mode
4        ,int shflag
5        )
6 {
7        REG1 FILE *stream;
8        REG2 FILE *retval;
9
10        _ASSERTE(file != NULL);
11        _ASSERTE(*file != _T('\0'));
12        _ASSERTE(mode != NULL);
13        _ASSERTE(*mode != _T('\0'));
14
15/* Get a free stream */
16/* [NOTE: _getstream() returns a locked stream.] */
17
18if ((stream = _getstream()) == NULL)
19return(NULL);
20
21/* open the stream */
22 #ifdef _UNICODE
23        retval = _wopenfile(file,mode,shflag,stream);
24#else  /* _UNICODE */
25        retval = _openfile(file,mode,shflag,stream);
26#endif  /* _UNICODE */
27
28/* unlock stream and return. */
29        _unlock_str(stream);
30return(retval);
31 }
其中FILE *类型变量通过_getstream()获得。所以,我们可以稍微看下_getstream()的实现。不过在看_getstream()的实现之前,有必要介绍C 标准库中关于IO控制块(即FILE⽂件)的管理机制:
⾸先,在I/O控制块中有三个特殊的控制块,分别是标准输⼊、标准输出和标准错误流。其中标准输⼊有其输⼊⼤⼩的限制。
#define _INTERNAL_BUFSIZ 4096
char _bufin[_INTERNAL_BUFSIZ]; //标准输⼊流使⽤的存储单元
可以看到我使⽤的这个版本的C标准库中,标准输⼊流的⼤⼩为:4096。我们可以简单⽤代码测试⼀下:
1int main(void)
2 {
3
4int i;
5char s[10000];
6for( i = 0; i < 10000; i++ )
7        s[i] = 0;
8    scanf("%s",s);
9for(i = 0; i < 10000; i++ ){
10if( s[i] == 0){
11            printf("%d",i+1);
12break;
13        }
14    }
15return0;
16 }
输⼊为5000个1,输出结果为:4095。应该是有⼀个结束标志符的缘故。显然,那些超出_INTERNAL_BUFSIZ的字符串部分被舍去了。
该C标准库中,这个I/O控制块通过⼀个FILE数组_iob和⼀个FILE **指针来进⾏管理。如下:
代码块1:
#ifdef _WIN32
#define _NSTREAM_  512
/*
* Number of entries in _iob[] (declared below). Note that _NSTREAM_ must be
* greater than or equal to _IOB_ENTRIES.
*/
#define _IOB_ENTRIES 20
#else  /* _WIN32 */
#ifdef CRTDLL
#define _NSTREAM_  128        /* *MUST* match the value under ifdef _DLL! */
#else  /* CRTDLL */
#ifdef _DLL
#define _NSTREAM_  128
#else  /* _DLL */
#ifdef _MT
#define _NSTREAM_  40
#else  /* _MT */
#define _NSTREAM_  20
代码块2:
FILE _iob[_IOB_ENTRIES] = {
/* _ptr, _cnt, _base,  _flag, _file, _charbuf, _bufsiz */
/* stdin (_iob[0]) */
{ _bufin, 0, _bufin, _IOREAD | _IOYOURBUF, 0, 0, _INTERNAL_BUFSIZ },
/* stdout (_iob[1]) */
{ NULL, 0, NULL, _IOWRT, 1, 0, 0 },
/
* stderr (_iob[3]) */
{ NULL, 0, NULL, _IOWRT, 2, 0, 0 },
};
代码块3:
#ifdef CRTDLL
int _nstream = _NSTREAM_;
#else  /* CRTDLL */
int _nstream;
#endif  /* CRTDLL */
 void ** __piob;
if ( (__piob = (void **)_calloc_crt( _nstream, sizeof(void *) )) == NULL ) {
_nstream = _IOB_ENTRIES;
if ( (__piob = (void **)_calloc_crt( _nstream, sizeof(void *))) == NULL )
_amsg_exit( _RT_STDIOINIT );
}
for ( i = 0 ; i < _IOB_ENTRIES ; i++ )
__piob[i] = (void *)&_iob[i];
从代码块2中我们可以看到,FILE _iob[_IOB_ENTRIES]是⼀个FILE数组,其中预设了三种FILE类型,分别是stdin,stdout和stderr。因为不同平台下,I/O控制块数量的⼤⼩⾄少为20(从_NSTREAM_的定义看出),所以_IOB_ENTRIES定义为20,作为前20个I/O控制块。此处的值是不是20其实没有什么意义,只要这个数组能容纳下3个预设的I/O控制块,同时⼤⼩⾄于产⽣浪费空间的可能即可(⼤于20就有可能浪费)。
__piob是⼀个FILE **的⼆维指针,管理着⼀个FILE *的指针数组,⽤来指向陆续分配的I/O控制块的地址,这个指针数组的⼤⼩最⼤为
_NSTREAM_ = 512,可以测试⼀下这个数值。
1 #include <stdlib.h>
2 #include <stdio.h>
3
4
5int main(void)
6 {
7    FILE *p[600];
8char file_name[10];
9for(int i = 0; i < 600; i++){
10        itoa(i,file_name,10);
11        p[i] = fopen(file_name,"w");
12    }
13for(int i = 0; i< 600; i++)
14        fclose(p[i]);
15return0;
16 }
可以看到在⽂件夹中建⽴了509个⽂件,加上预置的stdin、stdout、stderr正好为512个。管理⽅式如图:
现在,我们将⽬光移回,看下_getstream()的实现:
1 FILE * __cdecl _getstream (
2void
3        )
4 {
5        REG2 FILE *retval = NULL;
6
7 #ifdef _WIN32
8
9        REG1 int i;
10
11/* Get the iob[] scan lock */
12        _mlock(_IOB_SCAN_LOCK);
13
14/*
15        * Loop through the __piob table looking for a free stream, or the
16        * first NULL entry.
17*/
18for ( i = 0 ; i < _nstream ; i++ ) {
19
20if ( __piob[i] != NULL ) {
21/*
22                * if the stream is not inuse, return it.
23*/
24if ( !inuse( (FILE *)__piob[i] ) ) {
25 #ifdef _MT
26                    _lock_str2(i, __piob[i]);
27
28if ( inuse( (FILE *)__piob[i] ) ) {
29                        _unlock_str2(i, __piob[i]);
30continue;
31                    }
32#endif  /* _MT */
33                    retval = (FILE *)__piob[i];
34break;
35                }
36            }
37else {
38/*
39                * allocate a new _FILEX, set _piob[i] to it and return a
40                * pointer to it.
41*/
42if ( (__piob[i] = _malloc_crt( sizeof(_FILEX) )) != NULL ) {
43
44#if defined (_MT)
45                    InitializeCriticalSection( &(((_FILEX *)__piob[i])->lock) );
46                    EnterCriticalSection( &(((_FILEX *)__piob[i])->lock) );
47#endif  /* defined (_MT) */
48                    retval = (FILE *)__piob[i];
49                }
50
51break;
52            }
53        }
54
55/*
56        * Initialize the return stream.
57*/
58if ( retval != NULL ) {
59            retval->_flag = retval->_cnt = 0;
60            retval->_tmpfname = retval->_ptr = retval->_base = NULL;
61            retval->_file = -1;
62        }
63
64        _munlock(_IOB_SCAN_LOCK);
65
66#else  /* _WIN32 */
67#if defined (_M_MPPC) || defined (_M_M68K)
68
69        REG1 FILE *stream = _iob;
70
71/* Loop through the _iob table looking for a free stream.*/
72for (; stream <= _lastiob; stream++) {
73
74if ( !inuse(stream) ) {
75                        stream->_flag = stream->_cnt = 0;
76                        stream->_tmpfname = stream->_ptr = stream->_base = NULL;
77                        stream->_file = -1;
78                        retval = stream;
79break;
80                }
81        }
82
83#endif  /* defined (_M_MPPC) || defined (_M_M68K) */
84#endif  /* _WIN32 */
85
86return(retval);
87 }
显然,在仍有FILE*指针可⽤的情况下,为第⼀个空闲的FILE *分配⼀⽚对应的FILE空间。即将新的stream纳⼊到了整个I/O控制块的管理中。
OK,我们再回到fopen函数中,在得到⼀个没有使⽤过的I/O控制块之后,显然下⼀步要做的就是对这个I/O块,根据设定的模式进⾏配置。此时,要调⽤到的就是_openfile函数。
在_openfile中,需要标记了stream._flag = streamflag;streamflag通过位标记了_IOREAD、_IOWRT当
前所进⾏操作的类型。stream._file得到了⼀个int类型的⽂件描述符(通过更底层的open系列函数建⽴了⽂件描述符,同时决定了该数据流是⽂本流还是⼆进制流)。
那么,我们可以得到FILE中两个变量的意义了。
过了⼀天,回过头来,我觉得有必要深究⼀下_openfile,看看底层的C标准库是如何调⽤WINAPI确定⽂件描述符,从⽽实现流的,这对于后⾯深⼊了解其他函数的实现有帮助。
1 FILE * __cdecl _openfile (
2const _TSCHAR *filename,
3        REG3 const _TSCHAR *mode,
4int shflag,
5        FILE *str
6        )
7 {
8        REG2 int modeflag;
9int streamflag = _commode;
10int commodeset = 0;
11int scanset    = 0;
12int whileflag;
13int filedes;
14        REG1 FILE *stream;
15
16        _ASSERTE(filename != NULL);
17        _ASSERTE(mode != NULL);
18        _ASSERTE(str != NULL);
19
20/* Parse the user's specification string as set flags in
21              (1) modeflag - system call flags word
22              (2) streamflag - stream handle flags word. */
23
24/* First mode character must be 'r', 'w', or 'a'. */
25
26switch (*mode) {
27case _T('r'):
28                modeflag = _O_RDONLY;
29                streamflag |= _IOREAD;
30break;
31case _T('w'):
32                modeflag = _O_WRONLY | _O_CREAT | _O_TRUNC;
33                streamflag |= _IOWRT;
34break;
35case _T('a'):

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。