[OS_7] 访问操作系统对象 | offset | FHS | Handle

实验代码可以看去年暑假的这篇文章:【Linux】进程间通信:详解 VSCode使用 | 匿名管道


我们已经知道,进程从 execve 后的初始状态开始,可以通过 mmap 改变自己的地址空间,通过 fork 创建新的进程,再通过 execve 执行新的程序——我们慢慢已经开始理解 “操作系统上的应用生态“ 并没有魔法了。

本讲内容:操作系统还必须给应用程序提供访问操作系统对象的机制。当然,我们可以直接以 API 的形式提供,例如 Win32 API 包含 “RegOpenKeyEx” 访问注册表。这节课我们学习 UNIX 的 “Everything is a file” 带来的方便 (和不便)。


上一篇文章当中我们讲到过,边写代码边测试非常的重要,那么我们该如何实现测试呢

明确需求

  • 我们需要一个状态 (Everything is a state machine)
    • tests 里有全部测试用例程序还没结束
  • 需要遵守 libc 的约定
    • tk_add_test 和 tk_register_hook 执行顺序不确定
    • System test 里还可能调用 atexit()

解决问题

  • tk_register_hook 时立即创建一个进程
    • 等程序 atexit() 的时候把 tests 送过来
    • 程序之间交换数据:“serialization”(序列化)

Prompt: 有哪些对象 serialization 的通用机制?

常见的对象序列化机制包括:

  1. Java 序列化:使用 Serializable 接口。
  2. JSON:轻量级数据交换格式,广泛用于 Web 开发。
  3. XML:可扩展标记语言,适用于复杂数据结构。
  4. Protocol Buffers:Google 的高效二进制序列化格式。
  5. Avro:Apache 的数据序列化系统,支持动态模式。
  6. MessagePack:二进制 JSON 格式,更高效。
  7. Thrift:Facebook 开发的跨语言序列化框架。

根据需求选择合适的机制。

在这个例子里,因为是完全相同的进程地址空间

  • 我们直接做了一个 “内存复制”,保留了 argv 中指向只读数据的指针。
  • 对于跨应用的对象序列化,则需要做字符串的 deep copy。

我们的做法:利用编程语言机制 hack

  • 并不是好的 practice (但有时候需要)

方法一:请开发者主动调用 API

  • 在 main 里 run_all_tests()
  • 之前学习机 gtest 的时候用的就是这种方法

方法二:提供一个特别的编译器

  • JavaScript: 这个我懂

方法三:更好的编程语言

  • JVMTI: Tool Interface

testkit: Writing test cases fearlessly! 这是用于实验的第一个测试框架:支持单元测试和系统测试,自动注册测试用例并在程序退出后运行。

最重要的特点是它使用简单:只需要包含 testkit.h,并且链接 testkit.c 即可。

没有测试过的代码,都是有可能存在问题的!


操作系统中的对象

进程

  • 进程 = 状态机
  • 进程管理 API: fork, execve, exit

连续的内存段

  • 我们可以把 “连续的内存段” 看作一个对象
    • 可以在进程间共享
    • 也可以映射文件
  • 内存管理 API: mmap, munmap, mprotect, msync

操作系统肯定还有其他对象的!

是如何访问操作系统对象的呢,那通过文件来访问操作系统的对象

  • 文件像键盘显示器也都可以理解为文件,哦我好像知道了
  • 相当于是平时写的代码生成了程序和进程,跑在操作系统这个环境上
  • 然后通过文件来访问这些东西,读取到的结果就是,例如是以我们显示器也是一个文件,来显示出来

7.1 文件描述符

文件和设备

文件:有 “名字” 的数据对象

  • 字节流 (终端,random)
  • 字节序列 (普通文件)

文件描述符

  • 指向操作系统对象的 “指针”
    • Everything is a file
    • 通过指针可以访问 “一切”
  • 对象的访问都需要指针
    • open, close, read/write (解引用), lseek (指针内赋值/运算), dup (指针间赋值)

文件描述符:访问文件的 “指针”

  • open
    • p = malloc(sizeof(FileDescriptor));
  • close
    • delete(p);
  • read/write
    • *(p.data++);
  • lseek
    • p.data += offset;
  • dup
    • q = p;

在去年暑假,我们手写 shell 的时候,有详细写过这部分的代码,感兴趣的可以去看一下

访问操作系统中的对象--文件描述符

  • 总是分配最小的未使用描述符
  • 0, 1, 2 是标准输入、输出和错误
  • 新打开的文件从 3 开始分配
    • 文件描述符是进程文件描述符表的索引
    • 关闭文件后,该描述符号可以被重新分配
    • Linux 下一切皆文件

进程能打开多少文件?

  • ulimit -n (进程限制)
  • sysctl fs.file-max (系统限制)

文件描述符中的 offset

文件描述符是 “进程状态的” 的一部分

  • 保存在操作系统中;程序只能通过整数编号访问
  • 文件描述符自带一个 offset

Quiz: fork() 和 dup()(共享) 之后,文件描述符共享 offset 吗?

  • 这就是 fork() 看似优雅,实际复杂的地方

场景

是否共享 offset

原因

独立打开同一文件

❌ 不共享

每个 open()

生成独立文件表项

fork()

✅ 共享(继承描述符)

子进程复制父进程文件表项,所以他们打开同一文件的话,不会实现覆盖,顶多出现交叉写入

dup()

✅ 共享

描述符指向同一文件表项

文件描述符:文件描述符是指向操作系统对象的 “指针”——系统调用通过这个指针 (fd) 确定进程希望访问操作系统中的哪个对象。我们有 open, close, read/write, lseek, dup 管理文件描述符。


Windows 中的文件描述符

Handle (把手;握把;把柄)

  • 比 file descriptor 更像 “指针”
  • 你有一个 “handle” 在我手上,我就可以更好地控制你

Windows 的进程创建

面向工程的设计

  • 默认 handle 是不继承的 (和 UNIX 默认继承相反)
    • 可以在创建时设置 bInheritHandles,或者运行时修改
    • “最小权限原则”
  • lpStartupInfo 用于配置 stdin, stdout, stderr

(参考) Windows进程创建的工程化设计核心要点

1. 默认不继承句柄
  • 安全设计新进程默认不继承父进程的资源访问权限(句柄),防止意外泄露。
  • 按需授权:通过参数 bInheritHandles 或运行时调整,显式指定需共享的资源。
2. 集中式配置入口
  • 统一管理STARTUPINFO 结构体统一配置子进程的标准输入/输出/错误流,避免参数分散。
  • 模块化扩展:通过结构化字段支持未来功能扩展,降低接口变动风险。
3. 安全与功能的工程权衡
  • 安全优先:相比UNIX默认继承的便利性,Windows更强调最小权限原则(仅开放必要权限)。
  • 灵活控制:开发者可精准指定共享资源,平衡功能需求与安全风险。

Linux 引入了 O_CLOEXEC

  • fcntl(fd, F_SETFD, FD_CLOEXEC)
//对fd进行各种操作,成功返回0,失败返回-1设errno
#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd, ... );		//...表示可变长参数
/*cmd:
Adversory record locking:
F_SETLK(struct flock*)	//设建议锁
F_SETLKW(struct flock*)	//设建议锁,如果文件上有冲突的锁,且在等待的时候捕获了一个信号,则调用被打断并在信号捕获之后立即返回一个错误,如果等待期间没有信号,则一直等待 
F_GETLK(struct flock*)	//尝试放锁,如果能放锁,则不会放锁,而是返回一个含有F_UNLCK而其他不变的l_type类型,如果不能放锁,那么fcntl()会将新类型的锁加在文件上,并把当前PID留在锁上
Duplicating a file descriptor:
F_DUPFD (int)		//找到>=arg的最小的可以使用的文件描述符,并把这个文件描述符用作fd的一个副本
F_DUPFD_CLOEXEC(int)//和F_DUPFD一样,除了会在新的文件描述符上设置close-on-exec
F_GETFD (void)		//读取fd的flag,忽略arg的值
F_SETFD (int)		//将fd的flags设置成arg的值.
F_GETFL (void)		//读取fd的Access Mode和其他的file status flags; 忽略arg
F_SETFL (long)		//设置file status flags为arg
F_GETOWN(void)		//返回fd上接受SIGIO和SIGURG的PID或进程组ID
F_SETOWN(int)		//设置fd上接受SIGIO和SIGURG的PID或进程组ID为arg
F_GETOWN_EX(struct f_owner_ex*)	//返回当前文件被之前的F_SETOWN_EX操作定义的文件描述符R
F_SETOWN_EX(struct f_owner_ex*)	//和F_SETOWN类似,允许调用程序将fd的I/O信号处理权限直接交给一个线程,进程或进程组
F_GETSIG(void)		//当文件的输入输出可用时返回一个信号
F_SETSIG(int)		//当文件的输入输出可用时发送arg指定的信号
*//*…: 	
可选参素,是否需要得看cmd,如果是加锁,这里应是struct flock*
struct flock {short l_type;	//%d Type of lock: F_RDLCK(读锁), F_WRLCK(写锁), F_UNLCK(解锁)short l_whence;	//%d How to interpret l_start, 加锁的位置参考标准:SEEK_SET, SEEK_CUR, SEEK_ENDoff_t l_start;  //%ld Starting offset for lock, 	加锁的起始位置off_t l_len;    //%ld Number of bytes to lock , 锁定的字节数pid_t l_pid;   	// PID of process blocking our lock, (F_GETLK only)加锁的进程号,,默认给-1
};
*/

  • 文件描述符:文件描述符是指向操作系统对象的 “指针”——系统调用通过这个指针 (fd) 确定进程希望访问操作系统中的哪个对象。

Filesystem Hierarchy Standard --FHS

  • enables software and user to predict the location of installed files and directories: 例如 macOS 就不遵循 FHS

只要拷对了文件,操作系统就能正常执行啦!

  1. 创建 UEFI 分区,并复制正确的 Loader
  2. 创建文件系统
    • mkfs (格式化)
  1. cp -ar 把文件正确复制 (保留权限)
    • 注意 fstab 里的 UUID
    • 就得到了一个可以正常启动的系统盘!
  1. 运行时挂载必要的其他文件系统
    • 磁盘上的 /dev, /proc, ... 都是空的
    • mount -t proc proc /mount/point 可以 “创建” procfs

对于Linux制作系统盘的实验前文有写过,感兴趣的可以找着看一下Linux 系统盘制作 | 引导加载器(GRUB 为例)| mount

操作系统给了我们很多API,可以创建各种各样的对象


任何 “可读写” 的东西都可以是文件

真实的设备

  • /dev/sda
  • /dev/tty

虚拟的设备 (文件)

  • /dev/urandom (随机数), /dev/null (黑洞), ...
    • 它们并没有实际的 “文件”
    • 操作系统为虚拟的设备 (文件)实现了特别的 read 和 write 操作

      • /drivers/char/mem.c
      • 发现的一些有意思的事情:甚至可以通过 /sys/class/backlight 控制屏幕亮度
  • procfs 也是用类似的方式实现的

管道:一个特殊的 “文件” (流)

  • 由读者/写者共享
    • 读口:支持 read
    • 写口:支持 write

匿名管道

(Unix得图灵奖的一个重要原因🙂

  • 返回两个文件描述符
  • 进程同时拥有读口和写口
    • 看起来没用?不,fork 一下就有用了 (testkit)

  • 然后甚至还可以再结合dup,来实现对 0 1 2 指向替代也可以
  • ls | wc -l (这就是一个管道的应用)

⭕理解管道的意义:

  • UNIX 管道:UNIX 管道 (pipe) 是一种典型的进程间通信机制,允许数据在不同的进程之间单向流动。
  • 管道可以被视为一种特殊的文件,其中一个进程将数据写入管道的一端,而另一个进程从另一端读取数据。
  • 管道实现了父子进程当中,先后的同步

⭕sum

进程管理

  • fork, execve, waitpid, exit

内存管理

  • mmap, munmap, mprotect, msync

文件管理

  • open, close, read, write, lseek, dup

  • mkfifo 命名管道,可以属于某一个文件
  • pipe 匿名管道。

  • proc
  • sys
  • Everything is a File ... 

例如来和 ag 结合,cat readme.md | ag 就可以问AI 啦


一切皆文件的好处

一套 API 访问所有对象

  • 一切都可以 | grep
    • Introducing ag -g

同时,UNIX Shell 的语法广受诟病

  • 稍大一些的项目就应该用更好的语言 (Python, Rust!)
  • 但是:We all love quick & dirty!
ls -l /proc/*/fd/* 2>/dev/null | awk '{print $(NF-2), $(NF-1), $NF}'
grep -s VmRSS /proc/*[0-9]/status | awk '{sum += $2} END {print sum " kB"}'

Prompt: 什么是 glob patterns? 有什么实际中常用的扩展?

Glob patterns 是一种用于匹配文件路径名的模式,常用于文件搜索和操作。常见的通配符包括:

  • *:匹配任意数量的字符(包括零个)
  • ?:匹配单个字符
  • []:匹配指定范围内的字符

实际中常用的扩展包括:

  • **:递归匹配任意层级的子目录
  • {}:匹配多个模式,如 {a,b,c} 匹配 abc
  • !(pattern):排除指定模式

这些扩展增强了 glob patterns 的灵活性和功能性。


字节流

  • 顺序读/顺序写
    • 没有数据时等待
    • 典型代表:管道
    • 但是会存在操作系统对offset默默地移动...

字节序列

  • 其实就有一点点不方便了
    • 需要到处 lseek 再 read/write
      • mmap 不香吗?指针指哪打哪
      • madvise, msync 提供了更精细的控制

lseek 用于重新定位文件偏移量(文件指针位置),支持三种定位模式:

  • SEEK_SET:绝对定位(从文件头开始偏移)
  • SEEK_CUR:相对定位(从当前位置偏移)
  • SEEK_END:从文件末尾偏移

补充说明:该函数不会触发任何物理 I/O 操作,仅修改内核中的文件偏移量记录。

优点

  • 优雅,文本接口,就是好用

缺点

  • 和各种 API 紧密耦合
    • A fork() in the road

  • im ple men ta tion 实现
  • al ter na tives 替代方案
  • con flates 合并

如果我fork出的父子进程,同时写Hello和world,它会是覆盖呢还是实现延续呢?

  1. 进程独立性
    fork()会创建子进程,父子进程的内存空间是独立的,但文件描述符(如标准输出)是共享的。因此,两者的输出会混合到同一个目标中,但不会直接覆盖(每个write操作是原子的)
  2. 输出顺序不确定
    父子进程的执行顺序由操作系统调度决定。可能的结果包括:
    • 父进程先输出“Hello”,子进程后输出“World” → HelloWorld
    • 子进程先输出“World”,父进程后输出“Hello” → WorldHello
    • 两者交替执行,导致字符交错(如HWeolrllod

  • 对高速设备不够友好(why)
    • 额外的延迟和内存拷贝
    • 单线程 I/O

Any problem in computer science can be solved with another level of indirection. (Butler Lampson)

  • Windows NT: Win32 API → POSIX 子系统
    • Windows Subsystem for Linux (WSL)
  • macOS: Cocoa API → BSD 子系统
  • Fuchsia: Zircon 微内核 → POSIX 兼容层

兼容当然没法做到 100%

  • sysfs, procfs 就是没法兼容
  • 优雅的 WSL1 已经暴毙
    • “Windows Subsystem for Linux”
    • “Linux Subsystem for Windows” (wine)
    • 对硬件做抽象,给应用程序提供服务

拓展: OpenHarmony


对于 硬件和软件
  • “初学FPGA,突然顿悟何为“硬件的并行化思维”的美妙,那一瞬之后,就像打通了任督二脉,之后不管是看代码还是写代码都变得十分顺畅。更关键的是我的认知也得到了提升:那是我第一次认知到不同思维模式会对coding产生如此之大的区别。
  • 其实反过来各种语言也在(强迫)塑造人的思维模式,比如cuda之类的并行编程要求程序员转换思维模式。HDL也一样。人发明工具,然后被工具改变。
  • 以除法为例,软件工程师代码的除法: int val=3300/256 ; 硬件工程师:int val = 3300》8;“

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/pingmian/77533.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

关于TCP三次握手和四次挥手过程中的状态机、使用三次握手和四次挥手的原因、拥塞控制

关于传输层中的TCP协议&#xff0c;我们在之前的博客中对其报文格式、三次握手、四次挥手、流量控制、数据传输等机制进行了具体说明&#xff0c;接下来在前面所学的基础上&#xff0c;我们再来讲讲TCP中三次握手和四次挥手各阶段所处的状态机以及为什么要使用三次握手和四次挥…

学习笔记二十——Rust trait

&#x1f9e9; Rust Trait 彻底搞懂版 &#x1f440; 目标读者&#xff1a;对 Rust 完全陌生&#xff0c;但想真正明白 “Trait、Trait Bound、孤岛法则” 在做什么、怎么用、为什么这样设计。 &#x1f6e0; 方法&#xff1a; 先给“心里模型”——用生活类比把抽象概念掰开揉…

es 混合检索多向量

在结合向量相似度检索的同时,可以通过 bool 查询的 filter 或 must 子句实现关键词过滤。以下是一个同时包含 关键词匹配 和 多向量相似度计算 的完整示例: 参考博文:ES集群多向量字段检索及混合检索方法-CSDN博客 示例:带关键词过滤的多向量联合检索 GET /my_index/_sea…

HTML5好看的水果蔬菜在线商城网站源码系列模板4

文章目录 1.设计来源1.1 主界面1.2 关于我们1.3 商品信息1.4 新闻资讯1.5 联系我们1.5 登录注册 2.效果和源码2.1 动态效果2.2 源代码 源码下载 作者&#xff1a;xcLeigh 文章地址&#xff1a;https://blog.csdn.net/weixin_43151418/article/details/147264262 HTML5好看的水果…

Kubernetes(k8s)学习笔记(二)--k8s 集群安装

1、kubeadm kubeadm 是官方社区推出的一个用于快速部署 kubernetes 集群的工具。这个工具能通过两条指令完成一个 kubernetes 集群的部署&#xff1a; 1.1 创建一个 Master 节点$ kubeadm init 1.2 将一个 Node 节点加入到当前集群中$ kubeadm join <Master 节点的 IP 和…

AI数据分析的优势分析

随着科技的飞速发展&#xff0c;人工智能&#xff08;AI&#xff09;已经深入渗透到数据分析领域&#xff0c;为各行各业带来了前所未有的变革。AI数据分析作为一种新兴的技术手段&#xff0c;通过运用机器学习、深度学习等算法对海量数据进行挖掘和分析&#xff0c;显著提升了…

leetcode(01)森林中的兔子

今天开始记录刷题的过程&#xff0c;每天记录自己刷题的题目和自己的解法&#xff0c;欢迎朋友们给出更多更好的解法。 森林中的兔子 森林中有未知数量的兔子&#xff0c;提问其中若干只兔子“还有多少只兔子与你&#xff08;被提问的兔子&#xff09;颜色相同”。将答案收集到…

基于SpringBoot+Vue实现的旅游景点预约平台功能一

一、前言介绍&#xff1a; 1.1 项目摘要 随着人们生活水平的提高和休闲时间的增多&#xff0c;旅游已经成为人们生活中不可或缺的一部分。旅游业作为全球经济的重要支柱&#xff0c;其发展趋势呈现出数字化、网络化和智能化的特点。传统的旅游服务方式&#xff0c;如人工预约…

【支付】支付宝支付

下面为你详细介绍使用 Spring Boot 对接支付宝支付&#xff0c;实现支付与退款功能的具体步骤和代码示例。 添加依赖 在 pom.xml 里添加支付宝 SDK 依赖&#xff1a; <dependencies><!-- Spring Boot Web --><dependency><groupId>org.springframewo…

shell 正则表达式与文本处理器

目录 前言 一、正则表达式 &#xff08;一&#xff09;定义与用途 &#xff08;二&#xff09;基础正则表达式 &#xff08;三&#xff09;基础正则表达式元字符 &#xff08;四&#xff09;扩展正则表达式 二、文本处理器&#xff1a;Shell 编程的得力助手 &#xff0…

ASP.NET Core 最小 API:极简开发,高效构建(上)

一、概述 构建最小 API&#xff0c;以创建具有最小依赖项的 HTTP API。 它们非常适合于需要在 ASP.NET Core 中仅包括最少文件、功能和依赖项的微服务和应用。 本文介绍使用 ASP.NET Core 生成最小 API 的基础知识&#xff0c;将创建以下 API&#xff1a; API&#xff08;应用…

Apache Parquet 文件组织结构

简要概述 Apache Parquet 是一个开源、列式存储文件格式&#xff0c;最初由 Twitter 与 Cloudera 联合开发&#xff0c;旨在提供高效的压缩与编码方案以支持大规模复杂数据的快速分析与处理。Parquet 文件采用分离式元数据设计 —— 在数据写入完成后&#xff0c;再追加文件级…

IntelliJ IDEA 2025.1 发布 ,默认 K2 模式 | Android Studio 也将跟进

2025.1 版本已经发布&#xff0c;在此之前我们就聊过该版本的 《Terminal 又发布全新重构版本》&#xff0c;而现在 2025.1 中的 K2 模式也成为了默认选项。 可以预见&#xff0c;这个版本可能会包含不少大坑&#xff0c;为下个 Android Studio 祈祷。 首先有一点可以确定&…

云效部署实现Java项目自动化部署图解

前言 记录下使用云效部署Java项目&#xff0c;实现java项目一键化自动化部署。 云效流程说明&#xff1a; 1.云效拉取最新git代码后 2.进行maven编译打包后&#xff0c;上传到指定服务器目录 3.通过shell脚本&#xff0c;先kill java项目后&#xff0c;通过java -jar 启动项…

国际数据加密算法(IDEA)详解

以下是修正后的准确版本,已解决原文中的术语、符号及技术细节问题: ​国际数据加密算法(IDEA)​ IDEA是一种分组加密算法,由Xuejia Lai(来学嘉)和James Massey于1990年设计。IDEA使用128位密钥对64位明文分组进行加密,经过8轮迭代运算后生成64位密文分组。其安全性基于…

TensorFlow介绍

TensorFlow 是由 Google 开发 的开源机器学习框架&#xff0c;主要用于构建、训练和部署机器学习模型。它支持深度学习、传统机器学习和数值计算&#xff0c;适用于图像识别、自然语言处理&#xff08;NLP&#xff09;、推荐系统、强化学习等多种任务。 核心特性 基于 数据流…

百级Function架构集成DeepSeek实践:Go语言超大规模AI工具系统设计

一、百级Function系统的核心挑战 1.1 代码结构问题 代码膨胀现象&#xff1a;单个文件超过2000行代码路由逻辑复杂&#xff1a;巨型switch-case结构维护困难依赖管理失控&#xff1a;跨Function依赖难以追踪 // 传统实现方式的问题示例 switch functionName { case "fu…

嵌入式芯片中的 SRAM 内容细讲

什么是 RAM&#xff1f; RAM 指的是“随机存取”&#xff0c;意思是存储单元都可以在相同的时间内被读写&#xff0c;和“顺序访问”&#xff08;如磁带&#xff09;相对。 RAM 不等于 DRAM&#xff0c;而是一类统称&#xff0c;包括 SRAM 和 DRAM 两种主要类型。 静态随机存…

标准的JNI (Java Native Interface) 加载函数 JNI_OnLoad

1.JNI_OnLoad 在 Android Native 开发中&#xff0c;JNI_OnLoad 是动态注册本地方法的标准入口点。以下是一个标准实现示例及其说明&#xff1a; JNI_OnLoad 标准实现 #include <jni.h> #include <string>// 声明本地方法对应的 C/C 函数 jint native_add(JNIEnv…

算法导论思考题

2-1 在归并排序中对小数组采用插入排序 c. 假定修改后的算法的最坏情况运行时间为 Θ \Theta Θ(nknlg(n/k))&#xff0c;要使修改后的算法与标准的归并排序具有相同的运行时间&#xff0c;作为n的一个函数&#xff0c;借助 Θ \Theta Θ记号&#xff0c;k的最大值是什么&#…