Linux学习五(进程)
Linux学习五 : 进程
虚拟地址空间
虚拟地址空间
:虚拟内存将物理内存抽象为地址空间,每个进程都有各自地址空间。地址空间中页被映射到物理内存,地址空间的页并不需要全部在物理内存中,当使用到一个没有在物理内存的页时,执行页面置换算法,将该页置换到内存中。
虚拟地址空间的大小也由操作系统决定,32位的操作系统虚拟地址空间的大小为 2^32 字节,也就是4G,64位的操作系统虚拟地址空间大小为2 ^64 字节。
- 虚拟内存的目的是为了让物理内存扩充成更大的逻辑内存,从而让程序获得更多的可用内存。
- 为了更好的管理内存,操作系统将内存抽象成地址空间。每个程序拥有自己的地址空间,这个地址空间被分割成多个块,每一块称为一页。
从操作系统层级上看,虚拟地址空间主要分为两个部分内核区和用户区。
内核空间在3-4G在今后陪你过,驻留在内存中,是操作系统的一部分。系统中所有进程对应的虚拟地址空间的内核区都会映射到同一块物理内存上(系统内核只有一个)。
- 保留区:未赋予物理地址。NULL指向这块地址。
- 代码段:通常存放CPU指令和函数。
- 数据段:存放已初始化且初值不为0的全局变量和静态变量。数据段属于静态内存分配(静态存储区),可读可写。
- .bss段: 未初始化以及初始为0的全局变量和静态变量,操作系统会将这些未初始化变量初始化为0
- 堆:new出来的,需要释放。
- 内存映射区:作为内存映射区加载磁盘文件,或者加载程序运作过程中需要调用的动态库。
- 栈:存储函数内部声明的非静态局部变量,函数参数,函数返回地址等信息,栈内存由编译器自动分配释放。栈和堆相反地址“向下生长”,分配的内存是连续的。
文件描述符
Linux中一切都是文件,需要操作文件描述符进行读写操作。
Linux下进程启动就会得到一个对应的虚拟地址空间,进程控制块PCB中包含一个文件描述符表,用于存储文件描述符。
- 每一个进程对应的文件描述符表能够存储的打开的文件数是有限制的, 默认为1024个,这个默认值是可以修改的,支持打开的最大文件数据取决于操作系统的硬件配置。
- 当一个进程被启动之后,内核PCB的文件描述符表中就已经分配了三个文件描述符(标准输入stdin,标准输出stdout,标准错误stderr),这三个文件描述符对应的都是当前启动这个进程的终端文件。
- 文件描述符表中不同的文件描述符可以对应同一个磁盘文件。
open/close 可以打开关闭文件,open还可以创建新的文件。
read 函数用于读取文件内部数据,在通过 open 打开文件的时候需要指定读权限。
write 函数用于将数据写入到文件内部,在通过 open 打开文件的时候需要指定写权限。
系统函数 lseek 的功能是比较强大的, 我们既可以通过这个函数移动文件指针, 也可以通过这个函数进行文件的拓展。
file命令可以查看文件信息。
stat命令显示文件或目录的详细属性信息包括文件系统状态,比ls命令输出的信息更详细。
dup函数的作用是复制文件描述符,这样就有多个文件描述符可以指向同一个文件了。可以在进程中创建多个对同一文件的引用,可以对文件进行读取写入等操作。
dup2() 函数是 dup() 函数的加强版,基于dup2() 既可以进行文件描述符的复制, 也可以进行文件描述符的重定向。文件描述符重定向就是改变已经分配的文件描述符关联的磁盘文件。
fcntl() 是一个变参函数, 并且是多功能函数,可以通过这个函数实现文件描述符的复制和获取/设置已打开的文件属性。
opendir(), readdir(), closedir()。 readdir() 函数遍历目录中的文件信息。
scandir()函数进行目录的遍历(只遍历指定目录,不进入到子目录中进行递归遍历)。
文件同步
:Linux中,文件系统通常使用缓存区来提高文件读写性能,当程序对文件进行读写操作时,数据首先会被写入到内核的缓冲区中,而不是直接写入磁盘,这样可以减少磁盘IO的次数,提高文件读写的效率。但是如果出现意外崩溃或断电,还未写入磁盘的数据将会丢失,导致数据的不一致性或丢失,这时候就需要用文件同步来确保数据的持久性和一致性。
文件同步需要用到的函数:
fsync:单个文件,同步数据,同步元数据(文件加上属性,如时间,大小等)
fdatasync:单个文件,同步数据,不同步元数据
sync:所有文件,同步数据,同步元数据
文本文件和二进制文件
:文本文件按照ASCII码编码,二进制文件按照十六进制编码。二进制文件比较稳定,不容易出错。
fopen : 标准IO打开文件。
fread:标准IO读取二进制文件。
fwrite:标准IO写二进制文件。
缓存
标准IO(fread,fwrite)引入了缓存的概念,为了减少系统调用,提高IO性能。标准IO比文件IO多了一个缓存的流程。
全缓存:数据量达到一定大小(4096字节)或遇到文件结束符时,缓冲区被刷新,数据才会被写入到目标设备(如终端或文件)。当打开文件后,如果文件与终端设备无关,则默认是全缓冲模式。
行缓存:包含换行符时会立即刷新。
无缓存:每次机械能IO操作时,都会立即刷新。
进程
- 程序是磁盘上的可执行文件,只占用磁盘空间,是一个静态概念。
- 进程是被执行的程序,不占用磁盘空间,需要消耗系统的内存,CPU资源,每个进程都有一个自己的虚拟地址空间,是一个动态概念。
- 进程是操作系统进行资源分配和管理的基本单位,每个进程都有自己的独立空间,包含代码、数据和堆栈等。
- 环境变量是操作系统中用来存储特定信息的动态值,通常被用于配置程序运行所需的各种信息,如系统路径,语言设置,临时文件设置等。
并行和并发
CPU在某个时间节点只能处理一个任务,但是操作系统都支持多任务的,CPU会给每个进程被分配一个时间段,进程得到这个时间片之后才可以运行,使各个程序从表面上看是同时进行的。如果在时间片结束时进程还在运行,CPU的使用权将被收回,该进程将会被中断挂起等待下一个时间片。如果进程在时间片结束前阻塞或结束,则CPU当即进行切换,这样就可以避免CPU资源的浪费。
在我们使用的计算机中启动的多个程序,从宏观上看是同时运行的,从微观上看由于CPU一次只能处理一个进程,所有它们是轮流执行的,只不过切换速度太快,我们感觉不到罢了,因此CPU的核数越多计算机的处理效率越高。
并发:并发的同时进行是一个假象,针对一个硬件资源,通过计算机CPU时间片的快速切换,使得程序表面看起来像是一起执行的。
并行:多进程同时运行时真实存在的,可以在同一时刻运行多个进程。
进程控制块:PCB - 进程控制块(Processing Control Block),Linux内核的进程控制块本质上是一个叫做 task_struct的结构体。在这个结构体中记录了进程运行相关的一些信息
进程一共有五种状态分别为:创建态,就绪态,运行态,阻塞态(挂起态),退出态(终止态)其中创建态和退出态维持的时间是非常短的,稍纵即逝。
- 就绪态:进程被创建出来了,有运行的资格但是还没有运行,需要抢CPU时间片,得到CPU时间片,进程开始运行,从就绪态转换为运行态。进程的CPU时间片用完了, 再次失去CPU, 从运行态转换为就绪态。
- 运行态:运行态不会一直持续,进程的CPU时间片用完之后, 再次失去CPU,从运行态转换为就绪态,只要进程还没有退出,就会在就绪态和运行态之间不停的切换。
- 阻塞态:进程被强制放弃CPU,并且没有抢夺CPU时间片的资格,比如: 在程序中调用了某些函数(比如: sleep()),进程又运行态转换为阻塞态(挂起态),当某些条件被满足了(比如:slee() 睡醒了),进程的阻塞状态也就被解除了,进程从阻塞态转换为就绪态。
ps aux 查看进程信息 kill 杀死进程
1 |
|
进程控制
- Linux中进程ID为 pid_t 类型,其本质是一个正整数。
getpid:获取当前进程的ID
getppid:获取父进程的ID
fork:创建一个新的进程
fork函数
- fork创建子进程,每个进程都对应一个属于自己的虚拟地址空间,子进程的地址空间是基于父进程的地址空间拷贝出来的,虽然是拷贝但是两个地址空间中存储的信息不可能是完全相同的。
fork调用成功后,父子进程的返回值不同,父进程返回值大于0,子进程的返回值等于0。
- exec族函数可以将进程中执行的函数和数据进行替换。
- 父子进程中是不能通过全局变量实现数据交互的,因为每个进程都有自己的地址空间,两个同名全局变量存储在不同的虚拟地址空间中,二者没有任何关联性。如果要进行进程间通信需要使用:管道,共享内存,本地套接字,内存映射区,消息队列等方式。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26int number = 10;
int main(){
cout<<"创建子进程之前numeber = "<<number<<endl;
pid_t pid = fork();
cout<<"当前进程fork()返回值 "<<pid<<endl;
//如果是父进程
if(pid > 0)
{
printf("我是父进程, pid = %d, number = %d\n", getpid(), ++number); //11
printf("父进程的父进程(终端进程), pid = %d\n", getppid());
sleep(1);
}
else if(pid == 0)
{
// 子进程
number += 100;
printf("我是子进程, pid = %d, number = %d\n", getpid(), number); //110
printf("子进程的父进程, pid = %d\n", getppid());
}
return 0;
}
父子进程虚拟地址空间:
- 各自拥有独立的虚拟地址空间
- 父子进程共享代码段(只读)
- 采用写时拷贝计算创建子进程虚拟地址空间:写时拷贝指只创建虚拟地址空间,不为子进程分配实际的内存,父进程和子进程之间共享相同的物理内存页面。当子进程或父进程对虚拟地址空间对应的内存进行更改时才会分配实际内存。
- 节省内存开销
- 提高创建进程效率
结束进程:
exit()或者_exit()函数,函数的参数相当于退出码
孤儿进程:父进程由于某种原因先退出了,子进程还在运行,这时候这个子进程就可以被称之为孤儿进程。当检测到某一个进程变成了孤儿进程,这时候系统中就会有一个固定的进程领养这个孤儿进程。
系统为什么要领养这个孤儿进程呢?
在子进程退出的时候, 进程中的用户区可以自己释放, 但是进程内核区的pcb资源自己无法释放,必须要由父进程来释放子进程的pcb资源,孤儿进程被领养之后,操作系统释放资源,这样可以避免系统资源的浪费。
僵尸进程
:已经终止执行的进程,父进程没有它的回收资源,用户区资源已经被释放了,只是还占用着一些内核资源(PCB)。
- 进程资源表浪费
- 父进程资源泄露
- 进程通信异常
- 系统性能下降。
如何避免僵尸进程?使用wait()或waitpid()系统调用来等待子进程的退出,并释放相关资源,来避免僵尸进程产生。
- wait()为阻塞方式。如果没有子进程退出, 函数会一直阻塞等待, 当检测到子进程退出了, 该函数阻塞解除回收子进程资源。这个函数被调用一次, 只能回收一个子进程的资源,如果有多个子进程需要资源回收, 函数需要被调用多次。
- waitpid()是非阻塞方式。通过该函数可以控制回收子进程资源的方式是阻塞还是非阻塞,另外还可以通过该函数进行精准打击,可以精确指定回收某个或者某一类或者是全部子进程资源。
守护进程
守护进程是一种在系统后台运行的特殊进程,独立的提供服务或执行任务,无需与用户交互,系统启动时自动启动,具有稳定性和长时间运行的能力。
守护进程PPID = 1,没有控制终端。
创建守护进程的过程:
- 创建子进程, 让父进程退出,子进程没有任何职务, 目的是让子进程最终变成一个会话, 最终就会得到守护进程。
- 通过子进程创建新的会话,调用函数 setsid(),脱离控制终端。
- 子进程调用fork成功,子进程调用exit(0)终止。由孙进程来充当守护进程,防止子进程成为会话首进程出现异常。
- 改变当前进程的工作目录 (可选项, 不是必须要做的)。
- 重新设置文件的掩码 (可选项, 不是必须要做的)。
- 关闭/重定向文件描述符 (不做也可以)
- 孙进程忽略SIGCHLD信号
- 根据实际需求在守护进程中执行某些特定的操作
参考列表
https://subingwen.cn/linux/file-descriptor/
https://space.bilibili.com/397638507/