0%

1. 文件描述符

Linux系统中一切皆文件,所有打开的文件都通过文件描述符进行索引。文件描述符是一个非负整数,当打开或创建文件时,内核向该进程返回一个文件描述符用于指代该文件。

Linux内核对所有打开的文件都维护了一个文件描述符表,用于存储文件描述符。且在文件描述符表中已经默认分配了三个文件描述符:0是标准输入stdin,1是标准输出stdout,2是标准错误stderr。故打开或创建的文件描述符大于或等于3。

进程描述符表

2. open/close

2.1 open函数

通过open函数可以打开或创建一个文件,返回的文件描述符一定是该进程尚未使用的最小描述符,大于等于3。

函数原型:

1
2
3
4
5
#include <fcntl.h>
// 打开一个已经存在的磁盘文件
int open(const char *path, int flags);
// 打开磁盘文件, 如果文件不存在, 就会自动创建
int open(const char *path, int flags, mode_t mode);

参数:

  • path:要打开的文件的路径名。

  • flags:打开文件的方式和选项,它是一个位掩码,可以通过按位或运算组合多个选项。常见选项包括:

    • O_RDONLY —— 以只读方式打开文件
    • O_WRONLY —— 以只写方式打开文件
    • O_RDWR —— 以读写方式打开文件
    • 可选属性:(和以上属性一起使用)
      • O_APPEND —— 新数据追加到文件的尾部
      • O_CREAT —— 如果文件不存在,则创建该文件
      • O_TRUNC—— 如果文件存在且以写入方式打开,则清空文件内容
      • O_EXCL —— 检测文件是否存在, 必须要和 O_CREAT 一起使用, 不能单独使用:O_CREAT | O_EXCL检测到文件不存在, 创建新文件;检测到文件已存在, 创建失败, 函数直接返回-1
    • O_NONBLOCK ——对于设备文件,以O_NONBLOCK方式打开可以做非阻塞I/O
  • mode:表示文件的权限掩码,指定新建文件的权限,仅在O_CREAT选项被指定时有效

    • 文件权限掩码是一个八进制数,每一位分别代表了不同的权限。
    • 一般情况下,文件权限掩码可以分为三组:用户权限、组权限和其他用户权限。每组权限又包括读(R)、写(W)和执行(X)权限。
    • 读(R):4、写(W):2、执行(X):1
    • 例如:
      • rwxrwxrwx:全部权限,八进制数值为777。
      • rw-r--r--:所有者可读写,其他用户只读,八进制数值为644。

返回值:

  • 成功:返回一个非负整数,表示文件描述符。
  • 失败:返回 -1,并设置 errno 来指示错误类型。

2.2 close函数

通过close函数关闭一个打开文件,释放打开时分配的文件描述符。close()原型:

1
2
#include <unisted>
int close(int fd)

参数:

  • fd:要关闭的文件描述符。

返回值:

  • 成功:返回0。
  • 失败:返回-1,并设置 errno 来指示错误类型。

2.3 文件操作

打开已有文件:

1
int fd = open("abc.txt", O_RDWR);

创建新文件:

1
int fd = open("./new.txt", O_CREAT|O_RDWR, 0664);

3. read/write

3.1 read函数

用于从打开的文件中读数据

函数原型:

1
2
#include <unisted.h>
ssize_t read(int fd, void *buf, size_t count);

参数:

  • fd: 要读取的文件描述符
  • buf: 指向存放读取数据的缓冲区的指针。
  • count: 要读取的字节数

返回值: 返回值的数据类型是 ssize_t,表示带符号的整型,这样既可以返回正的字节数、0(表
示到达文件末尾)也可以返回负值-1(表示出错) 。

  • 成功:返回读取的字节数,可能小于 count,因为可能在读取count个字节之前已到达文件末尾。
  • 文件结束(EOF):返回 0,表示已读取到文件末尾。
  • 失败:返回 -1,并设置 errno 来指示错误类型。

什么是缓冲区:

  • 缓冲区(Buffer)是指在计算机中用于临时存储数据的一段内存区域。在程序中,缓冲区通常用于临时存放数据,以便稍后进行处理或传输。缓冲区的使用可以提高数据的读写效率,减少频繁的IO操作,特别是在涉及大量数据的情况下。

  • 在文件IO操作中,缓冲区通常用于存储要读取或写入的数据。当调用 read() 系统调用从文件中读取数据时,数据首先被读取到缓冲区中,然后程序可以从缓冲区中获取数据进行进一步处理。类似地,当调用 write() 系统调用向文件中写入数据时,数据首先被写入到缓冲区中,然后由系统决定何时将缓冲区中的数据写入到文件中。

3.2 write函数

用于将数据从用户空间写入到已打开文件中

函数原型:

1
2
#include <unisted.h>
ssize_t write(int fd, const void *buf, size_t count)

参数:

  • fd:要写入的文件描述符。
  • buf:指向要写入数据的缓冲区的指针。
  • count:往磁盘文件中写入的字节数,一般是buf的长度sizeof(buf)。

返回值:

  • 成功:返回写入的字节数,可以小于 count
  • 失败:返回 -1,并设置 errno 来指示错误类型。

3.3 示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <fcntl.h>
#include <stdio.h>
#include <errno.h>

int main() {
int fd = open("example.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd == -1) {
perror("open");
return 1;
}

const char *data = "Hello, World!\n";
ssize_t bytes_written = write(fd, data, strlen(data));
if (bytes_written == -1) {
perror("write");
return 1;
}

close(fd);

return 0;
}

4. 阻塞和非阻塞

阻塞和非阻塞是指,程序在执行某种操作时的行为方式。

  • 阻塞(Blocking):

    ​ 在阻塞模式下,当程序执行某个操作时,如果该操作无法立即完成,程序将一直等待,直到操作完成为止。

  • 非阻塞(Non-blocking):

    ​ 在非阻塞模式下,当程序执行某个操作时,如果该操作无法立即完成,程序将立即返回而不会等待。程序可以继续执行其他任务,不会因为某个操作的未完成而被阻塞。

使用非阻塞 I/O 可以使得一个进程能够同时处理多个连接而不会被阻塞。当一个套接字上没有数据可读或者没有数据可写时,程序不会等待,而是立即返回,这样可以更有效地利用 CPU 时间。

非阻塞 I/O 结合事件驱动模型(如 epoll)可以实现异步 I/O。通过 epoll 等机制,程序可以注册对多个文件描述符的监听,一旦有事件发生,程序立即得到通知,而不需要轮询或阻塞等待。

5. fcntl函数

fcntl()可以修改一个已打开文件的属性,可以重新设置读、写、追加、非阻塞等标志,而不必重新open文件 。

函数原型:

1
2
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* arg */ );

参数fd表示已打开的文件描述符,cmd 是控制命令,指定了要对文件进行的操作,arg 是与控制命令相关的参数,类型和意义取决于具体的命令。

常用命令:

  • F_DUPFD

    • 复制文件描述符,创建一个新的文件描述符,其值大于或等于第三个参数(指定的最小文件描述符值)。

    • fcntl(fd, F_DUPFD, newfd)
      
      1
      2
      3
      4
      5
      6
      7

      - `F_GETFL`:

      - 获取文件状态标志(file status flags)。

      - ```
      fcntl(fd, F_GETFL)
  • F_SETFL

    • 设置文件状态标志为flags。

    • fcntl(fd, F_SETFL, flags)
      
      1
      2
      3
      4
      5
      6
      7

      - `F_GETFD`:

      - 获取文件描述符标志(file descriptor flags)。

      - ```
      fcntl(fd, F_GETFD)
  • F_SETFD

    • 设置文件描述符标志为flags。

    • fcntl(fd, F_SETFD, flags)
      
      1
      2
      3
      4
      5
      6
      7

      - `F_GETLK`:

      - 获取文件锁信息,并将锁信息写入提供的结构体 `lock` 中。

      - ```
      fcntl(fd, F_GETLK, &lock)
  • F_SETLK

    • 设置文件锁,如果无法获取锁,则调用失败。

    • fcntl(fd, F_SETLK, &lock)
      
      1
      2
      3
      4
      5
      6
      7

      - `F_SETLKW`:

      - 设置文件锁,并在必要时阻塞。

      - ```
      fcntl(fd, F_SETLKW, &lock)

将文件设置为非阻塞:

1
fcntl(fd, F_SETFL, fcntl(fd, F_GETFL) | O_NONBLOCK)

6. Iseek函数

用于移动文件的指针。

每一个打开文件都有一个与其相关联的”当前文件偏移量“,即文件指针指向的位置,作用在于控制文件的读写位置

1
2
#include <unisted.h>
off_t lseek(int fd, off_t offset, int whence);
  • 参数:

    • fd:所操纵文件的描述符
    • offset:偏移量,需要与whence搭配使用
    • whence:通过该参数指定函数功能
      • SEEK_SET:从文件头部开始偏移 offset 个字节
      • SEEK_CUR: 从当前文件指针的位置向后偏移offset个字节
      • SEEK_END: 从文件尾部向后偏移offset个字节
  • 返回值:成功则返回新的文件偏移量(文件头部到当前位置的偏移量),失败则返回-1

  • lseek(fd, 0, SEEK_SET); //将文件指针移动到头部
    lseek(fd, 0, SEEK_CUR); //得到当前文件指针的位置
    lseek(fd, 0, SEEK_END); //得到文件的总大小
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11


    # 7. 复制与重定向

    ## 7.1 dup函数

    dup函数的作用是复制一个现有的文件描述符,使多个文件描述符指向同一个文件。函数原型:

    ```c
    #include <unisted.h>
    int dup(int oldfd);

参数oldfd是被复制的文件描述符

函数调用成功后返回新的文件描述符,且是当前可用文件描述符中的最小数值;调用失败则返回-1

被复制出的新文件描述符是独立于旧的文件描述符的,二者没有连带关系。也就是说当旧的文件描述符被关闭了,复制出的新文件描述符还是可以继续使用的。

7.2 dup2函数

dup2函数可以进行文件描述符的复制,也可以进行文件描述符的重定向。重定向指的是断开文件描述符和当前文件的关联关系,与新的文件建立关联关系。

函数原型:

1
2
#include <unisted.h>
int dup2(int oldfd, int newfd);

作用是复制参数 oldfd 所指向的文件描述符,创建一个新的文件描述符,新的文件描述符的值是参数 newfd。如果 newfd 已经是一个打开的文件描述符,dup2() 会先关闭它。

dup2() 调用成功返回新的文件描述符,该文件描述符与 newfd 相同;调用失败则返回-1

1. 概念

虚拟地址空间是指一个进程所使用的地址范围,但这些地址并不直接对应于物理内存中的位置,而是通过CPU中的内存管理单元MMU将虚拟地址空间映射到物理内存上。

虚拟地址空间的大小由操作系统决定,32位操作系统虚拟地址空间的大小为 2的32字节,也就是4G,64位的操作系统虚拟地址空间大小为2的64 字节。

当我们运行磁盘上一个可执行程序, 就会得到一个进程,内核会给每一个运行的进程创建一块属于自己的虚拟地址空间,并将应用程序数据装载到虚拟地址空间对应的地址上。

1.1 为什么使用虚拟内存

对于直接操纵物理内存的单片机来说,同时运行多个程序是不可能的。虚拟地址的存在是为了解决多程序并发执行时的内存管理和隔离问题,它带来了以下几个重要的好处和必要性:

  1. 内存隔离:虚拟地址允许每个进程拥有自己独立的地址空间,使得每个进程认为自己在使用整个系统的全部内存,从而实现了进程之间的内存隔离。这意味着一个进程无法直接访问另一个进程的内存,增强了系统的安全性和稳定性。
  2. 内存管理:通过虚拟地址空间,操作系统可以更灵活地管理内存,例如采用虚拟内存技术将部分数据存储在磁盘上,从而扩展可用内存大小。此外,还能使操作系统更容易地进行内存分配和释放,从而降低了内存管理的复杂性。

2. 虚拟地址到物理地址的映射

2.1 分页

页表是最常见的地址转换方式。虚拟地址被划分为固定大小的页(linux中每页为4KB),每个页都有对应的页表条目。页表记录了虚拟地址到物理地址的映射关系。当CPU发出一个内存访问请求时,MMU会根据页表将虚拟地址翻译成物理地址。

2.2 分段

虚拟地址被划分为逻辑段,每个逻辑段有对应的段描述符,记录了段的起始地址和长度等信息。CPU根据段描述符将虚拟地址转换成线性地址,然后再通过页表将线性地址映射到物理地址。

1. 面向过程

面向过程是一种以事件为中心的编程思想,按照从上到下的步骤解决问题,通过编写函数来实现每一步的操作,最终依次调用完成程序的运行。

2. 面向对象

简单问题采用面向过程的思想来解决时直接有效,但当问题的规模变得更大、更繁琐时就显得远远不够。面向对象是一种基于面向过程的编程思想,是向现实世界模型的自然延伸,是一种“万物皆对象”的编程思想。面向对象把事物抽象成对象的概念,给对象赋一些属性和方法,然后让每个对象去执行自己的方法。

即以对象为中心,以消息为驱动。

3. 优缺点

面向过程

优点:效率高,面向过程强调代码的短小精悍,善于结合数据结构来开发高效率的程序。

缺点:代码重用性低,扩展能力差,后期维护难度比较大。

面向对象

优点:基于封装、继承和多态的特性,具有易扩展,代码复用率高,易维护等特点,可以设计低耦合的系统。

缺点:开销大,性能低。

Welcome to Hexo! This is your very first post. Check documentation for more info. If you get any problems when using Hexo, you can find the answer in troubleshooting or you can ask me on GitHub.

Quick Start

Create a new post

1
$ hexo new "My New Post"

More info: Writing

Run server

1
$ hexo server

More info: Server

Generate static files

1
$ hexo generate

More info: Generating

Deploy to remote sites

1
$ hexo deploy

More info: Deployment