操作系统实践05—文件描述符和系统调用
文章目录
1. 概念
1.1 文件描述符
定义:
- 一个非负整数;
- 应用程序利用文件描述符来访问文件;
file descriptor
,简写为fd
。
打开现存文件或新建文件时,内核会返回一个文件描述符;打开现存文件或新建文件时,内核会返回一个文件描述符。
1.2 系统调用
打开文件
int open(char *path, int flags, mode_t mode);
- 内核会返回一个文件描述符
fd
用来表示该文件 - 读写时需要使用
fd
指定待读写的文件
读写文件
int read(int fd, void *buf, size_t size);
int write(int fd, void *buf, size_t size);
fd
是open返回的文件描述符,用于指定待读写的文件。
1.3 例子
// exe1.c
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
// 使用open系统调用需要包含以上三个头文件
#include <unistd.h>
// 使用read/write系统调用需要包含头文件unistd.h
int main()
{
int fd;
// O_RDONLY表示以只读方式打开
fd = open("/etc/hosts", O_RDONLY);
char buf[1024];
int count;
// 从文件中读取数据存放到buf中,read返回实际读取的字节个数
count = read(fd, buf, sizeof(buf));
// 设置buf中的文本以0结尾,并打印
buf[count] = 0;
puts(buf);
close(fd);
return 0;
}
编译运行。
$ cc -o exe1 exe1.c
$ ./exe1
127.0.0.1 localhost
2. 内核实现
2.1 file结构体
内核使用file结构体
表示一个被打开的文件。file结构体存了被打开文件的信息:
- 文件对应的索引节点
inode
; - 文件的当前访问位置;
- 文件的打开模式:只读、只写、可读可写。
2.2 文件描述符表
文件描述符表是一个数组:
- 数组的元素类型是指针,指针指向一个file结构体;
- 用于保存被打开的文件。
内核打开文件时:
- 分配一个file结构体表示被打开的文件;
- 将该file结构体指针保存在文件描述符表中。
打开文件的过程如下:
- 找到文件对应的
索引节点inode
; - 分配一个file结构体,file结构体的
inode
字段指向第1步的inode,file结构体的文件访问位置字段初始化为0; - 从文件描述符表中找一个空闲项,指向第2步的file结构体,返回该空闲项在数组中的的下标,即
fd
。
2.3 进程控制块
进程控制块是操作系统表示进程状态的数据结构。存放用于描述进程情况及控制进程运行所需的全部信息:
- 进程标识信息;
- 处理机状态;
- 进程调度信息;
- 打开文件列表,即文件描述符表,记录了该进程打开的文件。
2.4 私有的文件描述符表
文件描述符表对进程来说是私有的:
- 每个进程都有一个私有的文件描述符表;
- 操作系统有N个进程,则对应有N张文件描述符表。
两个进程打开不同的文件,文件描述符可能是相同的
- 进程A打开文件a.txt,open返回值是3;
- 进程B打开文件b.txt,open返回值也可能是3。
例子如下:
当前目录下存在两个文件a.c和b.c
// 程序a.c打开文件/etc/passwd
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
int main()
{
int fd;
fd = open("/etc/passwd", O_RDONLY);
printf("open(/etc/passwd) = %d\n", fd);
close(fd);
return 0;
}
// 程序b.c打开文件/etc/hosts
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
int main()
{
int fd;
fd = open("/etc/hosts", O_RDONLY);
printf("open(/etc/hosts) = %d\n", fd);
close(fd);
return 0;
}
编译运行:
$ ls
a.c b.c
$ cc -o a.exe a.c
$ ./a.exe
open(/etc/passwd) = 3
$ cc -o b.exe b.c
$ ./b.exe
open(/etc/hosts) = 3
尽管打开不同的文件,但是返回的文件描述符相同。
3. 标准输入和输出
3.1 简介
每个进程执行时,会自动打开三个标准文件:
-
标准输入文件,通常对应终端的键盘;
-
标准输出文件,通常对应终端的屏幕;
-
标准错误输出文件,通常对应终端的屏幕。
进程的文件描述符表前三项已经被打开了:
- 第0项,对应标准输入;
- 第1项,对应标准输出;
- 第2项,对应标准错误输出。
3.2 预定义的文件描述符
// exe2.c
#include <unistd.h>
int main()
{
char buf[80];
int count;
// read返回读取字节的实际大小
count = read(0, buf, sizeof(buf));
buf[count] = 0;
write(1, buf, count);
}
可以直接使用文件描述符0和1。从文件描述符0读取一行,再将读取的内容写到文件描述符1。编译运行程序。
$ cc -o exe2 exe2.c
$ ./exe2
hello
hello
3.3 新打开文件
验证新打开文件的文件描述符是否为3。
// exe3.c
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
int main()
{
int fd;
fd = open("/etc/hosts", O_RDONLY);
printf("open(/etc/hosts) = %d\n", fd);
return 0;
}
编译运行程序。
$ cc -o exe3 exe3.c
$ ./exe3
open(/etc/hosts) = 3
文件描述符表的前3项已经被占用了,新打开的文件描述符一定是3。
// exe4.c
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
int main()
{
int fd;
// 先关闭预定义的文件描述符2,再打开新文件
close(2);
fd = open("/etc/hosts", O_RDONLY);
printf("open(/etc/hosts) = %d\n", fd);
return 0;
}
文件描述符表的第2项为空闲,预期新打开的文件描述符是2。编译运行程序。
$ cc -o exe4 exe4.c
$ ./exe4
open(/etc/hosts) = 2
4. 描述符继承
4.1 fork系统调用
// 原型
#include <unistd.h>
pid_t fork(void);
创建一个子进程:
-
为子进程创建一个独立的地址空间;
-
为子进程创建一个独立的文件描述符表。
子进程复制父进程的如下属性:
-
代码段、数据段的内容;
-
文件描述符表;
-
子进程继承父进程中打开的文件描述符。
4.2 例子
函数dump
读取fd
指向的文件内容并打印。
// exe5.c
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
// 函数dump读取fd指向的文件内容并打印
void dump(int fd)
{
char buf[128];
int count;
count = read(fd, buf, sizeof(buf));
buf[count] = 0;
puts(buf);
}
int main()
{
pid_t pid;
int fd;
// 父进程打开文件/etc/passwd,返回fd
fd = open("/etc/passwd", O_RDONLY);
pid = fork();
if (pid == 0)
// 在子进程中使用dump显示文件内容
dump(fd);
return 0;
}
子进程继承父进程的文件描述符fd
,且可以使用。编译运行程序。
$ cc -o exe5 exe5.c
$ ./exe5
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:s
子进程正确的打印了文件/etc/passwd内容,说明父进程打开的文件描述符fd
在子进程中是有效的。
5. 系统调用dup
5.1 dup原型
// 原型
#include <unistd.h>
int dup(int oldfd);
功能:
- 通过复制文件描述符
oldfd
,创建一个新的文件描述符newfd
newfd
和oldfd
指向相同的文件
参数:
- oldfd:被复制的文件描述符
返回值:
- 如果成功,返回新复制的文件描述符;
- 如果失败,返回非0。
dup前:文件描述符表的前3项已经被占用,oldfd
指向文件描述符表的第2项(下标)。
dup后:dup找到一个空闲的表项,即文件描述符表的第3项(下标)是空闲的。因此dup返回的newfd
为3。
5.2 dup2原型
// 原型
#include <unistd.h>
int dup2(int oldfd, int newfd);
功能:
-
通过复制文件描述符
oldfd
,创建一个新的文件描述符newfd
; -
newfd
和oldfd
指向相同的文件。
参数:
-
oldfd
:被复制的文件描述符; -
newfd
:新创建的文件描述符。
返回值
-
如果成功,返回新复制的文件描述符;
-
如果失败,返回非0。
5.3 显式输出至log文件
// exe6.c
#include <stdio.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
int fd;
// 在当前目录下创建一个文件log
fd = open("log", O_CREAT|O_RDWR, 0666);
// 将字符串"hello"写到文件log中
write(fd, "hello\n", 6);
close(fd);
return 0;
}
编译运行程序。
$ cc -o exe6 exe6.c
$ ./exe6
$ cat log
hello
5.4 重定向至log文件
进程初始化时刻的文件描述符表,前3个文件描述符已经被打开了。
// exe7.c
#include <stdio.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
int fd;
// 在当前目录下创建一个文件log
fd = open("log.txt", O_CREAT|O_RDWR, 0666);
// 使用dup2将标准输出重定向到文件log, 文件描述符1是标准输出,fd指向log文件
dup2(fd, 1);
// 标准输出已经定向到文件log,之后通过标准输出写文件log,不再需要使用fd,因此关闭fd
close(fd);
// 将字符串"hello"写到标准输出,标准输出已经定向到文件log,最终输出保存到文件log
write(1, "hello\n", 6);
return 0;
}
使用open(“log”)后的文件描述符表如下,因为文件描述符表的前3项已经被占用了,故新打开的文件描述符是3。
dup2(fd,1)对文件描述符表的操作如下:
- 首先关闭文件描述符1;
- 然后把文件描述符1指向文件描述fd
使用close(fd)后的文件描述符表如下。
编译运行程序。
$ cc -o exe7 exe7.c
$ ./exe7
$ cat log
hello
6. 系统调用pipe
6.1 pipe原型
// 原型
#include <unistd.h>
int pipe(int fd[2]);
功能:创建一个可读写的管道,管道具备读端和写端。
参数:fd[0]
为管道的读端;fd[1]
为管道的写端。
返回值:如果成功,返回0;如果失败,返回非0。
- 创建一个先进先出的
队列
用于存储数据。 - 创建两个file结构体:管道的读端,从先进先出队列中读取数据;管道的写端,向先进先出队列中写入数据。
- 返回两个文件描述符
fd[0]
和fd[1]
:fd[0]
指向管道的读端;fd[1]
指向管道的写端。
6.2 例子1
// exe8.c
#include <stdio.h>
#include <unistd.h>
int main()
{
int fd[2];
char buf[32];
pipe(fd);
// 通过write(fd[1])将字符串hello,发送给管道
write(fd[1], "hello", 6);
// 通过read(fd[0])从管道中读取数据
read(fd[0], buf, sizeof(buf));
printf("Receive:%s\n", buf);
return 0;
}
编译运行该程序。
$ cc -o exe8 exe8.c
$ ./exe8
Receive:hello
6.3 例子2
// exe9.c
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
int pid;
int fd[2];
char buf[32];
// 先创建管道
pipe(fd);
// 再创建子进程,子进程将继承文件描述符fd[0]和fd[1]
pid = fork();
if (pid == 0) {
// child
close(fd[0]);
// 子进程write(fd[1])将字符串hello写入管道
write(fd[1], "hello", 6);
exit(0);
}
// parent
close(fd[1]);
// 父进程read(fd[0])从管道中读取数据
read(fd[0], buf, sizeof(buf));
printf("Receive:%s\n", buf);
return 0;
}
编译运行程序。
$ cc -o exe9 exe9.c
$ ./exe9
Receive:hello
父进程使用pipe()创建管道。使用fork()后,子进程复制父进程的文件描述符表
子进程close(fd[0])
:关闭管道的读端,使用管道的写端。父进程close(fd[1])
:关闭管道的写端,使用管道的读端。
6.4 例子3
// exe10.c
#include <stdio.h>
#include <unistd.h>
int main()
{
int pid;
int fd[2];
char buf[32];
// 创建管道
pipe(fd);
// 创建子进程,子进程将继承文件描述符fd[0]和fd[1]
pid = fork();
if (pid == 0) {
// child
// 子进程将标准输出定向到管道的写端(fd[1]),子进程使用标准输出将数据发送到父进程
dup2(fd[1], 1);
close(fd[0]);
close(fd[1]);
// 子进程write(fd[1])将字符串hello写入管道
write(1, "hello", 6);
exit(0);
}
// parent
// 父进程将标准输入定向到管道的读端(fd[0])
dup2(fd[0], 0);
close(fd[0]);
close(fd[1]);
// 父进程read(fd[0])从管道中读取数据
read(0, buf, sizeof(buf));
printf("Receive:%s\n", buf);
return 0;
}
编译运行程序。
$ cc -o exe10 exe10.c
$ ./exe10
Receive:hello
父进程使用pipe()创建管道。使用fork()后,子进程复制父进程的文件描述符表
子进程dup2(fd[1], 1)
,将标准输出定向到管道的写端fd[1]
。父进程dup2(fd[0], 0)
,将标准输入定向到管道的读端fd[0]
。
子进程close(fd[0])/close(fd[1])
,关闭管道的读端和写端。父进程close(fd[0])/close(fd[1])
,关闭管道的读端和写端。
父子进程通过管道连接,子进程的标准输出连接到父进程的标准输入。
shell提供了管道命令,例如cat /etc/passwd | wc -l
,cat
命令的标准输出连接到wc
命令的标准输入。wc
统计/etc/passwd
的行数,有45行。修改程序exe10.c
,实现如上的管道命令。
// exe11.c
#include <stdio.h>
#include <unistd.h>
int main()
{
int pid;
int fd[2];
char buf[32];
pipe(fd);
pid = fork();
if (pid == 0) {
// child
dup2(fd[1], 1);
close(fd[0]);
close(fd[1]);
// write(1, "hello", 6);
// 执行cat命令将文件/etc/passwd的内容送往标准输出
execlp("cat", "cat", "/etc/passwd", NULL);
exit(0);
}
// parent
dup2(fd[0], 0);
close(fd[0]);
close(fd[1]);
read(0, buf, sizeof(buf));
// 执行wc命令将读取标准输入,统计行的个数
execlp("wc", "wc", "-l", NULL);
// printf("Receive:%s\n", buf);
return 0;
}
编译运行程序。
$ cc -o exe11 exe11.c
$ ./exe11
45
$ cat /etc/passwd | wc -l
45
与管道命令cat /etc/passwd | wc -l
的结果一致。