ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

操作系统实践05—文件描述符和系统调用

2022-03-20 15:58:51  阅读:149  来源: 互联网

标签:文件 操作系统 05 int 描述符 fd include buf


操作系统实践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结构体指针保存在文件描述符表中。

打开文件的过程如下:

在这里插入图片描述

  1. 找到文件对应的索引节点inode
  2. 分配一个file结构体,file结构体的inode字段指向第1步的inode,file结构体的文件访问位置字段初始化为0;
  3. 从文件描述符表中找一个空闲项,指向第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
  • newfdoldfd指向相同的文件

参数:

  • 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

  • newfdoldfd指向相同的文件。


参数:

  • 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。

在这里插入图片描述

  1. 创建一个先进先出的队列用于存储数据。
  2. 创建两个file结构体:管道的读端,从先进先出队列中读取数据;管道的写端,向先进先出队列中写入数据。
  3. 返回两个文件描述符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 -lcat命令的标准输出连接到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的结果一致。

标签:文件,操作系统,05,int,描述符,fd,include,buf
来源: https://blog.csdn.net/weixin_46003347/article/details/123614676

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有