Linux进程概念

冯诺伊曼体系结构

对于我们常见的计算机,包括笔记本,服务器等等都遵循冯诺依曼体系结构

简单而言计算机就是由一堆硬件构成的,输入设备经过数据的处理在到输出设备。

输入设备: 键盘,鼠标,扫描仪等

中央处理器(cpu):运算器和控制器和寄存器等

输出设备:显示屏,打印机等

关于冯诺伊曼体系结构必须强调一下几点

1.对于这里的存储器,是指内存,也就是8g,16g等,不是硬盘

2.不考虑缓存情况,这里的cpu只能和存储器打交道,也就是内存,不能访问其他外设(输入或输     出设备)

3.输入或者输出设备也只能写入内存或者从内存中读取

4.外存是相对于内存和cpu所说的,像磁盘、网卡等等都是外存,具有永久性存储能力 

  总而言之,cpu只能和内存打交道(提高整机效率)

对于cpu,只能被动接受别人的指令和数据,那就必须让cpu认识指令,所以cpu有自己的指令集,我们平时写的代码本质经过编译后产生二进制可执行程序就是写cpu认识的指令,让cpu去执行我们想要的过程。

操作系统概念与定位(operator system)

概念:是一个进行软硬件资源管理的软件,笼统的讲,操作系统包括内核(进程管理、内存管理、文件管理、驱动管理)和其他程序(例如数据库、shell程序等等)

设计os的目的:为用户提供稳定的良好的执行环境(因为你也不想你在使用计算机设备时出现蓝屏啊等情况吧)和与硬件交互,管理所有的软硬件资源

管理:管理的本质就是对数据的管理,管理的方法就是先描述后组织,在后续的学习中我们会深有体会

感性理解操作系统:

用户:也就是使用计算机的我们

系统调用接口(System Call Interface):是操作系统提供给应用程序的一组接口,它允许应用程序请求操作系统内核执行特定的操作,是应用程序与操作系统内核之间进行交互的桥梁。            例如进程管理:包括创建新进程、终止进程、等待进程结束、进程调度等。例如,在 Linux 系统中,fork()函数用于创建一个新的进程,exec()函数用于执行一个新的程序。

用户操作接口(User Interface,简称 UI):,也常被称为用户界面,是指用户与计算机系统、软件应用程序或其他设备进行交互的接口。它涵盖了用户与系统之间的信息输入和输出方式,旨在为用户提供便捷、高效且直观的操作体验

简单通俗来讲,c/c++库封装好给你的就是用户操作接口,我们可以操作操作系统进而操作硬件,而封装好的里面肯定调用的是系统接口,像malloc是封装好的,你只管用,malloc主要依赖与两个系统接口,mmap()和brk();

感性理解:操作系统更像一个决策者,根据数据做决策,驱动更像一个执行者,而硬件就是一个被管理者,执行者通过被管理者拿数据,交给决策者(os),os做出命令,驱动执行命令

也就是os做决策,驱动通过硬件拿数据和执行os的命令,硬件去执行任务,然后返回数据给驱动然后os就能拿到数据

总结:

管理:先描述后组织

如何描述:简单来说就是用结构体struct

如何组织:简单来说就是用所学到的高效的数据结构,链表等等

什么是进程?

概念:进程是指在系统中正在运行的一个程序实例,是操作系统进行资源分配和调度的基本单位。当一个程序被加载到内存中并开始执行时,它就成为了一个进程。(Windows下打开任务管理器就可以看到各个进程的调度情况)                                                                                                       内核观点:进程是担当分配系统资源(cpu时间,内存)的实体

描述进程-PCB

之前所认识到的冯诺依曼体系结构中提到:cpu只能和内存打交道,所以磁盘上有很多的程序要执行,就必然有很多程序加载到内存中,就会变成进程,那么多的进程我们如何进行管理(先描述,后组织),先描述:PCB(process control block)也就是进程控制块,在Linux下是task_struct,用于描述进程的各种属性和状态,是 Linux 内核管理进程的核心数据结构。它包含了进程运行所需的各种信息,内核通过对task_struct的操作来实现对进程的创建、调度、执行和销毁等管理。

struct task_struct
{
  //该进程的所有属性;
   属性:标识符:(用来区别其他进程)
         状态:任务状态,退出码等
         优先级:等等属性
  //该进程对应的代码和数据地址;
   struct tast_struct* next;//指针链接起来,链表
}
//这里是操作系统帮你自动做的,你的可执行程序只有你的代码逻辑,并没有这些属性
先描述后组织:如何组织:通过链表

 

优先级:遍历链表找到优先级高的先执行,退出:判断pcb属性状态是否为死亡,死亡就删掉结点

所谓的管理:先描述(pcb)后组织(链表),就变成了对进程对应的PCB进行相关的管理,最后是对链表这种数据结构增删查改。

进程:现在我们可以对进程有一定的认识,进程就是内核数据结构(task_struct)+进程对应的磁盘代码

查看进程(Linux下如何查看)

1.ps ajx | grep "test"   //test是我们的可执行程序 grep "test"可以过滤其他的进程,方便我们查看test这个进程

2.ls /proc/ PID   //这个目录是内存级的目录,这个目录下也有进程的相关信息,一个进程如果源程序被删了,但还会执行,因为已经被加载到内存了,此时是进程,源程序被删跟它此时执不执没关系,只是ls /proc/ PID exe会变红

head -1 可以拿到标题,也就是PPID等信息,方便我们查看

PID:这个进程的id  (可以kill -9 PID,就可以杀掉这个进程) 

PPID:父进程的id 

进程有关的系统调用(通过系统调用获取进程提示符)

getpid():获得进程的id

getppid():获得父进程的id

这些函数定义于  #include <sys/types.h>#include <unistd.h>头文中                                      重启进程,PID会变,但是PPID不会(bash)后面会学习

fork():创建进程,这个新进程被称为子进程,而调用 fork 的进程则是父进程                                 父子进程共享一份代码,数据各自开辟空间,私有一份(采用写时拷贝)

fork的返回值:pid_t fork (void);   //fork之后,给父进程返回子进程的id,给子进程返回0

通常fork用if语句进行分流(通过返回值不同),让父子进程执行后续共享代码的一部分           (并发式编程)

进程状态:

概念:进程状态描述了进程在其生命周期内所处的不同阶段,它反映了进程当前的活动情况和系统对进程的管理状态。(简单来说,就是进程在不同的队列中等待某种资源)(学习的越多后面对概念的理解会更清晰)

在大多数教材当中:进程状态有运行、新建、就绪、死亡、等待、阻塞等等                              (接下来的讲解以Linux内核为准

struct runqueque
{
   task_struct*head;//头结点
   //其他属性
}

通常情况,一个cpu对应一个运行队列    让进程入队列, 本质就是让进程的PCB(task_struct)结构体对象放入运行队列中   

R(running):进程PCB要么在运行队列中,要么在运行中就叫运行状态

进程不只会等待和占用cpu的资源,也有可能占有外设的资源。                                                        例如:一个进程在cpu中跑,碰到fwrite是要去磁盘写入,但此时磁盘正在被其他进程占用,cpu快,磁盘慢,那cpu会等磁盘中的进程结束后,在让此时在cpu中跑的进程去磁盘写入吗?显然不会,这样效率太慢了,实际:cpu这个进程去对应的硬件的等待序列,cpu去跑其他进程,等磁盘好了就把状态改为R(先简单理解后续会详细讲解)

struct dev_disk
{
   task_struct* wait_queue//这是磁盘的等待序列
   //其他属性
}

这就叫由R状态变为阻塞状态,这样搞来搞去的不是代码和数据,是task_struct结构体对象

问题:如果阻塞状态过多,进程一开始会被加载到内存当中,是会占有一定的大小的,阻塞状态过多,加载到内存的代码和数据就会长时间没有被执行而留在内存上,占有的内存大小过多,就会导致其他程序不能加载到内存上。解决方案:操作系统把不会立即被cpu调度的进程的数据和代码暂时放到磁盘上的特定区域(换出)(这样的进程的状态叫挂起),当资源被释放了,轮到这个进程跑了,操作系统会自动帮你从磁盘当中的挂起状态改为R(换入),进程从阻塞到挂起,在由挂起到运行的状态转换过程中包含了内存数据的换入和换出。

总结:阻塞不一定挂起,挂起一定阻塞

我们简单了解了一些状态,那Linux是如何做的?

/*
* 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 */
};

简单介绍各个状态的意思 

R(running):运行状态

S(sleeping):休眠状态,阻塞的一种

T(stopped):暂停状态,阻塞的一种

D(disk sleep):深度睡眠状态,S是浅度睡眠,可以被终止,D状态无法被os杀死,只能断电/自己醒来,只有在高IO的状态下才可能出现D状态(例如很多的数据正要写入磁盘,如果你被os杀死,就可能会导致数据丢失)

t(tracing stop):是一种暂停状态,比如你在调试的时候就会出现t状态,表示该进程正在被追踪

X(dead):死亡状态,太快了,你ps axj时看不到

Z(zombie):僵尸状态

int a=0;
while(1)
{
a=1+1;
printf("%d",a)
}
[Day2@VM-8-11-centos ~]$  ps axj | head -1 && ps axj | grep a.out
 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
27297 29772 29772 27297 pts/1    29772 S+    1001   0:01 ./a.out
29847 30071 30070 29847 pts/0    30070 S+    1001   0:00 grep --color=auto a.out
[Day2@VM-8-11-centos ~]$ kill -9 29772

我先写了一个死循环,死循环中由printf语句,像显示器中打印,显示器是外设,对于cpu来说很慢,cpu太快了,即使有时间在R,你每次ps axj大概率都看到的状态是S+,可以看到STAT那一栏变为S+(这里有个+号,后面会做解释)(阻塞状态)

在 Linux 系统中,kill 命令用于向进程发送信号,从而对进程进行控制                        可以通过kill -l查看所有的编号

kill -9 PID  :会杀死某个进程

kill -19 PID:会把某个进程由别的状态变为T状态(暂停状态):属于阻塞的一种,至于有没有被挂起,取决于操作系统

kill -18 PID:信号编号 18 对应的信号名称是 CONT(Continue),即继续执行信号。                    由T变为R(没有+号)由T变为S(没有+号)

需要注意的是,使用 STOP 信号停止进程后,通常需要使用 CONT 信号或其他方式来恢复进程执行,否则进程将一直处于暂停状态。如果不希望进程继续执行,可以使用 kill -9 PID 命令来强制终止进程,但这种方式可能会导致进程数据丢失或系统不稳定,应谨慎使用。

S与T的区别

S 状态(可中断睡眠状态):进程处于睡眠状态,正在等待某个事件的发生,例如等待 I/O 操作完成、等待信号等。处于该状态的进程可以被信号中断,一旦等待的事件发生或者接收到信号,进程就会被唤醒,进入运行队列等待 CPU 调度。例如,当进程读取文件时,如果数据尚未准备好,进程就会进入 S 状态等待数据准备完成。

T 状态(暂停状态或跟踪状态):进程被暂停执行,通常是因为收到了特定的信号,如 STOP 信号(通过 kill -19 发送)或 TSTP 信号(通常由终端输入 Ctrl + Z 产生)。处于 T 状态的进程不会占用 CPU 资源,也不会被调度执行,直到收到 CONT 信号(通过 kill -18 发送)才会继续执行。另外,当进程被调试器跟踪时,也会进入 T 状态,以便调试器对其进行控制和检查。

状态有无+号的区别

状态有+号的叫前台进程,可以由ctrl+c终止,并且运行期间ls等没用,就像刚刚打印了一堆a的值,你在屏幕当中ls等命令是没有用的

状态没有+号的叫后台进程,不可以由ctrl+c终止,ls有用,只能通过kill -9 PID杀死

如何看到Z(僵尸状态)

你写代码让cpu去完成任务,有两种情况,第一你想要知道完成如何,第二你不关心,无论你关不关心,os都必须保留结果,这就当进程退出时,不能立即释放该进程对应的资源,而是会保存一段时间,让父进程或os来读取(这里后面会讲解),在读取期间的状态就叫Z状态,然后读取完了之后就会变成X状态,由os来回收或者父进程回收,不回收可能会导致内存泄漏,所以如何看到?当进程退出并且父进程(使用wait()系统调用,后面讲)没有读取到子进程退出的返回代码时就会产生僵尸状态,僵尸进程会以终止状态保持在进程表中,等待父进程读取退出状态码,所以只要子进程退出,父进程在运行并且没有读取子进程的状态,子进程就会进入Z状态。

僵尸进程的危害:

1.进程的退出状态必须被维持下去,因为它要告诉父进程你交给我办的任务完成得怎么样,如果父     进程一直不读取,子进程就会一直处于Z状态

2.维护退出状态本身也要用数据维护,属于进程基本信息,所以保存在task_struct(PCB)它包含了进程得各种信息,包括进程的状态,标识符,内存映射,打开的文件描述符等等中,换句话说,Z状态一直不退出,PCB就要一直维护,就要一直占有空间

3.如果一个父进程创建了很多的子进程就是不回收,会造成内存资源的浪费。

4.内存泄漏

特殊的进程:孤儿进程

如果父进程先退出,子进程后退出,此时的子进程就叫孤儿进程

父进程exit时是由bash回收了,看不到Z状态,此时子进程在运行称为孤儿进程,孤儿进程会被操作系统中的特殊进程收养,通常是 init 进程(进程号为 1)成为孤儿进程的新父进程,然后由init进程回收

进程优先级

cpu资源分配的先后顺序,优先级高的有优先执行权利,配置进程优先级对多任务环境的Linux很有用,可以改善系统性能。

查看系统进程

在Linux中,用ps -l命令:用于查看当前进程详细信息的命令   -a 选项用于显示所有与终端关联的用户的进程

F S   UID   PID  PPID  C PRI  NI ADDR SZ WCHAN  TTY          TIME CMD
0 S  1000  2534  2533  0  80   0 -  3915 wait   pts/0    00:00:00 bash
0 R  1000  2578  2534  0  80   0 -  3916 -      pts/0    00:00:00 ps

  1. F(进程标志):显示进程的标志位,它是一个数字,不同的数字组合代表不同的含义。例如:1 表示进程拥有超级用户权限。4 表示进程是通过 fork() 产生的子进程。
  2. S(进程状态):显示进程当前的状态
  3. UID(用户 ID):运行该进程的用户的 ID。可以通过 /etc/passwd 文件查看用户 ID 对应的用户名。
  4. PID(进程 ID):进程的唯一标识符,系统中每个进程都有一个独一无二的 PID。
  5. PPID(父进程 ID):该进程的父进程的 ID。父进程是创建当前进程的进程。
  6. C(CPU 使用率):进程最近使用 CPU 的百分比,是一个相对值。
  7. PRI(优先级):进程的优先级,数值越小表示优先级越高,越容易被 CPU 调度执行。
  8. NI(nice 值):进程的 “nice” 值,也称为谦让值。可以通过 nice 或 renice 命令调整该值,范围是 -20 到 19,值越小优先级越高。
  9. ADDR(内存地址):进程在内存中的地址,通常显示为 - ,表示不显示具体地址。
  10. SZ(内存大小):进程使用的虚拟内存大小,单位是页(通常一页为 4KB)
  11. WCHAN(等待通道):进程正在等待的内核函数或事件。如果进程没有在等待,显示为 - 。
  12. TTY(终端设备):与进程关联的终端设备。如果进程不与终端关联,显示为 ? 。
  13. TIME(CPU 时间):进程总共使用的 CPU 时间。
  14. CMD(命令):启动该进程的命令。

我们先关注第7和第8点 即PRI和NI,PRI中的值越小代表优先级越高 ,nice值就表示可被执行的优先级的修正数值PRI(new)=PRI(old)+nice,所以,调整优先级在Linux下就是调整nice值 范围是 -20 到 19,

用top命令更改已存在进程的nice值

top(sudo提权成root才可以改nice值)

进入top后按"r"->输入进程PID->输入nice值,clear退出

环境变量

环境变量:一般是指在操作系统中用来知道操作系统运行环境的一些参数,为了满足不同的应用场景而预先在系统内设置的一大批的全局变量

例如:我们在编写c/c++代码的时候,在链接的时候,我们从来不知道我们的所链接的动态静态库在哪里,但是照样可以链接成功,生成可执行程序,原因就是有相关的环境变量帮助编译器进行查找

环境变量通常具有某些特殊用途,在系统中通常具有全局特性,意味着它们在整个操作系统环境中都可以被访问和使用。一旦环境变量被设置,无论是在当前用户的会话中还是系统级别的设置中,它们都可以在系统的各个部分被引用。

查看环境变量的方法:

echo $NAME    //NAME为你环境变量的名称,可以显示环境变量的值

[Day2@VM-8-11-centos ~]$ echo $PATH
/usr/local/bin:/usr/bin:/usr/local/sbin:/usr/sbin:/home/Day2/.local/bin:/home/Day2/bin

常见的环境变量:

PATH:指定命令的搜索路径

HOME:指定用户的主工作目录(即用户登录到Linux系统时默认的目录)

SHELL:当前shell,它的值通常是/bin/bash

USER:当前登录的用户,知道了用户就可以知道它的权限,清楚它能干什么不能干什么

PATH:解释

为什么我们的可执行文件需要加绝对路径才能执行,也就是./a.out,而系统级别的命令如ls等他们也是一个可执行程序,直接就ls就会执行,因为ls在环境变量PATH的搜索路径中

[Day2@VM-8-11-centos ~]$ echo $PATH
/usr/local/bin:/usr/bin:/usr/local/sbin:/usr/sbin:/home/Day2/.local/bin:/home/Day2/bin

可以看到当你echo $PATH时出现一堆路径,每次执行命令时会在这些路径下搜索,有就执行命令,没有就报找不到命令

如何让自己的可执行程序像ls一样,可以export PATH=$PATH:test(可执行文件) 所在路径               这样新的PATH=旧的+你的程序所在路径

当然你也可以sudo cp test /usr/bin,把可执行程序拷贝到其中一个路径,但这样会污染指令集

[Day2@VM-8-11-centos dir1]$ gcc test.c -o test1
[Day2@VM-8-11-centos dir1]$ ll
total 16
-rwxrwxr-x 1 Day2 Day2 8360 Apr 24 14:14 test1
-rw-rw-r-- 1 Day2 Day2   83 Apr 24 14:13 test.c
[Day2@VM-8-11-centos dir1]$ echo $PATH
/usr/local/bin:/usr/bin:/usr/local/sbin:/usr/sbin:/home/Day2/.local/bin:/home/Day2/bin:/home/Day2/dir1
[Day2@VM-8-11-centos dir1]$ test1
0[Day2@VM-8-11-centos dir1]$ 

 可以看到我编译了一个文件形成test1可执行程序,并且已经把路径添加到了PATH中,当我直接test1时,打印出来一个0;

HOME:解释
[root@VM-8-11-centos ~]# echo $HOME
/root
[Day2@VM-8-11-centos dir1]$ echo $HOME
/home/Day2

可以查看当前的家目录,用root用户和普通用户分别执行echo $HOME对比差别

根据命令cd ~就可以知道这个命令在执行时,是找HOME这个环境变量

和环境变量相关的命令:

1.echo:用来查看当前环境变量值

2.export:设置一个新的环境变量,当你使用export命令在当前终端会话中改变了某些环境变量,通常情况下只在这次会话中有效。当你重新登录或者打开一个新的终端时,系统会重新加载原来的环境变量,这是因为你没有修改相应的配置文件

3.env(environment):显示所有的环境变量

4.unset:清楚环境变量和本地变量(这个本地变量稍后讲解)

5.set:显示本地定义的shell变量和环境变量

 如何通过函数调用获取环境变量:

getenv()函数调用

NAME
       getenv, secure_getenv - get an environment variable

SYNOPSIS
       #include <stdlib.h>

       char *getenv(const char *name);

       char *secure_getenv(const char *name);

   Feature Test Macro Requirements for glibc (see feature_test_macros(7)):

       secure_getenv(): _GNU_SOURCE

 通过查询手册,我们可以知道getenv是用来获取环境变量的

char* who=getenv("USER");

那对于这条语句,我们是不是就可以知道当前运行这个程序的主机的用户,就可以标识,然后确定这个用户的权限能做什么

本地变量与环境变量的区别:

可以看到最上面第一行,直接myval=1234,echo $myval时可以看到打印1234                                但是当我env时并没有查到myval ,因为此时的myval是本地变量,这种本地变量是局部变量,    环境变量具有全局性,getenv():获取环境变量,这种本地变量用getenv获取不到

如何从本地(局部属性)到环境变量(全局属性):export myval

可以看到env后有环境变量myval,这样env和getenv都可以得的到环境变量

深入理解环境变量和本地变量:

你自己写了一个可执行程序mycmd,里面获取myval(只在bash中muval=1234,并没有加到环境变量,此时是本地变量),当你./mycmd时,系统进程bash,会创建一个子进程,你的mycmd就是bash的子进程,环境变量具有全局性,是会被子进程继承下去的(因为要适应不同的应用场景),那就可以getenv("USER")来进行身份标识,而myval没有被继承,只会在当前进程(bash)中有效,所以你getenv("myval”);时获取不了,而你在echo $myval可以看到它的值是因为你此时在bash的进程中

环境变量的应用场景体会:

为什么ls时就会帮你显示当前路径下的文件和目录;

因为编写ls这个命令程序的人,getenv("PWD");pwd是记录当前路径的环境变量,那是不是获得环境变量后可以找到你当前所属的路径,那就可以显示出来;总之ls是一个程序,当你运行时,bash创建子程序去ls,ls这个程序继承环境变量,所以ls知道PWD

环境变量的组织方式:

每一个程序都会获得一张环境表。环境表以"\0"结尾的环境字符串 

如何继承

1.理解命令行参数,在devc++编译器中,                                                                                         你会看到你编写的int main(int argc,char* argv[],char* env[]);vs2022可以自己添加;

  • argc 和 argv:用于传递命令行参数。argc 表示命令行参数的数量,argv 是一个字符串数组,存储着每个命令行参数。argv的第一个参数是“ls”,第二个是你传的“-a”等
  • env:是一个字符串数组,用于存储环境变量。不过,即使你没有在 main 函数中声明 env 参数,环境变量仍然会被继承,只是你无法通过 env 数组来直接访问它们。

 当你在 bash 进程下执行 myls -a 命令时,-a 会作为一个命令行参数被传递给 main 函数的 argv 数组

通过现在的学习就可以编写一个属于自己的myls命令程序:大致思路,可以通过比较命令行参数,比如传进来-a就去做-a的事情

int main(int argc, char *argv[]) {
    DIR *dir;
    struct dirent *entry;
    const char *path;

    // 如果没有提供命令行参数,默认使用当前目录
    if (argc == 1) {
        path = ".";
    } else {
        path = argv[1];
    }

    // 打开指定目录
    dir = opendir(path);
    if (dir == NULL) {
        perror("无法打开目录");
        return 1;
    }

    // 读取并打印目录中的每个条目
    while ((entry = readdir(dir)) != NULL) {
        printf("%s\n", entry->d_name);
    }

    // 关闭目录
    closedir(dir);

    return 0;
}    

总之:会传两只表,一个是运行时传进来的命令行参数形成argv[]命令行参数表(通过比较表中的值就可以知道去执行什么样的操作)另一个是继承父进程的环境变量表

2.如果我没有显示的继承char* env[],操作系统也会帮我们隐式继承,这是操作系统已经帮我们做好的事情,你照样可以getenv()获得环境变量,但你无法打印出来所以的环境变量,也就是通过for循环去操作env[i]来打印所有的环境变量

可以通过第三方变量environ获取

可以看到environ是一个二级指针,libc中定义的environ是一个全局变量,并没有包含在任何的头文件中,声明一下就可以使用了

#include <stdio.h>
#include <stdlib.h>

// 声明 environ 变量
extern char **environ;

int main() {
    // 遍历 environ 数组
    for (char **env = environ; *env != NULL; env++) {
        // 打印每个环境变量
        printf("%s\n", *env);
    }
    return 0;
}    

 进程地址空间:

对于一个进程来说,根本就不会给4G的空间给这个进程用 ,当进程进来的时候,创建PCB,PCB当中又会创建一个struct mm_struct结构体对象,mm_struct*mm=malloc之后

struct mm_struct{

code_start=null;
code_end=null;

等等区域的划分;}

mm->code_start=0x11111111

mm->code_end=0x12111111

代码区多大?当程序被加载到内存中时就可以读取然后区初始化start和end了

stack向下增长,heap向上增长,本质就是修改start和end的数据

free缩小,malloc扩大这样就可以为区域划分 

每个地址都是虚拟地址(不是真实的物理地址),因为可能有的进程的数据地址大小可能一样,但实际数据不一样

这张图能够清晰的表示虚拟地址和物理地址的区别:

假设对于全局变量g_val,创建子进程去修改g_val,即使你打印g_val的地址一样,但两个也属于不同的空间。

解释:当你创建进程时,就会创PCB,然后根据其在加载到内存的代码和数据,把PCB中的mm初始化,什么虚拟地址到什么虚拟地址是代码区,划分区域,这也就是进程地址空间,然后建立页表的映射关系,把虚拟地址通过页表和物理地址映射起来,所以我们平时&g_val是一个虚拟地址。

所以进程地址空间与物理内存之间是通过页表进行映射的

1.为什么要有进程地址空间?直接让它访问物理空间不就行了?                                                    因为不安全,万一进程非法越界操作呢?因为页表存在就只会映射到安全区域

2. 那为什么上面的子进程修改g_val没有影响到父进程?因为这是进程的独立性的体现?               一个进程对被共享的数据做修改,而如果影响了其他进程,这就不能称作进程的独立性,任何一方尝试对共享数据修改时,os会先进行数据拷贝,更改页表映射(写时拷贝),所以子进程在改全局变量g_val时发生写时拷贝,映射关系改变了,所以子进程修改其值不影响父进程的值

3.为什么你的可执行程序中在没有被加载到内存时是有地址的?                                                    因为虚拟地址空间,不仅仅是os要遵守对应的规则,编译器也要遵守,所以在编译代码的时候,就是按照虚拟地址空间的方式对我们的代码和数据进行编址的 ,在实际运行时,在通过页表映射到物理内存中,这样方便进程以统一的视角看待进程对应的代码和数据等各个区域,也方便编译器以统一的视角进行编译代码(编完即可使用,因为规则一样)

4.cpu至始至终看到的都是虚拟地址,通过虚拟地址,然后通过页表映射到实际的物理内存,然后读取数据

总结:

至此,我们已经初步了解了进程的概念,以及初步了解了一个程序到进程在结束的大致的流程。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值