一.系统调用和库函数调用的异同
(1)系统调用提供的函数如:open, close, write, read等需要包含头文件
C库函数提供的文件操作函数如:fopen, fclose, fread,fwrite等需包含头文件stdio.h
(2)库函数调用是与系统无关的,因此可移植性好。
(3)由于库函数调用是基于C库的,因此也就不可能用于内核空间的驱动程序中对设备的操作。
(4)系统调用通常用于底层文件访问,是与操作系统有关的,因此一般没有跨操作系统的可移植性。eg:驱动程序中对设备文件的直接访问。
(5)在所有的ANSI C编译器版本中,C库函数是相同的;各个操作系统的系统调用是不同的。
(6)使用库函数也有系统调用的开销,为什么不直接使用系统调用呢?这是因为读写文件通常是大量的数据(这种大量是相对于底层驱动的系统调用的数据操作单位而言的),这时,使用库函数就可以大大减少系统调用的次数(这里需要解释一下:因为库函数每次对文件读写数据都是将数据读写入缓冲区,直到缓冲区满或者关闭文件时才将数据写入文件或将数据从文件中读出,这样只用了一次系统调用;而系统调用是没有缓冲区的,所以对文件进行多次操作时就要频繁的使用系统调用所以比较费时)
二. C标准I/O库函数与Unbuffered I/O函数
现在看看C标准I/O库函数是如何用系统调用实现的。
fopen(3)
调用open(2)打开指定的文件,返回一个文件描述符(就是一个int类型的编号),分配一个FILE结构体,其中包含该文件的描述符、I/O缓冲区和当前读写位置等信息,返回这
个FILE结构体的地址。
fgetc(3)
通过传入的FILE *参数找到该文件的描述符、I/O缓冲区和当前读写位置,判断能否从I/O缓冲区中读到下一个字符,如果能读到就直接返回该字符,否则调用read(2),把文件描述符传进去,让内核读取该文件的数据到I/O缓冲区,然后返回下一个字符。注意,对于C标准I/O库来说,打开的文件由FILE *指针标识,而对于内核来说,打开的文件由文件描述符标识,文件描述符从open系统调用获得,在使用read、write、close系统调用时都需要传文件描述符。
fputc(3)
判断该文件的I/O缓冲区是否有空间再存放一个字符,如果有空间则直接保存在I/O缓冲区中并返回,如果I/O缓冲区已满就调用write(2),让内核把I/O缓冲区的内容写回文件。
fclose(3)
如果I/O缓冲区中还有数据没写回文件,就调用write(2)写回文件,然后调用close(2)关闭文件,释放FILE结构体和I/O缓冲区。
open、read、write、close等系统函数称为无缓冲I/O(Unbuffered I/O)函数,因为它们位于C标准库的I/O缓冲区的底层[36]。用户程序在读写文件时既可以调用C标准I/O库函数,也可以直接调用底层的Unbuffered I/O函数。
用Unbuffered I/O函数每次读写都要进内核,调一个系统调用比调一个用户空间的函数要慢得多,所以在用户空间开辟I/O缓冲区还是必要的,用C标准I/O库函数就比较方便,省去了自己管理I/O缓冲区的麻烦。
用C标准I/O库函数要时刻注意I/O缓冲区和实际文件有可能不一致,在必要时需调
用fflush(3)。
我们知道UNIX的传统是Everythingis a file,I/O函数不仅用于读写常规文件,也用于读写设备,比如终端或网络设备。在读写设备时通常是不希望有缓冲的,例如向代表网络设备的文件写数据就是希望数据通过网络设备发送出去,而不希望只写到缓冲区里就算完事儿了,当网络设备接收到数据时应用程序也希望第一时间被通知到,所以网络编程通常直接调
用UnbufferedI/O函数。
事实上Unbuffered I/O这个名词是有些误导的,虽然write系统调用位于C标准库I/O缓冲区的底层,但在write的底层也可以分配一个内核I/O缓冲区,所以write也不一定是直接写到文件的,也可能写到内核I/O缓冲区中,至于究竟写到了文件中还是内核缓冲区中对于进程来说是没有差别的,如果进程A和进程B打开同一文件,进程A写到内核I/O缓冲区中的数据从进程B也能读到,而C标准库的I/O缓冲区则不具有这一特性。
三.常用系统调用对文件的操作函数和实例
(1)open
头文件:#include
#include
#include
int open(const char *pathname,int flags)
int open(const char *pathname,intflags,mode_t mode)
pathname 参数是要打开或创建的文件名,和fopen 一样,pathname 既可以是相对路径也可以是绝对路径。flags 参数有一系列常数值可供选择,可以同时选择多个常数用按位或运算符连接起来,所以这些常数的宏定义都以O_开头,表示or。
O_RDONLY 只读打开
O_WRONLY 只写打开
O_RDWR 可读可写打开
O_APPEND 追加
O_CREAT 若文件不存在则创建
O_TRUNC 如果文件已存在,并且以只写或可读可写方式打开,则将其长度截断为0字节
mode指定文件权限可以用八进制数表示例如:0644表示-rw-r--r--
文件权限又mode和当前进程的umask掩码共同决定
O_NONBLOCK 对于设备文件,以O_NONBLOCK方式打开可以做非阻塞I/O(Nonblock I/O)
注意:
以w或w+方式fopen一个文件时,如果文件已存在就截断为0字节,而open一个文件时必须明确指定O_TRUNC才会截断文件,否则直接在原来的数据上改写。
(2)close
#include
intclose(int fd)
成功返回0出错返回-1并设置error
参数fd是要关闭的文件描述符。需要说明的是,当一个进程终止时,内核对该进程所有尚未关闭的文件描述符调用close关闭,所以即使用户程序不调用close,在终止时内核也会自动关闭它打开的所有文件。但是对于一个长年累月运行的程序(比如网络服务器),打开的文件描述符一定要记得关闭,否则随着打开的文件越来越多,会占用大量文件描述符和系统资源。
由open返回的文件描述符一定是该进程尚未使用的最小描述符。由于程序启动时自动打开文件描述符0、1、2,因此第一次调用open打开文件通常会返回描述符3,再调用open就会返回4。可以利用这一点在标准输入、标准输出或标准错误输出上打开一个新文件,实现重定向的功能。例如,首先调用close关闭文件描述符1,然后调用open打开一个常规文件,则一定会返回文件描述符1,这时候标准输出就不再是终端,而是一个常规文件了,再调用printf就不会打印到屏幕上,而是写到这个文件中了。
eg1重定向输出
/*************************************************************************
> File Name: cdx.c
> Author: 朱志祥
> Mail: [email protected]
> Created Time: Wed 16 Jul 2014 05:11:59 PM PDT
************************************************************************/
#include
#include
#include
#include
#include
int main(void)
{
intfd;
close(1); /*关闭标准输出文件描述符*/
fd=open("printf_file",O_WRONLY|O_CREAT|O_TRUNC,0666);
printf("helloworld!!!!!!!\nhello world!!!!!\n");
//close(fd); 这里不能关闭fd,不然就在文件中没法看到输入的数据
return0;
}
eg2拷贝文件
#include
#include
#include
#include
#include
#include
#define LEN 128
int copy_file(int src,int dest)
{
intr_ret,w_ret;
charbuf[LEN];
char*tmp;
memset(buf,0,LEN); /*这里可以不需要清空*/
while((r_ret=read(src,buf,LEN))>0)
{
tmp=buf;
while(r_ret)
{
w_ret=write(dest,tmp,r_ret); /*写入数据*/
r_ret=r_ret-w_ret;
tmp+=w_ret;
}
memset(buf,0,LEN); /*这里不需要清空*/
}
return0;
}
int main(int argc,char *argv[])
{
intsrc_fd,obj_fd;
src_fd=open(argv[1],O_RDONLY);
obj_fd=open(argv[2],O_WRONLY|O_CREAT|O_TRUNC,0644);
if(src_fd<0|obj_fd<0)
{
printf("fileopen error\n");
return1;
}
copy_file(src_fd,obj_fd);
close(src_fd);
close(obj_fd);
return0;
}
(3).read/write
#include
ssize_tread(int fd,void *buf,size_t count);
执行成功返回实际读取的字节数,出错返回-1并设置errno,如果在调用read之前已经到达文件末尾,则这次read返回0.
#include
ssize_twrite(int fd,void *buf,size_t count);
执行成功返回实际写入的字节数,出错返回-1并设置errno
写常规文件时,write的返回值通常等于请求写的字节数count,而向终端设备或网络写则不一定。
eg1.向终端输出hello world!
/*************************************************************************
>File Name: c1.c
> Author: 朱志祥
> Mail: [email protected]
> Created Time: Thu 17 Jul 2014 05:49:13 PM PDT
************************************************************************/
#include
#define len 14
int main(void)
{
char msg[len]="hello world!\n";
write(1,msg,len);
_exit(0);
}
eg2.阻塞读终端
/*************************************************************************
> File Name: c2.c
> Author: 朱志祥
> Mail: [email protected]
> Created Time: Thu 17 Jul 2014 05:59:48 PM PDT
************************************************************************/
#include
#include
int main(void)
{
charbuf[10];
intn;
n=read(STDIN_FILENO,buf,10);
if(n<0)
{
perror("readSTDIN_FILENO");
exit(1);
}
write(STDOUT_FILENO,buf,10);
return0;
}
以下是一个非阻塞I/O的例子。目前我们学过的可能引起阻塞的设备只有终端,所以我们用终端来做这个实验
eg3.非阻塞读终端
/*************************************************************************
> File Name: c3.c
> Author: 朱志祥
> Mail: [email protected]
> Created Time: Thu 17 Jul 2014 06:23:25 PM PDT
************************************************************************/
#include
#include
#include
#include
#include
#define MSG_TRY "try again\n"
int main(void)
{
charbuf[10];
intfd,n;
fd=open("/dev/tty",O_RDONLY|O_NONBLOCK);
if(fd<0)
{
perror("open/dev/tty");
exit(1);
}
tryagain:
n=read(fd,buf,10);
if(n<0)
{
if(errno==EAGAIN)
{
sleep(1);
write(STDOUT_FILENO,MSG_TRY,sizeof(MSG_TRY));
gototryagain;
}
perror("read/dev/tty");
exit(1);
}
write(STDOUT_FILENO,buf,n);
close(fd);
return0;
}
eg4.非阻塞读终端和等待超时
/*************************************************************************
> File Name: c4.c
> Author: 朱志祥
> Mail: [email protected]
> Created Time: Sat 19 Jul 2014 09:28:55 PM PDT
************************************************************************/
#include
#include
#include
#include
#include
#define MSG_TRY "try again\n"
#define MSG_TIMEOUT "timeout\n"
int main(void)
{
charbuf[10];
intfd,n,i;
fd=open("/dev/tty",O_RDONLY|O_NONBLOCK);
if(fd<0)
{
perror("open/dev/tty");
exit(1);
}
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_TIMEOUT,strlen(MSG_TRY));
}
if(5==i)
{
write(STDOUT_FILENO,MSG_TIMEOUT,strlen(MSG_TIMEOUT));
}
else
{
write(STDOUT_FILENO,buf,n);
}
close(fd);
return0;
}
(4)lseek
每个打开的文件都记录着当前读写位置,打开文件时读写位置是0,表示文件开头,通常读写多少个字节就会将读写位置往后移多少个字节。但是有一个例外,如果以O_APPEND 方式打开,每次写操作都会在文件末尾追加数据,然后将读写位置移到新的文件末尾。lseek 和标准I/O库的fseek 函数类似,可以移动当前读写位置(或者叫偏移量)。
#include
#include
off_t lseek(int fd,off_t offset,int whence)
参数offset和whence的含义和fseek 函数完全相同。只不过第一个参数换成了文件描述符。和fseek 一样,偏移量允许超过文件末尾,这种情况下对该文件的下一次写操作将延长文件,中间空洞的部分读出来都是0。
若lseek 成功执行,则返回新的偏移量,因此可用以下方法确定一个打开文件的当前偏移量:
off_tcurrpos=lseek(fd,0,SEEK_CUR);
这种方法也可用来确定文件或设备是否可以设置偏移量,常规文件都可以设置偏移量,而设备一般是不可以设置偏移量的。如果设备不支持lseek ,则lseek 返回-1,并将errno 设置为ESPIPE。注意fseek 和lseek 在返回值上有细微的差别,fseek 成功时返回0失败时返回-1,要返回当前偏移量需调用ftell,而lseek 成功时返回当前偏移量失败时返回-1。