1.重定向
下面进入第四个话题,先说一下重定向。下面先写一段代码:
运行后整个结果符合我们的预期。下面基于上述代码来理解新知识:我们说过文件描述符本质是数组的下标,那么文件描述符对应的分配规则是什么?我们已经把文件打开了,默认是3。如果关闭0号文件描述符再运行一下:
此时对应的文件描述符是0。如果关闭1号再运行:
看到程序什么东西都没有显示出来,文件照样可打印。没显示是因为printf要向显示器打印,显示器所对应的stdout依赖的文件描述符是1,把1文件描述符关了对应的内容就显示不出来了。同样的关闭2号,再显示一下会发现fd显示是2。以上现象说明分配规则是从0下标开始,寻找最小的没有使用的数组位置,它的下标就是新文件的文件描述符。再改一下:
里面是打开文件和关闭文件,向显示器打印对应的内容。下面再调整一下:
看到运行后消息没有显示,因为把1号文件描述符关了很正常,但发现把本来应该显示到显示器上的内容写到了文件里,这个工作其实叫输出重定向。为什么这样做能完成输出重定向呢?因为先把1号文件描述符关了,相当于把进程和显示器的关联去了,1号文件描述符被腾出来了。紧接着打开一个对应文件时,新文件fd是1,此时1号下标指向新文件,所以写入时写到了文件里。可以通过下图再来理解一下:
运行时有个进程task_struct,每个进程有自己对应的文件描述符表struct file_struct,进程里有struct file_struct*files指针指向了对应的文件描述符表。文件描述表里包含了一个数组,整个数组叫struct file*fd_array[],下标分别是0、1、2…。进程启动时默认会打开三个流,0号指向对应的键盘文件(OS要先描述再组织,每个文件是个struct file),1和2号指向对应的显示器文件。我们还打开了一个文件叫log.txt,根据前面说的文件描述符分配规则,从数组中找最小的没被使用的下标,所以打开log.txt后往后给上层的文件表述符罢了。这份代码先做的是close(1),如果1指向的文件没人用,对应的文件和数组内容就直接被释放和置空了;如果有人用count变为1,内容置空,此时1号内容被腾出来了。在open时把log.txt地址填到1号文件描述符所对应的数组,1号数组下标本来指向显示器文件,现在转而指向log.txt文件,后续写入代码不知道OS底层把1号下标的内容改了,它只认1,向1写时变成了向普通文件写。我们上述说的思路将其称为重定向的原理,进行重定向时0、1、2这样的数字本身不变,本质是在对数组下标里面内容进行修改。
下面来进行进一步验证:对我们来说,我们把文件先关闭,再打开一个文件,就可以完成上述思路了。但这种方案是不行的,但必须要先自己关一次,然后紧接着打开一个文件,当别人问为啥这样做时需要给别人解释半天。其实系统中有一定系统调用,可以快速的不用让我们自己显示关闭而直接打开文件想重定向直接重定向,man dup2:
比如我想进行一个重定向工作,不想再close了,只想通过接口控制,打开文件后调一下重定向后面就重定向了。意思是说初始是这样的:
现在把3号文件描述符里的指针直接覆盖式的拷贝到1号里面(拷贝前可把1指向文件自动关了,拷贝后把3号里面的内容释放等):
所以没必要先关闭再打开,只要有个接口把文件描述符表里的指针内容做次拷贝就能完成重定向。因此有了一个dup2的接口,它有两个参数,分别是oldfd和newfd。这有段描述:
先来看个问题:拷贝后最终保留的是哪个文件描述符里的内容(全都是new还是全都是old)?描述中说了,最终都是oldfd,拷贝完只剩oldffd了,newfd是要被oldfd覆盖的。当我们要进行输出重定向时,把本来要显示到1号文件描述符对应的显示器文件内容直接重定向显示到fd所指向的log.txt文件,那么最后剩两个fd的内容还是1的内容呢?显然是fd,所以fd对应oldfd,1对应newfd。那这里说的拷贝是两个文件描述符在拷吗?不是了,文件描述符是数组下标,是常数,改不了,其实拷贝的是文件描述符在内核当中对应进程的文件描述符表中特定数组下标里的指针内容进行拷贝。下面测试一下:
发现实现了同样的效果。下面改为追加重定向:
这就是追加重定向,这里可知道追加重定向是打开文件时把选项由清空改为了追加。
再再做个测试,先回顾一下:
可用read从fd里把对应的数据读到buf指向的缓冲区,count是期望读多少字节,ssize_t类似于int,表示实际读了多少个。下面来看:
运行后阻塞了,因为当前从0读,进程读时发现键盘文件没有就绪:
下面改一下:
从标准输入读取转换为从新打开的文件读取,运行发现把文件中的内容读出来了,这就是输入重定向,本应该从键盘文件读,转而去指定文件读。下面再看一下:
因为printf或fprintf这里用的是stdin,里面封装的文件描述符是1,这里改了指向。目前知道了无论当前用系统调用还是库函数,最终消息打印都是符合的。继续看:
前面说的重定向和命令行上用的是什么关系?看下图:
前面我们自己写过一个shell,获取命令行可能有这样的情况。说白了>,>>,<符号都在获取时被整体当字符串读进来了,这些符号是整个字符串的一部分,符号左侧是指令,右侧是要重定向的文件。然后:
我们定义rdirfilename和rdir方式,然后:
通过Interact后commandline里不会带重定向符号,只有命令。下面执行普通命令,里面创建子进程,里面判断不同情况:
每次执行完从全局清空一下:
再进一步完善一下:
下面来编译看一下:
现在有个问题,我们做了重定向工作,后面我们在进行程序替换的时候难道不影响吗?
目前可以盘点一下了,之前说过进程=内核数据结构+自己的代码和数据,引入先描述再组织的概念后我们说每个进程都有自己的task_struct。后来我们又引入了一个数据结构叫mm_struct,与此同时引入了简易的页表映射关系,还有对应的物理内存。运行一个进程要把进程代码和数据放内存里,OS给我们创建对应地址空间pcb通过页表来映射,cpu找到进程调度。现在又知道了打开一个文件时要给我们创建struct file对象,进程为了维护自己和文件间的对应关系,就有了struct files_struct表,所以pcb里有struct files_struct*files这样的字段:
无论是打开文件,还是进程和打开文件产生关系的文件描述符数组,其中左侧这些都叫内核数据结构:
当我们打开文件重定向时,后面再做了程序替换,把新程序的代码和数据替换,修改页表和mm_struct部分字段:
它改的是进程代码和数据部分,和文件那边没有关系,很明显内存管理和文件操作之间也是一种解耦关系。因此进程历史打开的文件与进行的各种重定向关系都和未来进行程序替换无关,程序替换并不影响文件访问。
下面再来看个问题,先看问题1:1 vs 2,也就是我们重定向工作,当我们标准输出和标准错误输出时,都打到显示器上了,那有什么区别?下面写个代码:
都是往显示器上面打印,符合预期。下面再看:
重定向时发现error message这些消息没被重定向,normal message被重定向了:
因为输出重定向后,原来往1里写的会写到normal.log里,2没有做重定向,所以默认在显示器上继续显示。我们再继续加个指令:
回车后显示器上什么都没有,ll后看到有err.log和normal.log,打开后看到了它们的内容。其实上面的指令详细写是这样:
本来正常和错误消息是混在一起的,现在把它们分别重定向到不同文件,这样能很好的看出错误消息。那就想把它们弄到一起呢(上面详写,下面简写):
这写的什么意思?像前面就很直观,把标准输出的写到normal.log,标准输入的写到err.log(1可被省略)。这里代表的是./mytest>all.log完成后1已经指向all.log了,2>&1代表把1中的内容写到2,1里面的内容此时是all.log的地址,所以此时1和2都指向all.log,这样就写到了一个文件,如上就是重定向原理和操作后半部分。
再看第二个问题,如何理解linux中一切皆文件?所有操作计算机的动作,都是以进程形式进行操作的;所有访问文件的操作,都是用进程方式访问文件的。进程是OS帮用户完成任务的主要渠道,目前我们所有对文件的操作,都依赖于进程操作。在系统里一定会存在不同的设备,比如键盘、显示器、网卡、磁盘等,这里挪列的设备并没有包含内存、cpu这样的设备,这里列的设备大部分都是外设。先别说什么一切皆文件,磁盘和显示器就是不一样,比如属性、操作方法的不一样。但对OS来讲,在冯诺依曼体系结构中,虽然每一种外设需要的方法在实现上绝对不一样,但方法种类上差不多。比如磁盘有读写,显示器有读写,只不过写方法是有的,读方法是空的。我们每一种对应的设备最终给OS管理它,都要有对应的描述结构体,每种设备都要自己配上自己的读写方法,没有的置空:
也就是每种设备访问方法在实现上一定不一样,但在冯诺依曼中,每种方法其实可以都提供类似的接口。因为linux下一切皆文件,所以OS就说在我看来底层设备包括打开的普通文件都是文件,所以未来打开磁盘、显示器等都可以文件方式被系统调用open打开。打开后每个文件都要给它在内核中创建一种数据结构struct file,每种设备都有自己的读写方法,给每个设备都创建了struct_file。将来进程打开文件后要对文件读写,底层设备都不一样怎么做到让它访问不同的设备?因此linux内核里又提供了一个结构叫struct operation_func,这是一张方法表的数据结构:
每打开一个文件时,如打开磁盘文件时,为磁盘文件创建一个方法集对象,然后在struct file中包含一些指针指向这个对象:
里面是函数指针,然后把读磁盘的方法和写磁盘方法分别放过去:
让指针指向底层的方法。比如又打开了显示器,就创建一个通用的文件对象,然后创建一个方法集,然后以同样方法指向:
OS为了让所有人认为一切皆文件,用访问文件的方式访问所有的设备,所以进程被创建了,进程有了对应的文件描述符表,进程pcb指向自己的表,然后0、1、2分别指向对应的structfile:
然后OS专门给我们设计了系统调用。read,write它里面会传fd,通过task_struct找到files,再找到fd_array[fd],这样找到了对应的structfile,再找到f_ops,再找到方法:
此时系统层面看起来调的是read,但下面根据指向不同可通过一个上层接口调不同的方法。其实相当于OS在文件层面封装了一层叫struct file的文件对象,文件对象里面有指针指向不同设备操作方法,方法上面采用函数指针变相对底下方法汇总,上层用的时候不关心设备驱动层方法如何实现,这样看到了一切皆文件。在linux系统里把structfile这一层称为VFS,叫作虚拟文件系统:
比如网卡想被打开,这个设备要提供自己的读写方法(驱动中规定好的),然后创建对应structfile文件对象,然后创建操作的方法指针集,然后文件对象指向方法集,方法集指向方法,这样就把网卡设备纳入到了文件:
仔细来看,这两层其实是多态,所有struct file是基类,下面是派生类,上层指针指向哪个对象就访问哪个对象的方法。
2.缓冲区
下面先写一段代码:
这里说一下fwrite和fread差不多:
第二个参数是要写入块的大小,第三个参数是块的个数,这个返回值返回的是nmemb的个数:比如要写4个字节,写10个4字节,真正写了40个字节进去,这里size_t返回的是10;比如要写4个字节,写10个4字节,写了5个4字节,它的返回值就是5。向显示器写入有write接口:
它可向指定文件流中写入缓冲区,最后是缓冲区大小,返回值是实际写入的字节的个数。现在代码中:
上面三个是c语言提供的,最后一个是操作系统提供的,上三个其实最后都调用了write接口。下面运行一下:
一切符合预期。下面再做一下重定向:
一切也都符合预期。下面改一下再次运行:
再次重定向一下(重定向会对文件做清理):
此时发现消息变多了。以上做了这样一些现象:
首先看个问题,带fork后为啥重定向打印出来是7行呢?目前还不知道,但发现c接口打了两次,系统调用打了一次。第三次结尾也有fork,但显示器上都各打了一次,重定向文件后每个c式接口的输出字符串多打了一次,目前感觉到一定和fork有关系。
下面谈一下缓冲区,把代码改一下:
make后看到此时结果可以打印。下面把代码/n去掉再运行一下:
看到程序输出的结果没有了,重定向cat后也没有结果。再改一下代码并运行:
看到可以打出来。同样的待遇,用c语言的接口打印字符串,带/n的就直接刷出来了,不带/n的close后没有结果;系统调用接口时发现,带不带/n最后消息都能出来。凭什么调系统调用接口先调接口再关闭文件描述符就打出来了,凭什么打c语言的时候这上面都没有?我们说过我们用的printf/fprintf/fwrite等都是c式的接口,底层一定调用了write接口,截然这样打字符串时一定要交给对应的write,那应该和write效果一样呀,但事实并没有。曾经进度条那里不带\n看到即使把代码往显示器上打,但消息不会立刻刷新,其实这回消息已经被写入了,只不过写入到了缓冲区。今天调用printf、fprintf等时照样没带\n照样往显示器上打印,走到close前已把数据写入到了系统当中了(缓冲区),只不过这个缓冲区一定不在OS内部,不是系统级别的缓冲区。为啥这样说呢:
这有操作系统,里面有写的文件structfile,文件有自己对应的缓冲区,外面有磁盘、显示器等。进程pcb通过文件描述符找到文件,把数据写到自己文件缓冲区中,然后文件缓冲区的内容刷到磁盘里(文件一定要提供OS级别的缓冲区)。当调用printf、fprintf、fwrite这样的接口时,一定不是把数据拷贝到文件的内核缓冲区中,若拷贝进去了,后面调close时能找到对应文件,然后把文件缓冲区数据刷到磁盘里,最后是可以看到结果的,但事实上并没有。为啥write能看到呢?因为写入的字符串通过write这样的系统调用接口直接写到了系统缓冲区里,后面调close关这个文件时就把缓冲区的内容刷新出来了,这就是系统接口能看到的原因。但c接口底层调了write把参数给write,并且把文件拷到内核缓冲区了,数据注定会被刷新出来,可事实没有。因为c/c++里的缓冲区不在系统内部,这里说的缓冲区是在语言层的(c语言它会给我们提供一个缓冲区),这个缓冲区是用户级缓冲区,调的c式的这批接口并不是把数据直接给了write,而是把这些数据写到了c语言的缓冲区里,当我们在合适的时候如遇到强制刷新、\n才会去调write接口把这个缓冲区的内容写到系统缓冲区再刷新:
所以当我们直接fprintf等时,我们这个消息是在上层的c语言缓冲区,调close后把1号文件的描述符关了,进程退出时想调write刷新但已经关了所以刷不出来了。再回头看看:
第一次都打印了,第二次只有write打印。第一次为啥都打印出来了?像显示器的文件的刷新方案是行刷新,所以printf执行完会立即遇到\n时候将数据进行刷新,这里刷新的本质,目前就是将数据通过1+write写入到内核中。基于上述再继续,以前说exit和_exit是有区别的,以前说_exit更强势不会让系统数据刷新,exit会帮我们直接把数据刷新出来。现在就知道,exit是C语言的接口,它退的时候能看到语言提供的缓冲区,所以能把数据刷出来。_exit是系统调用,它不知道上面有语言的缓冲区,退的时候直接关文件描述符和释放进程,没有刷新的机会。目前我们认为,只要把数据通过系统调用接口写到了系统内部,OS会帮我们把数据刷到硬件。基于这个来谈谈语言这一层,c语言在语言层给我们封装了缓冲区,我们调printf、fprintf等接口可对缓冲区进行写入,写完后再调用合适的write接口把数据写到OS(如以前调fflush就是调write把数据写OS内部)。
说过上述再来引入下面的问题:1.缓冲区刷新问题:语言层把数据写到缓冲区里,就可以进行刷新了,可该怎么刷新呢?这里说的刷新策略是应用层的刷新策略,OS也要把自己的缓冲区数据刷到磁盘等,但我们不管,这是OS内部自己做的。我们只需要管好上面的,我们要清楚离用户近的语言层的刷新策略,这样才理解加和不加\n等为啥会出现不同的表现。所谓缓冲区刷新策略问题一般就三种:a.无缓冲。b.行缓冲。c.全缓冲。所谓的无缓冲,说白了是一种立即写入模式,就是直接刷新,相当于对应的printf接口把数据写到缓冲区里,别管什么刷新策略,写完后直接调write接口写到内核里。所谓的行缓冲,首先调c接口把数据写到缓冲区,c库就看看有没有\n,若缓冲区遇到了第一个\n再全刷新出来。也就是它的意思是缓冲区不刷新,直到碰到\n。所谓的全缓冲,不管写入什么都不管,直到把缓冲区写满,才调一次write把数据刷新到内核里,也就是缓冲区满了才刷新。下面整体做一个说明:
我们语言层首先调fprintf/fwrite等,然后将内容写到c缓冲区里,再根据一定策略调write,通过write写OS中。我们凭什么调write?取决于缓冲区的刷新策略。2.周边一些补充问题:我们以前用过一个接口叫fflush,它底层一定封装了write。下面问题来了,我们的刷新策略,默认向显示器打印时是行刷新的。因为显示器是给人看的,人的阅读习惯默认是以行为单位的,显示器要立即看到。我们一般向文件写入时采用的刷新策略是全缓冲,因为文件内容不需要用户立马来查看,所以为了提高效率就把对应的缓冲区写满,然后调write向文件写入。一般c语言上缓冲区刷新还有一种时机是当进程退出的时候,也会刷新。那为什么要有这个缓冲区,c为啥提供呢?这要分两方面谈:1.效率层面的价值。2.语言设计上。a.解决效率问题——用户的效率问题(比如快递的例子,快递站相当于用户缓冲区)。初学c语言时我们把printf这样的接口叫格式化输入输出接口,我们向显示器打印的是字符1、2、3这样,但我们调printf %d后面写的是整数变量,所以进行格式化输入输出,调系统调用前先把数据格式化形成字符串到一个区域里,如写入时有这样的转化:
转完后传给write。因此缓冲区的第二个意义是b.配合格式化。我们一般把c缓冲区叫文件流,因为我们调printf/fprintf这样的接口,不断向缓冲区里写入内容,然后把缓冲区的内容通过系统调用刷新到设备中。有数据不断向缓冲区写和拿出,缓冲区存在有点像河道有进有出,所以有了流的概念,称为文件流。
下面说第三个问题,这个缓冲区在哪里:
当我们在fflush一个流的时候,它里面只传了一个FILE*的接口,发现c语言中,文件操作绕不开FILE。我们说过FILE是一个struct结构体,它里面要封装对应的fd,它里面还有对应打开文件的缓冲区字段和维护信息,可理解为FILE结构体里包含了对应的缓冲区。比如当我们调fprintf(stdout, “hello world\n”)时,stdout是个FILE*的,它里面包含文件描述符,内部也要维护缓冲区,当我们调fprintf时是把"hello world"拷贝到自己的缓冲区,再根据\n和满写规律设定看是否把缓冲区数据刷出去调write方法。所以我如果在c语言中打开了10个文件,那有几个语言级别的缓冲区?10个,每个文件都有自己的缓冲区,也有10个文件描述符,每个文件配一个自己的语言层缓冲区,每个文件都把自己语言层缓冲区通过它自己的文件描述符刷新到我们对应的磁盘上。那这个FILE对象属于用户还是属于操作系统呢?FILE对象属于用户,因为语言都属于用户层,所以缓冲区也是属于用户的。那我们学习fopen时为啥返回的是FILE*呢:
fopen是c标准库(libc.so)给我们提供的接口,fopen打开文件底层调open在内核层面帮助我们建立内核级别的文件对象并且拿到文件描述符。同时在语言层给我们malloc(FILE),所以返回的是FILE*,一旦malloc出FILE,这个FILE里封装了对应的文件描述符和继续malloc出语言级的缓冲区。继续来看:
make运行时打印4条消息,重定向先清空文件打印的就是7条,为什么呢:
我们整个打印过程是,如我们调了fprintf,它里面对应的stdout输出字符串,然后在缓冲区把数据写进去了,然后调write接口把数据刷新出去到OS。OS里有对应的进程pcb,文件描述符表,文件对象,然后通过文件描述符表把数据写到文件级的缓冲区。其中我们打印时默认带了\n,所以是行刷新,见\n数据就调write写到内核进而刷新到磁盘。一旦我们重定向了,本来向显示器打印变成了向文件打印,我们的缓冲策略变成了缓冲,这样遇到\n不在刷新,而是写缓冲区被写满才刷新。下面举个例子:
每次sleep(2),因为缓冲区没满,即使sleep了代码也没有写到文件。write后sleep(5),write后把数据写到了磁盘,过5秒进程退出做强制刷新上面消息才能被刷出:
说明6秒c接口以及跑完了,只不过数据没刷新到系统里,write接口直接写OS所以能刷出来,当进程退出时全刷出来。最后带上fork,进程退出前执行fork,一旦执行fork OS要创建子进程,对应父子进程的代码是共享的,数据会以写时拷贝方式被各自私有一份。我们把数据从缓冲区刷新到系统中的时候,这个缓冲区是用户级的,也就属于进程的一部分(相当于malloc出的堆空间)。fork后父进程想刷新,本质也是对缓冲区的清空,清空的本质就是写入。OS在对这段缓冲区操作时发生写时拷贝,父子进程对这段缓冲区各自私有一份,所以发现最后被刷新了2份。那为什么同样有fork不重定向时打1份?因为没重定向时刷新方案是行缓冲,然后调c接口时消息被刷到了显示器上,fork后没什么刷新动作,所以只打了一份。
3.模拟实现
下面来模拟实现一下,来更好的能理解上述理论:
(为了不和库冲突,模拟的前面带-)今天模拟实现一下这样一些接口:
现在依次实现提供的一个个方法:
_fopen要开打对应的文件,它里面要封装open的。open的第一个参数是filename,第二个是方式(这里flsg只实现w,a,r),然后分别比较是w说明它想写,文件不存在就craet,默认清空,a和w类似:
不同的打开方式用不同的调用:
-1说明文件根本没有打开,当语句走到-1判断后面说明文件打开成功了。此时需要创建出对应的文件对象让别人来用:
走到下面说明malloc也成功了,设置好返回一个_FILE*:
上面就是fopen大概做的事情。下面看_fclose,关掉对应的文件描述符,然后释放:
至此有了文件打开和关闭了。再看_fwrite:
下面来测试一下:
经过这样的封装我们就不用关心系统了,若今天我们是在linux上,c语言中把linux的接口用这样方式封装一遍;OS不一样系统访问文件的接口肯定不一样,c语言中在windows下访问文件的接口封装一下;mac上也这样实现一份。然后把三分代码以条件编译形式都放到c语言里,然后分别裁为适合三个系统的库,然后三个系统上分别保留上层适合自己下层的接口,从此一个人在不同系统中能用同样的c接口,这就叫c语言具有跨平台性。
下面把缓冲区添加进去(简易版):
有对应的输入和输出缓冲区,这主要用一下输出缓冲区。我们以前说过printf是有缓冲区的,那scanf输入有没有缓冲区?有的,比如键盘输入123,其实我们输的是1,2,3字符,系统调用read读的是字符串‘123’字符串。要读字符串把它先保存到了接收缓冲区inbuffer,scanf后接收时用的整数变量,然后把字符串转整数拷贝到对应的变量。系统调用上没有所谓的类型:
所有东西再它看来都是字符串,只管把buf读或写,格式化是语言的事情。如printf打一个整数,首先做的是把整数转为字符串拷贝到outbuf里,最后经过write接口写出去。文件有最终对应的刷新方式,这里人为的写flag,还有pos,用来衡量缓冲区中的有效字符:
再补充一下.c:
这样fwrite要把char*s吓到缓冲区。下面再来测试一下:
看到按行刷新的方式不断地向我们文件内容写入。再改为全刷新方式测试一下:
进程退出时文件里照样什么都没有,因为我们代码中少了进程退出时代码强制刷新的逻辑,所以关闭文件前检查一下缓冲区中有没有数据,若有就刷出去:
这样就看到进程退出时文件刷新了。那缓冲区的意义是什么?调用系统调用把数据拷给OS是有时间成本的,要传10次就调系统调用10次效率太低。若此时攒一大批接口数据放buffer里,最后统一再刷新,此时1次和OS交互就可刷新很多数据,这样可让我们用c语言接口变的更快,调完c接口数据放缓冲区,函数返回。
4.完整代码
//main.c #include "Mystdio.h" #define myfile "test.txt" int main() { _FILE *fp = _fopen(myfile, "a"); if(fp == NULL) return 1; const char *msg = "hello world\n"; int cnt = 10; while (cnt){ _fwrite(fp, msg, strlen(msg)); sleep(1); cnt--; } _fflush(fp); _fclose(fp); return 0; }//Mystdio.h //#pragma once #ifndef __MYSTDIO_H__ #define __MYSTDIO_H__ #include <string.h> #include <stdlib.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> #include <assert.h> #define SIZE 1024 #define FLUSH_NOW 1 #define FLUSH_LINE 2 #define FLUSH_ALL 4 typedef struct IO_FILE{ int fileno; int flag; //char inbuffer[SIZE]; //int in_pos; char outbuffer[SIZE]; //用一下这个 int out_pos; }_FILE; _FILE* _fopen(const char*filename, const char *flag); int _fwrite(_FILE *fp, const char *s, int len); void _fclose(_FILE *fp); void _fflush(_FILE *fp); #endif//Mystdio.c #include "Mystdio.h" #define FILE_MODE 0666 // "w" "a" "r" _FILE* _fopen(const char*filename, const char *flag) { assert(filename); assert(flag); int f = 0; int fd = -1; if(strcmp(flag, "w") == 0){ f = (O_CREAT|O_WRONLY|O_TRUNC); fd = open(filename, f, FILE_MODE); } else if (strcmp(flag, "a") == 0){ f = (O_CREAT|O_WRONLY|O_APPEND); fd = open(filename, f, FILE_MODE); } else if (strcmp(flag, "r") == 0){ f = O_RDONLY; fd = open(filename, f); } else return NULL; if (fd == -1) return NULL; _FILE *fp = (_FILE*)malloc(sizeof(_FILE)); if (fp == NULL) return NULL; fp->fileno = fd; //fp->flag = FLUSH_LINE; fp->flag = FLUSH_ALL; fp->out_pos = 0; return fp; } int _fwrite(_FILE *fp, const char *s, int len) { //"abcd\n" memcpy(&fp->outbuffer[fp->out_pos], s, len); fp->out_pos += len; if(fp->flag&FLUSH_NOW) { write(fp->fileno, fp->outbuffer, fp->out_pos); fp->out_pos = 0; } else if(fp->flag&FLUSH_LINE) { if(fp->outbuffer[fp->out_pos-1] == '\n'){ write(fp->fileno, fp->outbuffer, fp->out_pos); fp->out_pos = 0; } } else if(fp->flag&FLUSH_ALL) { if(fp->out_pos == SIZE){ write(fp->fileno, fp->outbuffer, fp->out_pos); fp->out_pos = 0; } } return len; } void _fflush(_FILE* fp) { if(fp->out_pos > 0){ write(fp->fileno, fp->outbuffer, fp->out_pos); fp->out_pos = 0; } } void _fclose(_FILE *fp) { if (fp == NULL) return; close(fp->fileno); free(fp); }