重要术语
什么是文件描述符?
文件描述符是整数,用于唯一标识进程的打开文件。
文件描述符表:文件描述符表是整数数组索引的集合,这些整数数组的索引是文件描述符,整数数组的元素是指向文件表条目的指针。操作系统为每个进程提供了一个唯一的文件描述符表。
文件表条目:文件表条目是打开文件的内存代理结构,当进程请求打开文件时创建,同时这些条目记录文件位置。
标准文件描述符:当进程启动时,该进程文件描述符表的fd 0、1、2将自动打开,默认情况下这3个fd中的每一个都引用名为/dev/tty的文件的文件表条目。
/dev/tty:终端的内存代理
终端:组合键盘/视频屏幕
从stdin读取=>从fd 0读取:每当我们从键盘写入字符,都会通过fd0从stdin读取并保存到名为/dev/tty的文件中。
写入stdout =>写入fd 1:我们看到视频屏幕的任何输出,都来自/dev/tty文件,并通过fd1写入屏幕的标准输出。
写入stderr =>写入fd 2:我们看到的视频屏幕的任何错误,它也是从/dev/tty文件通过fd2写入stderr。
I/O系统调用
基本上共有5种类型的I/O系统调用:
1. Create
用于创建一个新的空文件。
C语言语法:
int creat(char *filename, mode_t mode)
参数:
- filename:要创建的文件的名称。
- mode:指定新文件的权限。
返回值:
- 返回第一个未使用的文件描述符(通常在进程中首次使用creat时为3,因为保留了fd 0、1、2)
- 错误时返回-1
在OS中的运作方式
- 在磁盘上创建新的空文件
- 创建文件表条目
- 将第一个未使用的文件描述符设置为指向文件表条目
- 返回使用的文件描述符,失败时返回-1
2. open
用于打开文件进行读取、写入或两者兼有。
C语言语法:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int open (const char* Path, int flags [, int mode ]);
参数:
- Path:要使用的文件的路径
- 所需文件不在当前目录时,请使用以“/”开头的绝对路径。
- 所需文件在当前目录时,请使用相对路径
- flags:O_RDONLY:只读,O_WRONLY:只写,O_RDWR:读写,O_CREAT:创建不存在的文件,O_EXCL:阻止创建已存在的文件
在OS中的运作方式
- 查找磁盘上的现有文件
- 创建文件表条目
- 将第一个未使用的文件描述符设置为指向文件表条目
- 返回使用的文件描述符,失败时返回-1
// C program to illustrate
// open system call
#include <stdio.h>
#include <fcntl.h>
#include <errno.h>
extern int errno;
int main()
{
// if file does not have in directory
// then file foo.txt is created.
int fd = open("foo.txt", O_RDONLY | O_CREAT);
printf("fd = %d\n", fd);
if (fd ==-1)
{
// print which type of error have in a code
printf("Error Number % d\n", errno);
// print program detail "Success or failure"
perror("Program");
}
return 0;
}
输出:
fd = 3
3. close
告诉操作系统你已经使用完成了一个文件描述符并关闭fd所指向的文件。
C语言语法:
#include <fcntl.h>
int close(int fd);
参数
- fd:文件描述符
返回值
- 0 成功时返回
- -1 错误时返回
在OS中的运作方式
- 销毁文件表条目,该条目被文件描述符表的元素fd引用
(只要没有其他进程指向它!) - 将文件描述符表的元素fd设置为NULL
// C program to illustrate close system Call
#include <stdio.h>
#include <fcntl.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{
int fd1 = open("foo.txt", O_RDONLY);
if (fd1 < 0)
{
perror("c1");
exit(1);
}
printf("opened the fd = % d\n", fd1);
// Using close system Call
if (close(fd1) < 0)
{
perror("c1");
exit(1);
}
printf("closed the fd.\n");
}
输出:
opened the fd = 3
closed the fd.
// C program to illustrate close system Call
#include <stdio.h>
#include <fcntl.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{
// assume that foo.txt is already created
int fd1 = open("foo.txt", O_RDONLY, 0);
close(fd1);
// assume that baz.tzt is already created
int fd2 = open("baz.txt", O_RDONLY, 0);
printf("fd2 = % d\n", fd2);
exit(0);
}
输出:
fd2 = 3
在这里,在这段代码中第一个open()返回3。因为当创建主进程时,fd 0、1、2已经被stdin、stdout和stderr使用,所以第一个未使用的文件描述符是文件描述符表中的3。在之后的close()系统调用中,文件描述符3空闲了,所以将文件描述符3设置为null。所以当我们调用第二个open()时,第一个未使用的fd也是3。所以,这个程序的输出是3。
4. read
从文件描述符fd指示的文件中,read()函数读取cnt字节,并将cnt字节写入buf指示的内存区域。成功执行的read()函数会更新文件的访问时间。
C语言语法
size_t read (int fd, void* buf, size_t cnt);
参数
- fd:文件描述符
- buf:从中读取数据的buffer
- cnt:buffer长度
返回值:实际读取了多少字节
- 成功时返回读取的字节数
- 到达文件结尾时返回0
- 错误时返回-1
- 信号中断时返回-1
要点
- 为了避免溢出,buf需要指向长度不小于指定大小的有效内存地址。
- fd应该是从open()返回的有效文件描述符,以执行读取操作,因为如果fd为NULL,则读取将产生错误。
- cnt是请求读取的字节数,而返回值是实际读取的字节数。另外,有时read系统调用应该读取比cnt更少的字节。
// C program to illustrate
// read system Call
#include<stdio.h>
#include <fcntl.h>
int main()
{
int fd, sz;
char *c = (char *) calloc(100, sizeof(char));
fd = open("foo.txt", O_RDONLY);
if (fd < 0) { perror("r1"); exit(1); }
sz = read(fd, c, 10);
printf("called read(% d, c, 10). returned that"
" %d bytes were read.\n", fd, sz);
c[sz] = '\0';
printf("Those bytes are as follows: % s\n", c);
}
输出:
called read(3, c, 10). returned that 10 bytes were read.
Those bytes are as follows: 0 0 0 foo.
假设foobar.txt文件由6个ASCII字符“foobar”组成。那么下面程序的输出是什么?
// C program to illustrate
// read system Call
#include<stdio.h>
#include<unistd.h>
#include<fcntl.h>
#include<stdlib.h>
int main()
{
char c;
int fd1 = open("sample.txt", O_RDONLY, 0);
int fd2 = open("sample.txt", O_RDONLY, 0);
read(fd1, &c, 1);
read(fd2, &c, 1);
printf("c = %c\n", c);
exit(0);
}
输出:
c = f
描述符fd1和fd2都有自己的打开文件表条目,因此每个描述符都有自己的foobar.txt文件位置。因此,从fd2读取foobar.txt文件的第一个字节,输出是c=f,而不是c=o。
5. write
将cnt个字节从buf写入与fd关联的文件或套接字。cnt不应大于INT_MAX(在limits.h头文件中定义)。如果cnt为零,write()只返回0而不尝试任何其他操作。
#include <fcntl.h>
size_t write (int fd, void* buf, size_t cnt);
参数
- fd:文件描述符
- buf:写入数据的buffer
- cnt:buffer长度
返回值
- 返回成功写入的字节数
- 到达文件结尾时返回0
- 错误时返回-1
- 信号中断时返回-1
要点
- 需要打开文件进行写入操作
- buf至少需要与cnt一样长,因为如果buf的大小小于cnt,那么buf将导致溢出。
- cnt是请求写入的字节数,而返回值是实际写入的字节数。当fd要写入的字节数少于cnt时,就会发生这种情况。
- 如果write()被信号中断,则效果如下:
-如果write()尚未写入任何数据,则返回-1并将errno设置为EINTR。
-如果write()已成功写入某些数据,则返回中断前写入的字节数。
// C program to illustrate
// write system Call
#include<stdio.h>
#include <fcntl.h>
main()
{
int sz;
int fd = open("foo.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd < 0)
{
perror("r1");
exit(1);
}
sz = write(fd, "hello geeks\n", strlen("hello geeks\n"));
printf("called write(% d, \"hello geeks\\n\", %d)."
" It returned %d\n", fd, strlen("hello geeks\n"), sz);
close(fd);
}
输出:
called write(3, "hello geeks\n", 12). it returned 11
在这里,当你在文件里看到foo.txt文件在运行代码之后,你会得到一个“hello geeks”。如果foo.txt文件文件已经有一些内容,然后write系统调用覆盖内容,所有以前的内容被删除,只有“hello geeks”的内容将在文件中。
从程序中打印“hello world”,而不使用任何printf或cout函数。
// C program to illustrate
// I/O system Calls
#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<fcntl.h>
int main (void)
{
int fd[2];
char buf1[12] = "hello world";
char buf2[12];
// assume foobar.txt is already created
fd[0] = open("foobar.txt", O_RDWR);
fd[1] = open("foobar.txt", O_RDWR);
write(fd[0], buf1, strlen(buf1));
write(1, buf2, read(fd[1], buf2, 12));
close(fd[0]);
close(fd[1]);
return 0;
}
输出:
hello world
在这段代码中,首先将buf1数组的字符串“hello world”写入标准输入fd[0],然后将该字符串写入标准输入buf2数组。之后,将buf2数组写入标准输出并打印输出“helloworld”。
参考文档
[1]Kadam Patel.Input-output system calls in C | Create, Open, Close, Read, Write[EB/OL].https://www.geeksforgeeks.org/input-output-system-calls-c-create-open-close-read-write/,2020-08-23.