【Linux】--- 进程的概念
- 一、进程概念
- 二、PCB
- 三、task_struct内容详解
-
- 1.查看进程
-
- (1)通过系统目录查看
- (2)通过ps命令查看
- (3)通过top命令查看
- (4)通过系统调用获取进程PID和父进程PPID
-
- [① 获取进程ID函数getpid和getppid](#① 获取进程ID函数getpid和getppid)
- [② 获取当前进程ID](#② 获取当前进程ID)
- [③ 获取父进程ID](#③ 获取父进程ID)
- 2.状态
- 3.优先级
- 4.上下文数据(重点!)
- 四、通过系统调用创建进程
- 五、进程状态
- 六、孤儿进程
- 七、关于进程状态的补充
-
- 1、kill
- [2、僵尸进程 和 孤儿进程](#2、僵尸进程 和 孤儿进程)
- 3、进程的状态:运行;挂起;阻塞;
- 八、进程优先级
-
- 1.概念
- 2.为什么要有进程优先级
- [3. 查看系统进程](#3. 查看系统进程)
- 4.PRI和NI
- 5.使用top命令更改进程优先级
- 九、环境变量
- 十、程序地址空间
一、进程概念
课本概念 :言简意赅的说就是,一个正在执行的程序
内核观点:进程是承担系统资源(CPU、内存)的实体
当我们写完代码之后,编译连接就形成一个可执行程序.exe,本质是二进制文件,在磁盘上存放着。双击这个.exe文件把程序运行起来就是把程序从磁盘加载到内存,然后CPU才能执行其代码语句。当把exe文件加载到内存后,此时这个exe文件形成的这程序就叫做:进程。所有启动程序的过程,本质都是在系统上创建进程,双击.exe文件也不例外:
二、PCB
1.什么是PCB
操作系统描述进程的时候就是PCB,PCB(process control block)
2.什么是task_struct(重点!)
在Linux中的PCB叫做:task_struct(task_struct就是PCB的一种)
创建进程不仅仅是,把代码和数据加载到内存里,还要为进程创建task_struct
所以:进程 = task_struct + 代码和数据
根据:"先描述,再组织"
①描述:task_struct在Linux内核中就是一种:结构体(里面包含着进程的相关信息)
②组织:系统里的进程都以task_struct 链表 的形式储存在内核里!
3.task_struct包含内容
标示符 : 描述本进程的唯一标示符,用来区别其他进程。(PID)
状态 : 任务状态,退出代码,退出信号等。
优先级 : 相对于其他进程的优先级。
程序计数器 : 程序中即将被执行的下一条指令的地址。
内存指针 : 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针。
上下文数据 : 进程执行时处理器的寄存器中的数据。
I/O状态信息 : 包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。
记账信息: 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。
三、task_struct内容详解
1.查看进程
(1)通过系统目录查看
proc是一个系统文件夹,在根目录下,通过ls可以看到该文件夹:
可以通过
ls /proc
命令查看进程的信息,数字是PID:
如果想查看进程信息,比如查看PID为989的进程信息,使用命令
ls /proc/PID
(2)通过ps命令查看
使用
ps aux //查看所有进程
如果结合grep可以查看某一个进程:
比如想查看包含proc的进程,可以使用如下命令:
ps aux | head -1 && ps aux | grep proc | grep -v grep
(3)通过top命令查看
也可以通过top查看进程:
top
(4)通过系统调用获取进程PID和父进程PPID
① 获取进程ID函数getpid和getppid
获取进程ID和获取父进程ID可以通过以下方式进行获取,其中pid_t是short类型变量:
cpp
#include <sys/types.h>
#include <unistd.h>
pid_t getpid(void);//获取当前进程ID
pid_t getppid(void);//获取当前进程的父进程ID
② 获取当前进程ID
获取当前进程,test_blog.c
cpp
#include<sys/types.h>
#include<stdio.h>
#include<unistd.h>
int main()
{
while(1)
{
printf("i am a process! :pid:%d\n",getpid());//获取当前进程ID
sleep(1);
}
return 0;
}
Makefile:
cpp
test_blog:test_blog.c
gcc -o $@ $^ -g
.PHONY:clean
clean:
rm -rf test_blog
③ 获取父进程ID
cpp
#include<sys/types.h>
#include<stdio.h>
#include<unistd.h>
int main()
{
while(1)
{
printf("i am a process! :pid:%d\n,my parent id is->ppid:%d\n",getpid(),getppid());//获取当前进程ID,和ppid
sleep(1);
}
return 0;
}
Makefile:
cpp
test_blog:test_blog.c
gcc -o $@ $^ -g
.PHONY:clean
clean:
rm -rf test_blog
2.状态
之前写代码的返回值是0 ,这个0是进程退出时的退出码,这个退出码是要被父进程拿到的,返回给系统,父进程通过系统拿到。比如以下代码的退出码是0
cpp
#include<stdio.h>
int main()
{
printf("hello linux!\n");
return 0;
}
那么使用
cpp
echo $?
假如将退出码改为99 :
所以,状态的作用是输出最近执行的命令的退出码。
3.优先级
权限指的是能不能,而优先级指的是已经能了,有权限了,但是至于什么时候执行得先排队 ,这就像在餐馆点餐结帐出小票之后,已经可以拿到餐食了,但是什么时候能拿到呢?需要排队,在这个过程中,是否出小票就代表是否有权限,排队取餐就代表的是优先级。
4.上下文数据(重点!)
当操作系统维护进程队列时,由于进程代码可能不会在很短时间就能执行完毕,假如操作系统也不会在执行一个进程时,让其他进程一直等待,直到当前进程执行完毕,那可能当前进程需要执行很久才执行完毕,其他进程会一直处于等待状态,这不合理。那么操作系统在实际执行进程调度时,按时间片分配执行时间,时间片一到,就切换下一个进程 。(多管齐下)
时间片是一个进程单次运行的最长时间。
比如有4个进程,在40ms之内先让第一个进程运行10ms,时间一到就算没有运行完毕,就把第一个进程从队列头移动到队列尾,再让第二个进程运行10ms。40ms后,使得用户感知到这4个进程都推进了,其实本质上是通过CPU的快速切换完成的。
有可能在一个进程的生命周期内被调度成百上千次。比如CPU有5个寄存器,进程A正在运行时时间片到了,被切走的时候,会把CPU里和进程A相关的保存到寄存器里面的临时数据带走。当进程B调度完后,再次调度进程A的时候,会把进程A里面保存的临时数据再恢复到CPU的寄存器当中,继续上次切走时的状态继续运行,因此保护上下文能够保证多个进程切换时共享CPU。
(保存上下文数据,实际上就是,保存:某一个进程在用完时间片后,剩下该进程没有被执行的代码和数据)
四、通过系统调用创建进程
1.使用fork创建子进程
fork用来创建子进程:
cpp
#include <unistd.h>
pid_t fork(void);//通过复制调用进程创建一个新进程。新进程称为子进程。调用进程称为父进程。
看一个奇奇怪怪的代码:
cpp
#include<unistd.h>
#include<stdio.h>
int main()
{
int ret = fork();
if(ret > 0)
{
printf("I am child\n");
sleep(1);
}
else
{
printf("I am father\n");
sleep(1);
}
return 0;
}
发现两句话都打印了,也就是既执行了if又执行了else:
再看代码:
cpp
#include<stdio.h>
#include<unistd.h>
int main()
{
int ret = fork();
while(1)
{
printf("I am a process ,pid = %d,ppid = %d\n",getpid(),getppid());
sleep(1);
}
return 0;
}
发现有两个pid和ppid:
这说明执行while死循环不只一个执行流在执行, 而是两个执行流在执行,每一行两个id都是父子关系。这是因为fork之后有两个执行流同时执行while循环。
2.理解fork创建子进程(重点!)
./可执行程序、命令行、fork,站在操作系统角度,创建进程的方式没有差别,都是系统中多了个进程。fork创建出来的子进程,和父进程不一样,父进程在磁盘上是有可执行程序的,运行可执行程序时会把对应的代码和数据加载到内存中去运行。
但是子进程只是被创建出来的,没有进程的代码和数据,默认情况下,子进程会继承父进程的代码和数据,子进程的数据结构task_struct也会以父进程的task_struct为模板来初始化子进程的task_struct 。因此子进程会执行父进程fork之后的代码,来访问父进程的数据 。
3.fork后的数据修改 (重点!)
(1)代码是不可以被修改的。 那么数据呢?子进程和父进程共享数据,当父进程修改数据时,子进程看到的数据也被修改了,那么父进程就会影响子进程。那这两个进程还具有独立性吗?
(2)当父子进程都只读不写数据时,数据是共享的。
但是这两个进程中的任何一个进程要修改数据,都会对对方造成影响,这时候作为进程管理者:操作系统就要站出来干涉了。
父子进程修改数据时,操作系统会在内存中重新开辟一块空间,把这部分数据拷贝过去之后再做修改,而不是在原数据上做修改,这叫做->写时拷贝。
(3)写时拷贝,存在的意义:维护进程的独立性!(提高空间利用率)
对空间利用率的理解:而在创建子进程时不会让子进程把父进程的所有数据全部都拷贝一份,因为并不是所有情况下都可能产生数据写入/修改,所以这就避免了fork时的效率降低和浪费更多空间的问题。因此只有写入数据时再开辟空间才是合理的。
4.fork的返回值(重点!)
(1)fork返回值含义
fork出子进程后,根据fork()返回值的不同,一般会让子进程和父进程去干不同的事情 ,这时候如何区分父子进程呢?fork函数的返回值如下:
打印一下fork的返回值:
这说明:
- fork准备return的时候子进程被创建出来了。
- 这里有两个返回值,由于函数的返回值是通过寄存器写入的, 函数返回时把变量值写入到保存数据的空间。所以当父子执行流执行完毕以后,有两次返回,就有两个不同的返回值,就要进行写入,谁先返回谁就先写入,即发生写时拷贝。
- 给父进程返回子进程的pid的原因是,一个父进程可能有多个子进程,子进程必须得用pid来进行标识区分,所以一般给父进程返回子进程的pid来控制子进程。子进程想知道父进程pid可以通过get_ppid( )来获取。这样就可以维护父子进程了。
(2)根据fork返回值让父子进程执行不同的功能
通过返回值来让父子进程分流,去执行不同的功能:
cpp
#include<stdio.h>
#include<unistd.h>
int main()
{
pid_t ret = fork();
//通过if else来分流
if(ret == 0)//child
{
while(1)
{
printf("I am child, pid = %d,ppid = %d\n",getpid(),getppid());
sleep(1);
}
}
else if(ret > 0)//parent
{
while(1)
{
printf("I am parent, pid = %d,ppid = %d\n",getpid(),getppid());
sleep(3);
}
}
else
{
}
return 0;
}
五、进程状态
1.进程状态定义
一个进程从创建而产生至撤销而消亡的整个生命期间,有时占有处理器执行,有时虽可运行但分不到处理器、有时虽有空闲处理器但因等待某个事件的发生而无法执行,这说明进程和程序不相同,它是活动的且有状态变化的,能够体现一个进程的生命状态,可以用一组状态来描述:
内核源代码里面的状态定义:
cpp
/*
* The task state array is a strange "bitmap" of
* reasons to sleep. Thus "running" is zero, and
* you can test for combinations of others with
* simple bit tests.
*/
static const char * const task_state_array[] = {//进程也叫做任务
"R (running)", /* 0 */
"S (sleeping)", /* 1 */
"D (disk sleep)", /* 2 */
"T (stopped)", /* 4 */
"t (tracing stop)", /* 8 */
"X (dead)", /* 16 */
"Z (zombie)", /* 32 */
};
使用如下两条命令都可以查看进程当前状态:
cpp
ps aux
cpp
ps axj
2.进程状态分类
(1)R-运行状态
R(Running) :要么在运行中,要么在运行队列里,所以R状态并不意味着进程一定在运行中,因此系统中可能同时存在多个R状态进程。
如果运行时在后面加&,就会在后台运行,就变成R状态了:
(2)S-浅睡眠状态
S(Sleeping) :进程正在等待某事件完成,可以被唤醒,也可被杀死,浅睡眠状态也叫做可中断睡眠。
比如如下代码:
status.c
cpp
int main()
{
printf("hello linux\n");
sleep(20);
return 0;
}
在运行后20s内查看status进程的状态,发现为S+,执行kill命令后,该进程被杀死
(3)D-深睡眠状态
D(Disk sleep):进程正在等待IO,不能被杀死,必须自动唤醒才能恢复,也叫不可中断睡眠状态。
进程等待IO时,比如对磁盘写入,正在写入时,进程处于深度睡眠状态,需要等待磁盘将是否写入成功的信息返回给进程,因此此时进程不会被杀掉
(4)T-停止状态
T(Stopped):可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可以通过发送SIGCONT 信号让进程继续运行。
运行起来的status进程,通过SIGSTOP信号被暂停了,状态由S+变为T:
又通过SIGCONT信号恢复了,状态由T变为S:
kill -l命令可列出操作系统中所有信号,其中18就是SIGCONT信号,19就是SIGSTOP信号:
因此上述kill SIGCONT 进程号 也可以用kill -18 进程号来代替,kill SIGSTOP 进程号 也可以写成kill -19 进程号来代替。
(5)Z-僵尸状态
概念:
僵死状态(Zombies)是一个比较特殊的状态。当进程退出并且 父进程 没有读取到 子进程 退出的返回代码时就会产生僵尸进程
僵死进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码。
所以,只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程进入Z状态
(6)X-死亡状态
这个状态只是一个返回状态,在任务列表里看不到这个状态。因为当进程退出时,释放进程所占用的资源时一瞬间就释放完了,所以死亡状态看不到。
3.僵尸进程危害
(1)进程的退出状态必须被维持下去,因为它要把退出信息告诉父进程,如果父进程一直不读取,那么子进程就一直处于僵尸状态
(2)由于进程基本信息是保存在task_struct中的,如果僵尸状态一直不退出,只要父进程没有读取子进程退出信息,那么PCB一直都需要维护。
(3)如果一个父进程创建了多个子进程,并且不回收,那么就要维护多个task_struct 数据结构,会造成内存资源的浪费
(4)僵尸进程申请的资源无法进行回收,那么僵尸进程越多,实际可用的资源就越少,也就是说,僵尸进程会导致内存泄漏
六、孤儿进程
僵尸进程是子进程先退出,但是父进程没有读取子进程的退出信息。
假如父进程先退出,子进程后退出,此时子进程处于孤儿状态,没有父进程来读取它的退出信息,此时子进程就称为孤儿进程。
孤儿进程一般会被1号进程回收/领养(1号进程就是OS本身)!
七、关于进程状态的补充
1、kill
kill 命令:可以向指定进程发起命令!
① kill -9 XXX :杀掉进程
②kill -19 XXX :暂停进程
③kill -18 XXX:继续进程
2、僵尸进程 和 孤儿进程
3、进程的状态:运行;挂起;阻塞;
(1)一个进程的运行会在cpu内存中的一个进程队列里面,只要一个进程在队列里面或者已经准备好进入进程队列,他们就是R运行状态。
(2)并发、并行的概念:
(3)阻塞态:
①操作系统如何对外设(显示器、键盘、磁盘、网卡...)进行资源管理???
实际上就是对外设的数据进行管理,所有的外设都自己task_struct 里面的数据进行管理即可!
注意:不仅只有CPU有进程队列,各种外设也有自己的wait_queue!!!
②让一个进程变为阻塞,在内核视角就是:把一个进程从它的运行队列剥离下来,连接到外设的task_struct的wait_queue()里面,此时的进程就变为了 阻塞态
注意:入队列的 永远只是进程的 task_struct,而不是代码和数据!!!
(4)挂起态:
注意:但是频繁地唤入唤出,会导致效率问题!!!
八、进程优先级
1.概念
进程的优先级就是CPU资源分配的先后顺序 ,即进程的优先权,优先权高的进程有优先执行权力。
其他概念:
- 并发: 多个进程在一个CPU下采用进程切换的方式,在一段时间之内,让多个进程都得以推进,称之为并发
- 并行: 多个进程在多个CPU下分别,同时进行运行,这称之为并行
2.为什么要有进程优先级
因为CPU资源是有限的,一个CPU只能同时运行一个进程,当系统中有多个进程时,就需要进程优先级来确定进程获取CPU资源的能力。
3. 查看系统进程
cpp
ps -l
可以看到
UID : 代表执行者的身份,表明该进程由谁启动
PID : 代表这个进程的代号
PPID :代表这个进程是由哪个进程发展衍生而来的,亦即父进程的代号
PRI :代表这个进程可被执行的优先级,其值越小越早被执行
NI :代表这个进程的nice值
4.PRI和NI
PRI是进程的优先级,也就是就是程序被CPU执行的先后顺序,此值越小进程的优先级别越高
- NI就是nice值,表示进程可被执行的优先级的修正数值
- PRI值越小越快被执行,加入nice值后,将会使得PRI变为:PRI(new)=PRI(old)+nice
- 当nice值为负值时,该程序优先级值将变小,即其优先级会变高,则其越快被执行
- Linux下调整进程优先级,就是调整进程nice值
- nice其取值范围是-20至19,一共40个级别。
注意: nice值不是进程的优先级,是进程优先级的修正数据,会影响到进程的优先级变化。
5.使用top命令更改进程优先级
(1)更改NI值
先运行一个进程,使用
cpp
ps -l
查看进程号、优先级及NI值,比如执行./forkProcess_getpid进程:
可以查看到优先级为80,NI值为0:
在运行top命令之后,输入r,就会有PID to renice,此时输入进程号5255,再输入NI值,此处设为10:
然后查看进程的优先级和NI值,优先级变成了90,NI值变成了10:
说明优先级和NI值已经被改了。由此也能验证:
cpp
PRI(new) = PRI(old)+nice
PRI(old)一般都是80,这就是为什么没有修改NI值之前,用ps -al命令查看到的进程的PRI都是80的原因。
(2)NI的取值范围
NI的取值范围为-20~19,一共40个级别。
(3)NI取值范围较小的原因
因为优先级再怎么设置,也只能是一种相对的优先级,不能出现绝对的优先级,否则会出现很严重的进程"饥饿问题",即某个进程长时间得不到CPU资源,而调度器需要较为均衡地让每个进程享受到CPU资源。
九、环境变量
1.概念
- 环境变量(environment variables)一般是指在操作系统中用来指定操作系统运行环境的一些参数
- 如:我们在编写C/C++代码的时候,在链接的时候,从来不知道我们的所链接的动态静态库在哪里,但是照样可以链接成功,生成可执行程序,原因就是有相关环境变量帮助编译器进行查找
- 环境变量通常具有某些特殊用途,还有在系统当中通常具有全局特性
2.常见环境变量
- PATH : 指定命令的搜索路径
- HOME : 指定用户的主工作目录(即用户登陆到Linux系统中时,默认的目录)
- SHELL : 当前Shell,它的值通常是/bin/bash。
3.如何查看环境变量
cpp
echo $PATH
系统通过PATH进行路径查找,查找规则就是,在PATH中先在第一个路径中找,找不到就在第二个路径中找,再找不到就在第三个路径中找......,如果找到了就不往下找了,直接将找到的路径下的程序运行起来,这就完成了路径查找。即系统执行命令时,操作系统通过环境变量PATH,去搜索对应的可执行程序路径。
如何让一个可执行程序:Progress执行时不带./,跟执行系统命令一样,有2种做法:
- 把Progress命令拷贝到以上5种任意一个路径里,不过这种做法不推荐,会污染命令池
- 把当前路径添加到PATH环境变量中
平时安装软件,就是把软件拷贝到系统环境变量中特定的命令路径下 就完成了,安装的过程其实就是拷贝的过程。
不能直接把当前路径赋值给PATH,否则上面的6种路径就全没了。可以使用export导入环境变量:
cpp
export PATH=$PATH:程序路径
查找到 forkProcess的路径:
添加环境变量:
现在在其他路径下也可以执行该可执行程序了,比如在家目录下执行:
4.和环境变量相关的命令
环境变量的本质是操作系统在内存/磁盘上开辟的空间,用来保存系统相关的数据。在语言上定义环境变量的本质是在内存中开辟空间,存放key、value值,即变量名和数据。
- echo:显示某个环境变量值
- export:设置一个新的环境变量
- env:显示所有环境变量
- set:显示本地定义的shell变量和环境变量
- unset:清除环境变量
1、echo显示某个环境变量值
2、export设置一个新的环境变量
3、env显示所有环境变量
4、set显示本地定义的shell变量和环境变量
5、unset清除环境变量
5.环境变量的全局属性
环境变量通常具有全局属性,可以被子进程继承。
如下代码:
geteEnvironment.c
cpp
#include<stdio.h>
#include<sys/types.h>
#include<unistd.h>
int main()
{
printf("pid = %d,ppid = %d\n",getpid(),getppid());
return 0;
}
发现每次运行该程序,子进程的ID都不相同,但是父进程的ID都相同
命令行上启动的进程,父进程都是bash,bash的环境变量是从系统里读的,系统的环境变量就在系统配置中,bash登陆时,bash就把系统的配置导入到自己的上下文当中。子进程的环境变量是系统给的,也就是父进程bash给的。环境变量一旦导出是可以影响子进程的
6.本地变量
与环境变量相对的还有本地变量,针对当前用户的当前进程生效,是一种临时变量,退出本次登陆后就失效了。
如下,变量value的值在没有退出登录前,打印到是5,ctrl+d退出登录后
退出重新登录后,就不存在了
本地变量能被子进程继承吗?用env查看,发现shell的上下文中是没有的:
说明本地变量是不能被继承的,只能bash自己用。
十、程序地址空间
1.程序地址空间分布
C/C++程序地址空间:
2.程序地址空间是虚拟地址
先看一段下面的代码,子进程在运行过程中修改了全局变量的值:
printfFork.c
cpp
#include<stdio.h>
#include<string.h>
#include<unistd.h>
int g_Value = 1;
int main()
{
//发生写时拷贝时,数据是父子进程各自私有一份
if(fork() == 0)//子进程
{
int count = 5;
while(count)
{
printf("child,times:%d,g_Value = %d,&g_Value = %p\n",count,g_Value,&g_Value);
count--;
sleep(1);
if(count == 3)
{
printf("############child开始更改数据############\n");
g_Value = 5;
printf("############child数据更改完成############\n");
}
}
}
else//父进程
{
while(1)
{
printf("father:g_Value = %d,&g_Value = %p\n",g_Value,&g_Value);
sleep(1);
}
}
return 0;
}
但是打印时却发现,同一个地址,g_Value值却不一样:
如果写时拷贝访问的是同一个物理地址的话,为什么得到的g_Value是不一样的值呢?所以程序地址空间使用的不是物理地址,而是虚拟地址。
3.虚拟地址
(1)页表
页表是一种数据结构,记录页面和页框的对应关系,本质是映射表 ,增加了权限管理,隔离了地址空间,能够将虚拟地址转换成物理地址。操作系统为每个进程维护一张页表。
(2)如何理解地址空间?
地址空间本质:是内核的一个struct结构体,包含的都是各个区域的start和end
(2)为什么要存在地址空间?
①地址空间保证了每个进程以统一的视角(有序的区域划分)进行管理,从而实现了进程的独立性。
②可以对:进程管理模块 和 物理管理模块 进行解耦!
具体来说,操作系统可以在加载进程时确定映射关系,之后物理内存的分配和进程的管理就可以各自独立进行,互不影响。
③保护数据安全,拦截非法请求!
例如,当某个进程试图访问其未被授予权限的内存区域时,操作系统能够检测到这种非法访问,并采取相应的措施,如终止该进程,以防止数据泄露或损坏。
4.写时拷贝
深度理解写时拷贝:
写车拷贝针对于父进程和子进程,或者多个子进程之间:指向同一块物理内存其数据也相同,当某一个进程想要修改数据或者写入的时候,操作系统就会在物理内存里面重新开辟一块空间,然后会把原来的数据原模原样地拷贝到新开物理内存空间中,然后再经过页表对物理地址的重新进行了映射,然后将你想要修改或者写入的数据,通过页表新映射关系找到那块开的物理内存空间,然后对数据进行修改或者写入!
(注意:1、写时拷贝是因为当你想要入的数据被按下了暂停键,因为操作系统需要在物理内存中新开一块空间+修改页表的映射关系,所以叫做写时拷贝 2、你写入或者修改的数据不是直接进了新开物理内存,而是操作系统在新开这块物理内存的时候,是将原来的数据拷贝到新开物理空间,之后才会修改或者写入 3、写时拷贝只会在你100%确实要写入或者修改数据的时候才会发生,如果你不写是不会发生的)