Linux 系统编程
Linux 系统编程
通用 Makefile 文件
- 通用 Makefile 文件的代码如下:
1 |
|
目录相关操作
获取当前工作目录
- 我们可以调用库函数
getcwd()
获取当前工作目录的绝对路径:
- 如果传入的
buf
为NULL
,且size
为0
,则getcwd()
会调用malloc
申请合适大小的内存空间,填入当前工作目录的绝对路径,然后返回malloc
申请的空间的地址。- 注意:
getcwd()
不负责free
申请的空间,free
是调用者的职责。
- 注意:
- 代码:
1 |
|
- 也可以与库函数
error()
来联动进行错误处理,从下图中的数据手册可以看到getcwd()
出错的时候会设置errno
这个参数:
- 库函数
error()
的用法:
- 代码如下:
1 |
|
- 运行结果:
改变当前工作目录
- 我们可以调用库函数
chdir()
改变当前工作目录的绝对路径:
- 代码:
1 |
|
- 注意:当前工作目录是进程的属性,也就是说每一个进程都有自己的当前工作目录。且父进程创建 (
fork
)子进程的时候,子进程会继承父进程的当前工作目录。 - 运行结果:
创建目录
mkdir()
函数可以用来创建目录。
- 代码:
1 |
|
- 当不知道
mode_t
的类型是什么的时候,可以使用以下方法来查看:
1 |
|
- 运行结果:
删除空目录
rmdir()
可以删除空目录。
- 代码:
1 |
|
目录流
- 使用目录流,可以查看目录中的内容。
- 流模型:“流"类似"水流”,顺序访问流中的数据时,是不需要关注位置的。
- 目录流与文件流相比,文件流中的基本单位是字符或字节。而目录流中的基本单位是目录项。如下图所示:
打开目录流
opendir()
可以打开一个目录,得到一个指向目录流的指针DIR*
。
关闭目录流
closedir()
关闭目录流。
读取目录流
readdir()
读目录流,得到指向下一个目录项的指针。
- 结构体
dirent
的定义:
- 读取一个目录内的内容并打印的代码:
1 |
|
递归地打印目录
- 实现青春版
tree
命令:输出内容分为三部分- 1)目录的名字;
- 2)递归打印每一个目录项;
- 3)最后是统计信息。
- 代码:
1 |
|
- 效果:
文件相关操作
打开文件
- 系统调用
open()
打开文件。- 打开成功:返回新的文件描述符(最小可用的文件描述符);
- 打开失败:返回
-1
,设置errno
。
- 部分
flags
的含义:
- 代码:
1 |
|
- 通过系统调用
open()
的过程介绍内核管理文件的数据结构:- 文件描述符表的数组长度默认是 1024,并且可以进行设置:
- 系统调用
open()
的内核管理文件流程:
关闭文件
- 系统调用
close()
关闭文件。- 关闭成功:返回
0
; - 关闭失败:返回
-1
,设置errno
。
- 关闭成功:返回
- 系统调用
close()
的内核管理文件流程:
读取文件
- 系统调用
read()
读取文件。- 读取成功:返回实际读取的字节数目(
0
,表示读取的起始位置在文件末尾); - 读取失败:返回
-1
,设置errno
。
- 读取成功:返回实际读取的字节数目(
- 系统调用
read()
的内核管理文件流程:
写入文件
- 系统调用
write()
读取文件。- 写入成功:返回实际写入字节数目(其中
n≤count
); - 写入失败:返回
-1
,设置errno
。
- 写入成功:返回实际写入字节数目(其中
- 系统调用
write()
的内核管理文件流程:
修改文件偏移量
- 系统调用
lseek()
修改文件偏移量,本质上就是修改current file offset (pos)
的数值。- 修改成功:返回文件的位置(距离文件开头的字节数目);
- 修改失败:返回
-1
,设置errno
。
- 系统调用
lseek()
的内核管理文件流程:
- 文件库函数和文件系统调用之间的关系:
文件描述符和文件流的异同
- 文件描述符的系统调用与文件流的函数:
- 文件描述符和文件流的数据访问流程:
同步内存中所有已修改的文件数据到储存设备
- 函数
fsync()
同步内存中所有已修改的文件数据到储存设备。- 将和文件描述符相关联的脏页刷新到磁盘。
- 刷新成功:返回
0
; - 刷新失败:返回
-1
并设置errno
。
修改文件长度
- 函数
ftruncate()
修改文件长度为length
个字节大小。 - 用法如下:
- 修改文件的长度有两种情况:
- 情况二可能会出现文件空洞的情况,此时数据全为
0
的页不会分配磁盘空间。
- 情况二可能会出现文件空洞的情况,此时数据全为
- 使用代码示例:
1 |
|
- 关于文件空洞的测试:
获取文件状态信息
- 函数
fstat()
用于获取文件状态信息。- 获取成功:返回
0
; - 获取失败:返回
-1
,设置errno
。
- 获取成功:返回
- 其具体用法和状态信息结构体如下:
- 代码示例:
1 |
|
- 打印结果:
文件描述符的复制
- 系统调用
dup()
用于对文件描述符的复制。- 复制成功:返回新的文件描述符;
- 复制失败:返回
-1
,设置errno
。
- 其具体用法如下,分为
dup()
和dup2()
两个系统调用:
- 系统调用
dup()
的内核管理文件流程:
- 使用
dup()
函数实现重定向:
1 |
|
- 使用
dup2()
函数实现重定向:
1 |
|
- 分别执行完两个代码的运行结果:
- 第一个代码重定向
STDERR_FILENO
文件描述符到application.log
文件,写入一段语句; - 第二个代码同样重定向
STDERR_FILENO
文件描述符到application.log
文件,写入一段语句。
- 第一个代码重定向
零拷贝(mmap)
- 系统调用零拷贝(
mmap()
)能够省去内核态和用户态的内存拷贝。
- 系统调用零拷贝(
mmap()
)的工作原理:- 相当于将文件的一部分内存通过
I/O
操作拷贝到物理内存中,并且内核态和用户态共用这部分内存,不再需要重复拷贝。
- 相当于将文件的一部分内存通过
- 系统调用零拷贝(
mmap()
和munmap()
)的使用方法:
- 系统调用零拷贝(
mmap()
和munmap()
)的返回值:
- 系统调用零拷贝(
mmap()
和munmap()
)的复制大文件使用场景:
- 代码示例:
1 |
|
CPU 的虚拟化
前置知识
- 内核的职责:管理硬件资源
- 共享资源的方式:
- 时分共享(CPU)
- 空分共享(内存)
- 操作系统通过让一个进程运行一段时间,然后切换到其它进程,缺点是会造成性能损失(需要进行上下文切换)。
- 如何实现 CPU 的时分共享?
- 底层机制:如何进行上下文切换
- 上层策略:调度策略
认识进程
- 用户角度:进程就是正在执行的程序
- 内核角度:要执行的任务(
struct task_t
)- 进程之间必须隔离,进程之间是相互看不到的,感知不到另外的进程存在。
- 以进程的角度看,就像它独占计算机的所有资源(抽象机制:CPU 的虚拟化)。
xv6
操作系统的进程相关结构体:
底层机制:实现上下文切换的三种方式及优缺点
- 指标:
- 性能:不应该增加太多的系统开销
- 控制权:操作系统应该保留控制权
方式一:直接运行(无限制)
- 优点:简单、快
- 缺点:没有控制权、不安全。
- 如何限制应用程序的权限?
- 不能让用户态应用程序访问非法的内存空间和执行一些特权指令。
- 需要硬件的协助即 CPU 的模态(模式):
- 用户态:应用程序(不能访问非法的内存空间和执行一些特权指令)
- 内核态:操作系统(可以访问机器的所有资源)
方式二:受限直接运行协议
- 应用程序如何执行特权操作?
- 通过系统调用!
- 通过特殊指令
trap
来切换用户态和内核态:- 缺点在于控制权不是主动掌握在操作系统上。
方式三:受限直接运行协议(时钟中断)
- 方式二采用协作(
yield()
)的方式,等待系统调用。 - 方式三采用非协作方式(抢占方式),操作系统能够主动进行控制。
- 时钟中断的方式:
- 受限直接运行协议(时钟中断):
- 进程之间是隔离的(感知不到内核和其他进程的存在)。
- 进程是资源分配的最小单位(任务<-分配资源)。
- 上下文切换:
- 调用系统调用
- 切换进程
和进程相关的常用命令
显示进程
ps
命令显示和终端关联的进程:
ps x
显示和用户关联的进程:
ps aux
显示所有用户相关的进程:
top
每 3 秒统计一次进程信息:
pstree
打印进程树:
- 前台进程:
- 后台进程:
获取进程的标识
- 获取进程的标识的用法:
- 代码示例:
1 |
|
- 运行结果:
Linux
进程id
的分配策略:
进程的基本操作
- 创建进程:
fork()
- 终止进程:
exit()
、_exit()
、abort()
、wait()
、waitpid()
- 执行程序:
exec
函数簇
创建进程:fork()
- 系统调用
fork()
的用法:
-
系统调用
fork()
的返回值:- 成功:
- 父进程:子进程的
pid
- 子进程:
0
- 父进程:子进程的
- 失败:
- 父进程:
-1
,并且不会创建子进程,设置errno
。
- 父进程:
- 成功:
-
惯用法代码:
1 |
|
- 测试结果:
- 到底是父进程先执行还是子进程先执行是不确定的(不能假定到底是谁先执行)。
- 系统调用
fork()
的原理:
代码段:父子进程共享(不能修改)
- 栈、堆、数据段(父子进程私有)
- 测试代码:
1 |
|
- 测试结果:
用户态缓冲区(文件流):父子进程是私有的
- 测试代码:
1 |
|
- 测试结果:
- 用户缓冲区父子进程私有经典题目一:
- 总共输出 24 个
a
。
- 总共输出 24 个
- 用户缓冲区父子进程私有经典题目二:
- 总共输出 14 个
a
。
- 总共输出 14 个
打开文件(共享的)、文件描述符列表(私有的)
- 对于父子进程,打开文件(共享的)、文件描述符列表(私有的)
- 测试代码:
1 |
|
- 测试结果:
终止进程
- 基本概念:
正常终止
- 库函数
exit()
的步骤以及系统调用atexit()
的用法和返回值:
- 示例代码:
1 |
|
- 测试结果:
- 系统调用
_exit()
的用法:- 退出状态码传至操作系统。
- 测试程序:
1 |
|
- 测试结果:
异常终止
- 系统调用
abort()
的用法:
- 测试代码:
1 |
|
- 测试结果:
- 内核给该进程发送
SIGABRT
信号
孤儿进程和僵尸进程
- 孤儿进程:子进程存活,父进程终止了
- 测试代码:
1 |
|
- 测试结果:
- 分析:孤儿进程会被
1
号进程(init
进程)收养,该进程一直循环执行wait
函数。
- 僵尸进程:子进程死亡时,有一些信息会保存在内核(
pid
、退出状态、CPU 时间…),方便父进程以后查看这个信息,并且给父进程发送SIGCHLD
信号,但父进程默认会忽略信号。- 如何给僵尸进程收尸:
wait
、waitpid
。
- 如何给僵尸进程收尸:
wait()
- 系统调用
wait()
的用法和返回值:
- 测试程序:
1 |
|
- 测试结果:
waitpid()
- 系统调用
waitpid()
的用法:
- 系统调用
waitpid()
的返回值:
- 测试程序:
1 |
|
- 测试结果:
exec 函数簇
环境变量 env 的定义:
- 打印环境变量
env
测试程序:
1 |
|
- 测试结果:
exec 函数簇
exec
函数簇的用法:
exec
函数簇的测试程序:
1 |
|
- 测试结果:
exec
现象和原理:- 从上图可以看到,
pid
和ppid
没有改变,因此没有创建新的进程,并且在新的可执行程序mian
函数的第一行开始执行。 - 原理在于:
- 执行
exec
函数簇会清除进程的代码段、数据段、堆、栈、上下文; - 加载新的可执行程序(设置代码段、数据段);
- 从新的可执行程序
mian
函数的第一行开始执行。
- 执行
- 从上图可以看到,
- 清除进程的代码段、数据段、堆、栈、上下文如下图所示:
system 的实现和惯用法
system()
系统调用的用法:
- 简易
system()
的实现代码:
1 |
|
- 惯用法说明:
Linux 系统编程
http://example.com/2025/05/18/linux_program/