经过一个多月的学习,终于进入Linux系统函数(高编)的学习,所有I/O操作最终都是在内核中做的,以前我们用的C标准I/O库函数最终也是通过系统调用把I/O操作从用户空间传给内核,然后让内核去做I/O操作。
1)首先介绍了C标准I/O库函数和Unbuffered I/O函数
看了看C标准库函数是如何用系统调用的,以写文件为例,C标准I/O库函数(printf(3)、putchar(3)、fputs(3))与系统调用write(2)的关系如下图所示。
open、read、write、close等系统函数称为无缓冲I/O(Unbuffered I/O)函数,因为它们位于C标准库的I/O缓冲区的底层。用户程序在读写文件时既可以调用C标准I/O库函数,也可以直接调用底层的Unbuffered I/O函数。
两者的区别:(1)用Unbuffered I/O函数每次读写都要进内核,调一个系统调用比调一个用户空间的函数要慢很多,所以在用户空间开辟I/O缓冲区还是必要的,用C标准I/O库函数就比较方便,省去了自己管理I/O缓冲区的麻烦。用C标准I/O库函数要时刻注意I/O缓冲区和实际文件有可能不一致,在必要时需调用fflush(man 3)。(2)我们知道UNIX的传统是Everything is a file,I/O函数不仅用于读写常规文件,也用于读写设备,比如终端或网络设备。在读写设备时通常是不希望有缓冲的,例如向代表网络设备的文件写数据就是希望数据通过网络设备发送出去,而不希望只写到缓冲区里就算完事儿了,当网络设备接收到数据时应用程序也希望第一时间被通知到,所以网络编程通常直接调用Unbuffered I/O函数。
C标准库函数是C标准的一部分,而Unbuffered I/O函数是UNIX标准的一部分,在所有支持C语言的平台上应该都可以用C标准库函数(关于UNIX标准)。
用户程序不能直接访问内核中的文件描述符表,而只能使用文件描述符表的索引(即0、1、2、3这些数字),这些索引就称为文件描述符(File Descriptor),用int型变量保存。当调用open打开一个文件或创建一个新文件时,内核分配一个文件描述符并返回给用户程序,该文件描述符表项中的指针指向新打开的文件。当读写文件时,用户程序把文件描述符传给read或write,内核根据文件描述符找到相应的表项,再通过表项中的指针找到相应的文件。
我们知道,程序启动时会自动打开三个文件:标准输入、标准输出和标准错误输出。在C标准库中分别用FILE *指针stdin、stdout和stderr表示。这三个文件的描述符分别是0、1、2,保存在相应的FILE结构体中。
介绍了几个基本系统函数的应用,来这来记录之以防忘记。
open与close
open函数可以打开或创建一个文件。
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
返回值:成功返回新分配的文件描述符,出错返回-1并设置errno
pathname参数是要打开或创建的文件名,和fopen一样,pathname既可以是相对路径也可以是绝对路径。flags参数有一系列常数值可供选择,可以同时选择多个常数用按位或运算符连接起来,所以这些常数的宏定义都以O_开头,表示or。
以下三个函数必须指定一个:O_RDONLY 只读打开 O_WRONLY 只写打开 O_RDWR 可读可写打开
这些则为可选项:O_APPEND 表示追加。如果文件已有内容,这次打开文件所写的数据附加到文件的末尾而不覆盖原来的内容。
O_CREAT 若此文件不存在则创建它。使用此选项时需要提供第三个参数mode,表示该文件的访问权限。
O_EXCL 如果同时指定了O_CREAT,并且文件已存在,则出错返回。
O_TRUNC 如果文件已存在,并且以只写或可读可写方式打开,则将其长度截断(Truncate)为0字节。
O_NONBLOCK方式打开可以做非阻塞I/O(Nonblock I/O)。
第三个参数mode指定文件权限,用八进制表示。一般是0666;
close 函数表示关闭一个已打开的文件:
#include <unistd.h>i
nt close(int fd);
返回值:成功返回0,出错返回-1并设置errno
它的作用与C函数库中fclose的用法类似。
注: 由open返回的文件描述符一定是该进程尚未使用的最小描述符。由于程序启动时自动打开文件描述符0、1、2,因此第一次调用open打开文件通常会返回描述符3,再调用open就会返回4。
write函数:函数向打开的设备或文件中写数据。
#include <unistd.h>
int close(int fd);
返回值:成功返回0,出错返回-1并设置errno
它的作用与C函数库中fclose的用法类似
read函数:从打开的设备或文件中读取数据。
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
返回值:成功返回读取的字节数,出错返回-1并设置errno,如果在调read之前已到达文件末尾,则这次read返回0
参数count是请求读取的字节数,读上来的数据保存在缓冲区buf中,同时文件的当前读写位置向后移,这个与读的实际值有可能会有差别,一般常规文件不会出现阻塞,而向终端或者网络写则不一定。这儿有涉及到了阻塞与非阻塞的内容,一会通过一个代码可以掌握它的使用,如果在open一个设备时指定了O_NONBLOCK标志,read/write就不会阻塞,以下就是阻塞和非阻塞的程序(注释掉得部分为阻塞时的代码)
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <errno.h>
#define MSG_TRY "try again\n"
#define MSG_TIMEOUT "timeout\n"
void main()
{
char buf[10];
int fd,n,i;
fd=open("/dev/tty",O_RDONLY|O_NONBLOCK);
if(fd<0){
perror("open /dev/tty");
exit(1);
}
#if 0
tryagain:
n=read(fd,buf,10);
if(n<0){
if(errno==EAGAIN){
sleep(1);
write(STDOUT_FILENO,MSG_TRY,strlen(MSG_TRY));
goto tryagain;
}
perror("read /dev/tty");
exit(1);
}
write(STDOUT_FILENO,buf,n);
close(fd);
return ;
#endif
for(i=0;i<5;i++){
n=read(fd,buf,10);
if(n>=0)
break;
if(errno!=EAGAIN){
perror("read /dev/tty");
exit(1);
}
sleep(1);
write(STDOUT_FILENO,MSG_TRY,strlen(MSG_TRY));
}
if(i==5)
write(STDOUT_FILENO,MSG_TIMEOUT,strlen(MSG_TIMEOUT));
else
write(STDOUT_FILENO,buf,n);
close(fd);
return ;
}
下面这个代码用来处理实际写的字节数与想要写的字节数不统一的问题:
#include <stdio.h>
#include <unistd.h>
#include <strings.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#define FLAGS O_RDWR|O_CREAT|O_TRUNC
int cp(int rfd,int wfd)
{
int src,srv;
char buf[128];
char *p;
bzero(buf,128);
while((src=read(rfd,buf,128))>0){
p=buf;
while(src){
srv=write(wfd,p,src);
src=src-srv;
p=p+src;
}
bzero(buf,128);
}
return 0;
}
int main (int argc,char *argv[])
{
int rfd,wfd;
rfd=open(argv[1],O_RDONLY);
if(rfd<0){
perror("open rfd");
return rfd;
}
wfd=open(argv[2],FLAGS,0666);
if(wfd<0){
perror("open wfd");
return wfd;
}
cp(rfd,wfd);
close(wfd);
close(rfd);
return 0;
}