各位看官,本篇博客干货满满,请耐下心来,慢慢吸收!哈哈哈,内功一定会大增!
目录
一、printf 函数输出问题
1.1 第1个示例代码
1.2 第2个示例代码
1.3 分析与结论
二、主函数参数介绍
三、复制进程 fork
3.1 进程的基本概念
3.2 fork()方法
3.3 fork方法使用示例
3.4 面试题fork()
3.4.1 经典面试题1
3.4.2 经典面试题2
3.4.3 经典面试题3
3.5 逻辑地址 物理地址
3.6 写时拷贝技术
3.6.1 概念
3.6.2 写时拷贝的工作原理
3.6.3 写时拷贝的优势
3.6.4 写时拷贝的应用
四、僵死进程及处理方法
4.1 僵死进程是什么
4.2 僵死进程的危害
4.3 如何解决僵死进程
五、从进程的视角看堆区内存申请与释放的有关问题
5.0 虚拟内存
5.1 申请内存不进行释放
5.2 malloc 申请 1G 的内存空间是否能成功
5.3 在物理内存只有 2G 的系统中,malloc 能否申请 2G 空间,怎么思考
5.4 当前计算机物理内存有2G,虚拟内存有2G,一共4个G,申请3G空间呢能申请成功吗?
5.5 当前物理内存2G,关闭虚拟内存,还能使用2G吗?
5.6 malloc 与 fork,父进程堆区申请的空间复制后,子进程也会有一份,也需要释放?
六、系统调用与库函数的区别
6.1 系统调用
6.2 库函数
6.3 二者对比
6.4 实例
七、操作文件的系统调用
7.0 文件描述符的概念
7.1 open打开文件系统调用
7.2 read读文件系统调用
7.3 write写文件系统调用
7.4 close关闭文件系统调用
7.5 文件的基本使用
7.6 文件操作与fork()
八、进程替换
一、printf 函数输出问题
1.1 第1个示例代码
测试1
运行现象:先睡眠5秒,在打印hello,然后结束程序。
1.2 第2个示例代码
测试2
运行现象:先打印出hello,再睡眠5秒,最后退出程序。
1.3 分析与结论
造成上述现象的原因在于:在执行printf函数时,会先将打印的内容放入缓冲区(因为从用户态切切换到内核态要消耗时间),等达到刷新缓冲区的条件时,才会把打印的内容交给内核,内核最后打印到屏幕上。缓冲区相当于是一个字符数组。
printf 函数并不会直接将数据输出到屏幕,而是先放到缓冲区中,只有以下三种情况满足之一,才会刷新缓冲区,输出到屏幕。
1) 缓冲区放满
2) 强制刷新缓冲区 fflush、\n
3) 程序结束时 exit(0) 、exit(1)... 1,2,3错误码 0成功
二、主函数参数介绍
在Linux操作系统下,主函数(main函数)的参数通常用于接收从命令行传递给程序的输入。这些参数有三个:argc
和argv和envp
。它们的定义如下:
int main( int argc, char* argv[], char* envp[]);
参数详解
argc
:
- 类型:
int
- 代表命令行参数的数量。
- 包括程序名在内,传递给程序的参数总数。
argc
至少为1,因为程序名总是第一个参数。
argv
:
- 类型:
char* argv[]
或char** argv
- 是一个字符指针数组,每个元素都是指向一个字符串的指针。
argv[0]
指向程序名(包含路径的程序名,或者是只包含程序名,取决于如何运行程序)。- 其余的元素(从
argv[1]
到argv[argc-1]
)指向传递给程序的命令行参数。argv[argc]
是一个空指针(NULL),标志参数列表的结束。
3、envp: envp 环境变量
- 类型:
char* envp[]
或char** envp
- 是一个字符指针数组,每个元素都是指向一个字符串的指针。
- 每个值代表一个环境变量
- 最后一个为一个空指针(NULL),标志参数列表的结束。
三、复制进程 fork
3.1 进程的基本概念
进程可以简单理解为:一个正在运行的程序,程序本身是存放在磁盘上的,当我们运行程序时,就会将程序加载到内存,操作系统通过进程控制块(PCB)来管理进程的,每产生一个进程,就会对应有一个进程控制块PCB,其中PID唯一标识一个进程,PCB是一个结构体 struct task_struct ,它描述了进程的相关属性,包括程序计数器PC指针、PID、退出码等等,具体存放是:把每个进程控制块(PCB)放入链表中,通过PID来区分PCB,每一个进程都由PCB来管理。
3.2 fork()方法
利用man 2 fork 命令查看该系统调用的详细信息如下:
fork()
是一个系统调用,用于创建当前进程的一个副本。新创建的进程被称为子进程(child process),而原始进程被称为父进程(parent process)。子进程是父进程的一个几乎完全相同的副本。子进程继承了父进程的所有数据、代码、打开的文件描述符等。它的返回值是一个整数pit_t , 在父进程中返回子进程的 pid,在子进程中返回 0,失败返回-1。此外,它的取值是有限的,当我们每创建一个进程,就会产生一个管理该进程的进程控制块PCB,就会为其分配对应的PID, 因此,PID的数量会决定我们可以创建进程的个数,如果PID用完了,那么就无法创建新进程,同理释放进程,首先把进程本身的空间释放,再把对应的进程控制块PCB空间释放(结构体释放),再归还PID号,达到PID的复用。
注意:
- fork复制出来的子进程,也会同父进程在当前位置fork()调用结束后执行程序,前面的程序不会执行,但变量的值等与父进程相同(因为它是拷贝过来的)。
- fork执行完才会产生子进程。
3.3 fork方法使用示例
3.4 面试题fork()
3.4.1 经典面试题1
int main()
{fork() || fork();printf("A\n");exit(0);
}
3.4.2 经典面试题2
int main(int argc, char* argv[], char* envp[])
{int i = 0;for (; i < 2; i++){fork();printf("A\n");}exit(0);
}
初始状态
- 开始时,有一个父进程。
第一次循环 (i = 0)
- 初始时只有一个父进程。
- 调用
fork()
,创建一个子进程。
- 现在有两个进程:一个父进程和一个子进程。
- 每个进程执行
printf("A\n")
。
- 打印两个
A
。第二次循环 (i = 1)
- 目前有两个进程(一个父进程,一个子进程)。
- 每个进程再次调用
fork()
,每个进程都创建一个新的子进程。
- 现在有四个进程。
- 每个进程执行
printf("A\n")
。
- 每个进程打印一个
A
,共打印四个A
。总结
第一次循环:
- 1个初始进程变成2个进程。
- 每个进程打印一个
A
。- 打印2个
A
。第二次循环:
- 2个进程变成4个进程。
- 每个进程打印一个
A
。- 打印4个
A
。所以,总共打印的
A
数量是: 2(第一次循环) + 4(第二次循环) = 6 个A
。
3.4.3 经典面试题3
int main(int argc, char* argv[], char* envp[])
{int i = 0;for (; i < 2; i++){fork();printf("A");}exit(0);
}
3.5 逻辑地址 物理地址
先看一个例子,还是上面的进程实例,只不过我在父子进程同时打印变量n和n的地址。
从上面可以看到,两个进程的n的值不同,但是它的地址相同,也就是说:难道它们占用的是同一块内存空间吗?但是同一块内存空间不可能能够写入两个值呀???!这是为什么呢?这就是我们要说的逻辑地址和物理地址!
逻辑地址(Logical Address)
它是由CPU生成的地址,又称虚拟地址(Virtual Address)。它表示程序在其虚拟地址空间中的地址。逻辑地址是用户程序使用的地址,它不直接对应物理内存中的具体位置。
特点:
- 由CPU生成:每当程序指令访问内存时,CPU会生成一个逻辑地址。
- 独立于物理内存:逻辑地址在程序的虚拟地址空间内,它不直接映射到物理内存中的具体位置。
- 通过地址转换:逻辑地址通过地址转换机制(由MMU,即内存管理单元处理)映射到物理地址。
物理地址(Physical Address)
物理地址(Physical Address)是实际存在于计算机内存硬件中的地址。它表示数据在物理内存中的具体位置。
特点:
- 由硬件使用:物理地址是内存单元在实际物理内存(RAM)中的位置,内存控制器使用这些地址来访问内存。
- 直接映射:物理地址直接对应于硬件内存位置。
- 映射自逻辑地址:通过内存管理单元(MMU),逻辑地址被转换为物理地址。
3.6 写时拷贝技术
3.6.1 概念
写时拷贝是一种延迟复制技术。在操作系统中,当一个进程创建一个新进程(通常通过
fork()
系统调用)时,父进程和子进程通常需要共享相同的内存内容。为了避免不必要的内存复制,写时拷贝机制允许父进程和子进程初始时共享相同的物理内存页。只有当一个进程尝试修改共享内存页时,才会真正复制该内存页。这样可以显著减少内存复制的开销,提高系统效率。
3.6.2 写时拷贝的工作原理
进程创建:
- 当父进程通过
fork()
创建一个子进程时,子进程获得父进程的虚拟地址空间的副本,但这些地址空间中的物理内存页是共享的。- 操作系统将这些共享的物理内存页标记为只读。
页面共享:
- 父进程和子进程继续执行时,任何对共享内存页的读取操作都可以直接使用同一物理内存页,不需要额外的复制。
写入检测:
- 当其中一个进程试图写入一个共享的只读页面时,硬件内存管理单元(MMU)检测到这个写操作并触发一个页错误(Page Fault)。
- 操作系统处理这个页错误,创建共享页面的一个副本,并将这个副本分配给执行写操作的进程。
- 修改的进程将写操作应用到新的物理页面,而未修改的进程仍然使用原始的只读页面。
页面更新:
- 通过这种方式,只有在需要修改页面内容时,才会进行实际的页面复制,极大地减少了不必要的内存复制操作。
3.6.3 写时拷贝的优势
- 提高效率:避免了在创建新进程时立即复制整个进程地址空间的开销,减少了内存和CPU时间的消耗。
- 资源节约:通过共享未修改的内存页,减少了物理内存的使用量。
- 延迟复制:仅在实际需要修改时才进行内存页的复制,从而推迟了内存开销。
3.6.4 写时拷贝的应用
- 进程创建:在操作系统中,
fork()
系统调用通常配合写时拷贝使用。当进程调用fork()
时,新进程初始时共享父进程的内存页,直到需要写入时才进行实际的页面复制。- 内存分配:某些高级内存分配机制也使用写时拷贝,以提高内存分配和管理的效率。例如,某些虚拟内存系统在分配大块内存时初始时不实际分配物理内存,而是利用写时拷贝机制按需分配。
四、僵死进程及处理方法
4.1 僵死进程是什么
僵死状态(Zombies)是一个比较特殊的状态。当子进程已经终止并且父进程(使用wait()系统调用,后面讲) 没有读取到子进程退出的返回代码时就会产生僵死(尸)进程,僵死进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码。所以,只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程进入僵死状态。
僵死进程的特征
- 终止但未清理:僵死进程已经完成其执行并退出,但其进程描述符仍然存在于系统的进程表中。
- 占用资源:虽然僵死进程不再使用 CPU 和内存资源,但它占用一个进程表项。
- 状态标记:在系统工具如
ps
命令中,僵死进程通常会显示为<defunct>
状态。
Linux的机制对于进程的管理机制为:子进程先结束,退出码保存到PCB中,如果父进程不获取PCB中的退出码,这个结构体就不会删除,但还能看到子进程,但子进程执行不了,因为它已经结束了。
4.2 僵死进程的危害
进程的退出状态必须被维持下去,因为他要告诉关心它的进程(父进程),你交给我的任务,我办的怎么样了。可父进程如果一直不读取,那子进程就一直处于僵死状态?是的! 维护退出状态,本身就是要用数据维护,也属于进程基本信息,所以保存在task_struct(PCB)中,换句话 说,僵死状态一直不退出,PCB一直都要维护?是的! 那一个父进程创建了很多子进程,就是不回收,是不是就会造成内存资源的浪费?是的!因为数据结构对象本身就要占用内存,想想C中定义一个结构体变量(对象),是要在内存的某个位置进行开辟空间! 内存泄漏?是的!
4.3 如何解决僵死进程
- 调用
wait()
或waitpid()
:父进程应及时调用这些函数来读取子进程的退出状态并清理其进程表项。- 重启父进程:在某些情况下,如果父进程被设计得不好且未能清理僵死进程,重启父进程可以帮助系统自动清理这些僵死进程。
- 编写信号处理函数:父进程可以捕捉 SIGCHLD 信号,并在信号处理函数中调用
wait()
或waitpid()
来处理僵死进程。
这里展示利用方法1解决僵死进程问题。
五、从进程的视角看堆区内存申请与释放的有关问题
5.0 虚拟内存
虚拟内存是计算机操作系统中的一种技术,它通过将磁盘空间用作临时存储来扩展可用的物理内存。在使用虚拟内存的系统中,每个程序都被分配了一块连续的虚拟地址空间,而不需要考虑实际的物理内存大小或位置。这样,即使物理内存不足,程序也可以继续执行,因为虚拟内存可以将部分数据暂时存储到磁盘上,直到需要时再重新加载到物理内存中。这样便为物理内存节省了空间!访问物理内存通常比访问虚拟内存快得多,因为物理内存直接位于处理器和其他硬件组件之间。而访问虚拟内存需要通过磁盘 I/O 操作,速度较慢。
5.1 申请内存不进行释放
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <assert.h>int main()
{char * s = (char*)malloc(128);assert(s != NULL);exit(0);
}
分析上述代码是否存在内存泄漏?
内存泄漏是指在计算机程序中,动态分配的内存由于程序错误未能释放,导致这些内存块无法被再利用。即便程序不再需要这些内存,它们也无法被操作系统或程序本身重新分配或使用,从而引发内存资源的浪费。这种现象在长时间运行的程序中尤其显著,可能最终导致系统内存耗尽,程序崩溃或系统变慢。
在C语言中,我们知道动态申请的内存,使用完毕,必须要进行释放,否则,会存在内存泄漏,其实这是不严谨的,在这个程序中,虽然没有释放动态内存,但是进程结束后,系统会将申请的动态内存回收,但是如果在服务器上,运行时间特别长,动态申请的内存一直不释放,它便会占用堆区内存空间,且其他程序无法使用,这个时候便会造成内存泄漏!在写程序时养成良好的内存管理习惯仍然很重要。显式地释放分配的内存不仅有助于保持代码的清晰和正确,还能在更复杂的程序中避免潜在的问题!
5.2 malloc 申请 1G 的内存空间是否能成功
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <assert.h>int main()
{char * s = (char*)malloc(1024*1024*1024);assert(s != NULL);exit(0);
}
对于32位的操作系统,用户最多使用3G空间,因为总共4G空间,内核使用1G空间,只剩下3G,甚至可能不到3G,因为栈要用,最底下的空间也没使用,也就2G多,按正常来说,是可以申请成功的。
5.3 在物理内存只有 2G 的系统中,malloc 能否申请 2G 空间,怎么思考
在一个物理内存只有 2GB 的系统中, ma11oc() 是否能够成功申请 2GB 的空间取决于多个因素:
1.系统剩余内存:即使物理内存总量为 2GB,但系统中可能还有其他进程正在使用内存。如果系统中剩余的可用内存不足以满足ma11oc0) 请求,那么申请 2GB 的空间是不可能的。
3.虚拟内存:即使物理内存不足以满足请求,操作系统也可以通过虚拟内存来模拟更大的内存空间。虚拟内存允许操作系统将部分数据存储在硬盘上,从而释放物理内存供其他进程使用。但是,虚拟内存的使用可能会导致性能下降,因为访问硬盘上的数据比访问内存中的数据要慢得多。
因此,即使系统中的物理内存总量为 2GB, 也不一定能够成功电请 2GB,的空间,要确保分配的内存量不超过系统的物理内存限制,并且要考虑到其他正在运行的进程可能会使用的内存量。
5.4 当前计算机物理内存有2G,虚拟内存有2G,一共4个G,申请3G空间呢能申请成功吗?
不能。因为物理内存有2G,虚拟内存有2G,但内核要使用一些,物理内存实际只有1.7G,加上虚拟内存总共只有3.7G,但用户最多使用3G,甚至不到3G,栈,代码段,数据段,最下面空间占用一些,所以只有2G多,根本申请不了3G。
5.5 当前物理内存2G,关闭虚拟内存,还能使用2G吗?
不能。因为关闭了虚拟内存,虚拟内存上的东西会拷回,物理内存,没有了虚拟内存支持,不能使用2G空间。所以在我们申请空间时,看我们剩余的物理内存能否满足需求,满足不了,看是否开启虚拟内存,开启了,看物理内存加上虚拟内训是否能满足。如果申请内存超过3G或者3G,即使物理内存加上虚拟内存够用也申请不了。因为用户实际使用2G多。
5.6 malloc 与 fork,父进程堆区申请的空间复制后,子进程也会有一份,也需要释放?
是的,子进程的这份空间也需要释放。在操作系统中,当一个进程使用系统调用(例如 fork
)创建一个子进程时,子进程会得到父进程的一个几乎完全相同的拷贝,包括堆区中的数据。由于父子进程各自拥有自己的独立地址空间,这些空间并不共享,子进程中的这份堆区数据和父进程中的数据实际上是两个独立的副本。因此,子进程在不再需要使用这些堆区数据时,需要释放这部分空间,以避免内存泄漏。通常,子进程会按照和父进程类似的方式释放这些资源,例如使用 free
函数来释放用 malloc
或类似函数分配的内存。总结来说,父进程和子进程各自管理自己的内存,即使这些内存是在子进程创建时从父进程拷贝过来的,子进程也需要自己负责释放这部分内存。
六、系统调用与库函数的区别
6.1 系统调用
系统调用是程序向操作系统内核请求服务的接口。它们允许用户空间的程序与内核进行交互,从而访问硬件资源和系统服务。系统调用是操作系统内核的一部分,是执行关键任务(如文件操作、进程管理、内存管理等)的主要手段。
特点
- 特权级别:系统调用运行在内核态,具有最高权限,可以直接访问硬件资源。
- 接口:系统调用通过操作系统提供的接口来实现,通常需要通过软件中断或陷入指令来切换到内核态。
- 性能开销:由于需要在用户态和内核态之间进行切换,系统调用相对于普通函数调用有较大的性能开销。
- 安全性:因为直接与内核交互,系统调用需要严格的权限控制和参数验证。
示例:在 Unix/Linux 系统中,read
和 write
是常见的系统调用。它们的原型分别如下:
ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void *buf, size_t count);
系统调用执行过程如下图:
6.2 库函数
库函数是由程序库(例如标准 C 库)提供的函数,用于实现常见的编程任务。它们通常是对系统调用的封装,也可能是纯用户态的实现。
特点
- 特权级别:库函数运行在用户态,没有直接访问硬件资源的权限。
- 接口:库函数由编程语言的标准库或第三方库提供,调用时不需要进行用户态和内核态的切换。
- 性能开销:库函数的调用开销较小,因为它们通常不需要上下文切换。
- 灵活性:库函数可以实现更复杂的功能,通常通过组合多个系统调用或纯用户态的计算实现。
示例:在标准 C 库中,printf
和 malloc
是常见的库函数。它们的原型分别如下:
int printf(const char *format, ...);
void *malloc(size_t size);
6.3 二者对比
执行环境:
- 系统调用:内核态
- 库函数:用户态
性能:
- 系统调用:较高的性能开销(因为需要从用户态切换到内核态)
- 库函数:较低的性能开销(在用户态内执行)
权限:
- 系统调用:高权限,可以访问硬件和系统资源
- 库函数:低权限,只能执行用户态内的操作
实现:
- 系统调用:由操作系统内核提供
- 库函数:由标准库或第三方库提供,可能是对系统调用的封装
使用场景:
- 系统调用:直接与操作系统内核交互的操作,如文件读写、进程控制等
- 库函数:常用的编程操作,如字符串处理、内存管理、格式化输出等
6.4 实例
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>int main() {// 使用系统调用 open, read, writeint fd = open("example.txt", O_RDONLY);if (fd == -1) {perror("open");exit(EXIT_FAILURE);}char buffer[128];ssize_t bytesRead = read(fd, buffer, sizeof(buffer) - 1);if (bytesRead == -1) {perror("read");close(fd);exit(EXIT_FAILURE);}buffer[bytesRead] = '\0'; // Null-terminate the bufferif (write(STDOUT_FILENO, buffer, bytesRead) == -1) {perror("write");close(fd);exit(EXIT_FAILURE);}close(fd);// 使用库函数 printf 和 mallocprintf("This is a library function example.\n");char *dynamicBuffer = (char *)malloc(128 * sizeof(char));if (dynamicBuffer == NULL) {perror("malloc");exit(EXIT_FAILURE);}snprintf(dynamicBuffer, 128, "Allocated memory and wrote this string.\n");printf("%s", dynamicBuffer);free(dynamicBuffer);return 0;
}
七、操作文件的系统调用
在C语言中,fopen
、fread
、fwrite
、fclose
和fgets
这些函数属于标准I/O库(stdio.h),而open
、read
、write
、close
这些函数属于系统调用(syscall)。它们在使用上的主要区别在于以下几个方面:头文件 #include <fcntl.h>
1. 抽象层次
- 标准I/O库函数:
fopen
、fread
、fwrite
、fclose
和fgets
提供了一个更高层次的抽象,通常使用缓冲区来提高文件操作的效率。它们通过标准库封装了底层系统调用,提供了一些高级特性,如缓冲区管理、格式化输入输出等。- 系统调用:
open
、read
、write
、close
直接与操作系统内核交互,不进行任何缓冲操作,直接对文件描述符进行操作,通常用于更低级的文件操作。2. 缓冲机制
- 标准I/O库函数:使用缓冲区(buffer)来减少实际I/O操作的次数。例如,
fread
和fwrite
可能会一次性读写比实际请求更多的数据,从而减少系统调用的频率,提高性能。- 系统调用:不使用缓冲区。每次调用
read
和write
都会直接导致系统调用,数据直接从用户空间传递到内核空间或从内核空间传递到用户空间。
标准I/O库函数:fopen
返回一个 FILE*
指针,通过这个指针可以使用标准I/O库函数进行文件操作。系统调用:open
返回一个文件描述符(整数),通过这个文件描述符可以使用系统调用进行文件操作。
7.0 文件描述符的概念
文件描述符(File Descriptor, FD)是操作系统内核为每一个打开的文件、套接字或其他 I/O 资源分配的一个整数标识符。在 UNIX 和类 UNIX 操作系统(如 Linux、macOS)中,文件描述符是进行文件操作(如读、写、关闭)时使用的基本抽象概念。
1. 文件描述符的概念
- 整数标识符:文件描述符是一个简单的整数,通常从0开始分配。每个进程有自己独立的文件描述符表。
- 抽象层次:文件描述符抽象了底层的文件系统和设备,使程序可以统一地处理不同类型的 I/O 操作。
- 资源句柄:文件描述符不仅用于文件,还用于套接字、管道、设备等各种 I/O 资源。
2. 文件描述符的范围
- 标准文件描述符:
0
:标准输入(stdin)1
:标准输出(stdout)2
:标准错误(stderr)- 动态分配:打开新的文件或创建新的 I/O 资源时,操作系统会分配一个未使用的最小整数值作为文件描述符。
3. 文件描述符的生命周期
- 打开文件:
- 使用系统调用
open
打开文件,返回一个文件描述符。- 示例:
int fd = open("example.txt", O_RDONLY);
- 操作文件:
- 使用文件描述符进行读、写等操作。
- 示例:
read(fd, buffer, size);
- 关闭文件:
- 使用系统调用
close
关闭文件描述符,释放相关资源。- 示例:
close(fd);
4. 文件描述符表
- 每个进程维护一个文件描述符表,记录文件描述符和实际文件或设备的对应关系。
- 文件描述符表条目包含文件状态信息,如文件位置、打开模式等。
7.1 open打开文件系统调用
int open(const char* pathname, int flags);//用于打开一个已存在的文件
int open(const char* pathname, int flags,mode_t mode);//用于新建一个文件,并设置访问权限
1. 功能
- 打开文件系统调用用于在文件系统中打开一个文件,并返回一个称为文件描述符的整数值。文件描述符是操作系统用来标识已打开文件的一种方式。
- 通过文件描述符,可以对文件进行读取、写入、定位等操作。
2. 参数
- 文件路径:需要打开的文件的路径,可以是绝对路径或相对路径。
- 访问模式:指定打开文件的方式,常见的包括只读、只写、读写等。打开标志如下:
- O_WRONLY 只写打开
- O_RDONLY 只读打开
- O_RDWR 读写方式打开
- O_CREAT 文件不存在则创建
- O_APPEND 文件末尾追加
- O_TRUNC 清空文件,重新写入
- 权限模式(可选):权限 如:“0600”,在新建一个文件时必须指定,还要在第二个参数后面加|0_CREAT
3. 返回值
- 如果成功打开文件,系统调用返回一个非负整数作为文件描述符。
- 如果出现错误,则返回一个特殊的值,通常是-1,表示打开文件失败。
7.2 read读文件系统调用
ssize_t read(int fd, void* buf, size_t count);
1. 功能
- 读文件系统调用用于从指定的文件描述符关联的文件中读取数据,并将这些数据存储在用户提供的缓冲区中。
2. 参数
- 文件描述符(
fd
):对应打开的文件描述符。- 缓冲区(
buf
):存放数据的空间- 读取的字节数(
count
):计划一次从文件中读多少字节数据3. 返回值
- 成功时,返回实际读取的字节数(可能小于请求的字节数)。
- 如果读取到文件末尾,返回0。
- 如果出现错误,则返回-1,并设置适当的错误码。
7.3 write写文件系统调用
ssize_t write(int fd, const void* buf,size_t count);
1. 功能
- 写文件系统调用用于将缓冲区中的数据写入到指定的文件描述符关联的文件中。
2. 参数
- 文件描述符(
fd
):标识文件的整数值,由之前的打开文件系统调用(如open
)返回。- 缓冲区(
buf
):包含要写入数据的内存地址。- 写入的字节数(
count
):希望写入的字节数。3. 返回值
- 成功时,返回实际写入的字节数(可能小于请求的字节数)。
- 如果出现错误,则返回-1,并设置适当的错误码。
7.4 close关闭文件系统调用
int close(int fd);
1. 功能
- 关闭文件系统调用用于关闭由文件描述符标识的已打开文件。此操作会释放与文件相关的系统资源。
2. 参数
- 文件描述符(
fd
):要关闭的文件的整数标识符。3. 返回值
- 成功时,返回0。
- 如果出现错误,则返回-1,并设置适当的错误码。
7.5 文件的基本使用
7.6 文件操作与fork()
父进程先打开文件,之后再fork1出子进程,父进程和子进程是共享文件偏移量的。任何一个人关闭文件对其他人是没有影响的。想要真正关闭文件就需要父子进程都关闭这个文件。 文件打开就会创建一个struct file节点 计数器 文件偏移量 文件的属性 通过属性可以找到在磁盘上的文件。所以在fork时会把文件表也复制一份 子进程的元素也指向父进程的struct file,他们共享文件偏移量 计数器会变成2。
由于 fork 创建的子进程的 PCB 是拷贝父进程的,子进程的 PCB 中的文件表指向打开文 件的指针只是拷贝了父进程 PCB 中的值,所以父子进程会共享父进程 fork 之前打开的所有 文件描述符。如下图所示:
先fork产生子进程,父子进程有各自的struct file,偏移量不是共享的。
八、进程替换
在 Unix 系统中,exec
系列函数用于替换当前进程的代码段、数据段和堆栈段,即用一个新的程序替换当前进程的地址空间。在fork复制后,pid不变,把原来复制的进程实体扔掉,换上要替换进程的实体。 exec
系列函数包括以下几个常用的函数:
这些函数的基本过程是相同的,只是它们的参数形式有所不同。掌握一种即可。
exec
系列函数的不同变体
execl
和execlp
:
execl
接收多个参数,最后一个参数必须是NULL
。execlp
类似于execl
,但path
可以是程序名称或路径,会在PATH
环境变量中搜索。
execle
:
execle
类似于execl
,但可以传递新的环境变量数组envp
。
execv
和execvp
:
execv
接受一个参数数组argv
,数组的最后一个元素必须是NULL
。execvp
类似于execv
,但path
可以是程序名称或路径,会在PATH
环境变量中搜索。
execve
:
execve
最底层的exec
函数,接受参数数组argv
和环境变量数组envp
。
以 ps 替换当前程序为例,介绍 exec 系统函数使用,注意该系列方法功能都一样,没有 区别,只是参数不同:
需要注意:execl 执行成功不返回,直接从新程序的主函数开始执行,只有失败才 返回错误码
以上就是全部内容!请务必掌握,这是后续学习的基础,欢迎大家点赞加关注评论,您的支持是我前进最大的动力!下期再见!