计算机,都是有一个个的硬件组件组成
计算机的硬件有:

💡注意事项:
任何计算机系统都包含一个基本的程序集合/软件,称为操作系统(OS)
笼统的理解,操作系统包括:
内核:包含进程管理,内存管理,文件管理,驱动管理
其他程序,例如函数库, shell程序等

对上:提供一个良好的使用环境
对下:通过管理好软硬件资源的方式保证系统的稳定性。
在整个计算机软硬件架构中,操作系统的定位是: 一款纯正的⭐搞管理的软件
类比在我们的学校,校长像操作系统一样是管理者,辅导员像驱动程序一样是执行者(驱动程序也是软件),学生像硬件一样是被管理者。
管理者和被管理者可以不直接沟通,正如校长和我们没有直接接触却可以管理我们。
管理者拿到被管理者的核心数据,来支持管理决策,正如校长拿到学生绩点的数据,所以说管理是对被管理对象的数据进行管理。
./xxx ,其实就是在系统层面上创建了一个进程。task_ struct 属性分类
所有运行在系统里的进程都以task_struct描述起来,并且以双链表 的形式存储在内核里
ps该命令默认只能查看这个终端下对应的进程
ps axj查看系统中所有的进程
ps axj | grep ‘进程名’查看这个进程的属性信息
ps axj | head -1 && ps axj | grep '进程名'查看这个进程的属性信息并且列出对应的属性名。
top该命令也可以显示系统中的进程(相当于window下的任务管理器,但是不方便,q退出)
进程的信息也可以通过 /proc 系统文件夹查看,要获取PID为1的进程信息,你需要查看 /proc/1 这个文件夹。
举例:我的可执行程序为 myproc,运行期间的进程信息👇

//两个头文件 #include , #include
#include
#include
int main()
{
pid_t pid=getpid(); //获取自己的进程PID⭐ ,返回值是pid_t
pid_t ppid=getppid();//获取自己的父进程PPID,没有其他指令默认是bash进程
printf("pid: %d\n", pid);
printf("ppid: %d\n", ppid);
return 0;
}
⭐ 可以在Linux终端输入命令
kill -9 进程的PID中止进程
格式:
#include
pid_t fork(void);⭐
返回值:
失败时:返回-1;
成功时:给父进程返回子进程的pid,给子进程返回0
//fork()有两个返回值⭐
举例:
#include
#include
#include
int main()
{
printf("i am parent process :pid: %d\n",getpid());
pid_t ret =fork();
//变成两个进程,一个父进程,一个子进程
printf("ret:%d pid:%d ppid:%d\n ",ret,getpid(),getppid());
sleep(1);
return 0;
}
运行结果👇

⭐ fork()是个函数,是操作系统调用接口
#include
#include
#include
int main()
{
pid_t id=fork();
//变成两个进程,一个父进程,一个子进程
if(id<0)
{ //创建失败
perror("fork");//打印出fork失败的原因,是C语言提供的出错接口
return 1;
}
else if(id==0)//id在子进程里面是0
{
//child process(task)
while(1)
{
printf("I am child,pid:%d,ppid:%d\n",getpid(),getppid());
sleep(1);
}
}
else
{
//parent process
while(1)//id在父进程里面是子进程的pid
{
printf("I am father,pid:%d,ppid:%d\n",getpid(),getppid());
sleep(1);
}
}
sleep(1);
return 0;
}

🚨结论:
我们可以通过fork()创建父子进程,根据返回值的不同,让父子进程执行不同的代码。
父子进程代码共享,数据各自开辟空间,私有一份(采用写时拷贝)。
fork之后,代码是父子进程共享的,但是因为id值的不同,所以父子进程执行不同的代码 。
可以惊奇的发现,else if 和 else可以同时执行,并且两个死循环也同时执行❗ 主要是因为fork之后有两个不同的执行流。
fork之后,给父进程返回子进程的pid,给子进程返回0。因为父进程:子进程 = 1 : n ,父进程可以有很多子进程,子进程只能有一个父进程,这么设计是为了让父子进程都可以互相标识。
同一个变量id,会有不同的值❓ 这个问题会在进程地址空间详解
fork为什么会有两个返回值❓
- fork()是操作系统的接口,代码的实现是在操作系统里的。
- fork有两个返回值是因为fork内部,父子各自会执行自己的return语句。
- 但是返回两次并不代表一个变量会保存两次。
那么创建子进程时候,操作系统要做什么呢❓
本质就是系统多了一个进程,要新建一个 task_struct结构体,其内部属性要以父进程为模板创建。
操作系统和cpu运行某一个进程,本质是从task_struct形成的队列中挑选一个task_struct,来执行它的代码❗进程调度也就是在task_struct形成的队列中选择一个进程的过程❗ (只要想到进程,优先想到进程对应的task_struct)
父子进程哪一个被先运行❓
谁先运行不一定,这个是由操作系统的调度器决定的。
在Linux中进程可以分为:
kill -9 此进程的pid)⭐Linux进程状态可以分为:
- R运行状态(running) : 并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队列里,对应上面的运行态。
- S睡眠状态(sleeping): 意味着进程在等待事件完成,这里的睡眠也叫做可中断睡眠,对应的就是上面的阻塞状态。
- D磁盘休眠状态(Disk sleep)也叫不可中断睡眠状态(uninterruptible sleep)可以理解为深度睡眠,不可以被中断,也就是不可以被被动唤醒,在这个状态的进程会等待IO的结束。
- T停止状态(stopped): 可以通过发送 19(SIGSTOP) 信号给进程来停止(T)进程
kill -19 此进程的pid。这个被暂停的进程可以通过发送18(SIGCONT)信号让进程继续运行kill -18 此进程的pid。(应用最多就是在调试的场景)- X死亡状态(dead):这个状态只是一个返回状态,瞬时性非常强,你不会在任务列表里看到这个状态。
- Z僵尸状态(zombie):是什么? 为什么?怎么办?👇
👀当服务器压力过大的时候,os会通过一定的手段,杀掉一些处于睡眠进程来起到节省空间的作用。但是如果磁盘在读写数据的时候位于睡眠状态,杀掉此进程会导致数据的丢失,所以操作系统的设计者设计了D状态,防止该事件的发生。
ps aux / ps axj 命令
ps axj | head -1 && ps axj | grep 进程名 | grep -v grep
- PID : 代表这个进程的代号
- PPID :代表这个进程是由哪个进程发展衍生而来的,亦即父进程的代号
- UID : 代表执行者的身份
- PRI:代表这个进程可被执行的优先级,其值越小越早被执行
- NI :代表这个进程的nice值
是什么?
僵死状态(Zombies)是一个比较特殊的状态:当进程退出并且父进程没有读取到子进程退出的返回代码时就会产生僵尸进程,这个时候不允许被OS释放,处于一个被检测的状态就叫做僵尸状态。(代码和数据是可以释放的,但是描述进程的PCB还在)
为什么?
是为了让父进程和操作系统来进行回收。
僵尸进程危害
- 维护退出状态本身就是要用数据维护,也属于进程基本信息,所以保存在task_struct(PCB)中,换句话说, Z状态一直不退出,PCB一直都要维护。
- 那一个父进程创建了很多子进程,就是不回收,就会造成内存资源的浪费。因为数据结构对象本身就要占用内存,是要在内存的某个位置进行开辟空间。
- 内存泄漏
为什么要被领养?
因为:当子进程退出的时候,父进程早已不在,需要领养进程来进行回收
- 因为CPU是有限的,进程太多,需要通过某种方式竞争资源!
- 优先权高的进程有优先执行权利,配置进程优先权对多任务环境的linux很有用,可以改善系统性能。
- 还可以把进程运行到指定的CPU上,这样一来,把不重要的进程安排到某个CPU,可以大大改善系统整体性能。
确认是谁应该先获得某种资源,谁后获得该资源。我们可以用一些数据来表明优先级,该数字在PCB结构体内
优先级 = 老的优先级(PRI) + nice值 (NI)
输入 top打开Linux下的任务管理器,再输入 r ,再输入进程的pid , 再输入想要修改的nice值,每一次设置优先级,老的优先级都是80,不会记录上次的PRI值。进程切换:

⭐环境变量(environment variables)一般是指在操作系统中用来指定操作系统运行环境的一些参数。
当用户自己写了一个程序的时候,是需要加 ./ 来指定这个程序的路径才可以运行的,但是为什么而执行系统程序/命令 ls 可以不带路径呢❓
答:因为这与环境变量有关,这些系统命令在环境变量所指向的路径中,而用户自己写的可执行程序不在环境变量所指向的众多路径之中。
用户可以把自己写的可执行程序拷贝到环境变量指向的路径中,但是这样会污染系统的命令池,所以不建议这么做。可以通过输入命令export PATH=$PATH:可执行程序的路径(只在本次登录中被修改,永久生效需要修改配置文件),将用户写的可执行程序的路径放到系统的环境变量中,这时执行用户写的可执行程序的时候就不需要再前面加它的路径了。
echo $NAME //NAME:你的环境变量名称
例如:echo $PATH
int main(int argc, char* argv[], char* env[])
main函数可以带三个参数👆,前两个是命令行参数,第三个参数 char* env[ ] 是每一个进程在启动的时候,启动该进程的进程(父进程)传递给它的环境变量信息,都可以通过该参数传导过来。
char* env[] 是指针数组类型,数组里面存放的是一个个char*的指针,这些指针保存的是环境变量字符串。这些环境变量以指针数组的形式被一个个维护起来,整个数组作为参数传递给main函数。
每个程序都有一个环境表,环境表是一个字符指针数组,每个指针指向一个以’\0’结尾的环境字符串。

argc标识命令行参数中传了几个参数,argv [ ]是命令行参数的指针数组
命令行参数就是用户在启动这个可执行程序的时候,给这个程序传入的选项
. / myprc -a -b,ls -a -lls会被当作argv [0],依次递推
命令行参数可以让我们同样的一个程序通过选项的方式选择使用同一个程序的不同子功能,这个参数也是被父进程bash在命令行先拿到再传递给子进程的。
#include
int main(int argc, char *argv[], char *env[])
{
for(int i = 0;; env[i]; i++)
{
printf("%s\n", env[i]);
}
return 0;
}
#include
int main(int argc, char *argv[])
{
//全局变量environ指向环境变量表,environ没有包含在任何头文件中,所以在使用时 要用extern声明
extern char **environ;
for(int i = 0;; environ[i]; i++)
{
printf("%s\n", environ[i]);
}
return 0;
}
2.通过系统调用获取或设置环境变量
getenv ⭐
#include
#include
int main(int argc, char *argv[])
{
printf("%s\n", getenv("PATH"));⭐
return 0;
}
之前博客中提到过进程的空间布局图:

那么实际上的进程空间真的如此吗,每个区域都符合上述的布局吗❓
这里可以通过代码验证一下👇
1 #include<stdio.h>
2 #include<stdlib.h>
3 int unval;
4 int val=100;
5
6 int main(int argc,char* argv[],char* env[])
7 {
8 int i=0;
9 for(i=0;i<argc;i++)
10 {
11 printf("argv[%d]:%p\n",i,argv[i]);
12 }
13 int j=0;
14 for(j=0;env[j];j++)
15 {
16 printf("env[%d]:%p\n",j,env[j]);
17 }
18 printf("代码区:%p\n",main);
19 printf("init:%p\n",&val);
20 printf("uninit:%p\n",&unval);
21 char*p1=(char*)malloc(16);
22 char*p2=(char*)malloc(16);
23 char*p3=(char*)malloc(16);
24 char*p4=(char*)malloc(16);
25 printf("heap:%p\n",p1);
26 printf("heap:%p\n",p2);
27 printf("heap:%p\n",p3);
28 printf("heap:%p\n",p4);
29
30 printf("stack:%p\n",&p1);
31 printf("stack:%p\n",&p2);
32 printf("stack:%p\n",&p3);
33 printf("stack:%p\n",&p4);
34
35 return 0;
36 }
//malloc了16个字节,但是给了20个字节。
//多出来的字节用来记录这次申请的属性(cookie信息)信息,free的时候也是根据这个来释放空间的。
在Linux下的运行结果如下👇(window下可能会有偏差):

通过一一对比地址信息,我们可以发现,进程地址空间的布局果真如此❗
当使用fork()创建一个子进程并且让该子进程修改一个变量的值,再循环打印变量值和地址可以惊奇的发现同一个地址,同时读取的时候,出现了不同的值❗❗❗

💡结论:
老式的计算机没有虚拟地址的概念,进程可以直接访问物理内存,而内存是随时都可以被读写的,所以进程之间可能互相干扰造成安全问题。根本原因就是由于直接使用物理地址导致的。
所以现代计算机提出了下面的方式:
要访问物理地址,需要先进行映射(虚拟地址->页表->物理地址)。

💡综上所述:
mm_struct,用来描述一个进程所能看到的各个区域,地址空间和页表每个进程都私有一份。地址空间的本质在内核中的区域划分就是在地址空间是一种数据结构,里面至少要有各个区域的划分(区域的划分本质是在一定的范围里定义出start和end):
struct mm_struct
{
int code_start;
int code_end;
int init_start;
int init_end;
int uninit_start;
int uninit_end;
int heap_start;
int heap_end;
int stack_start;
int stack_end;
//....其他的属性
}
范围变化,本质就是对
start或者end标记值+ / -特定的范围
fork函数内部执行完毕后执行return的时候发生了写时拷贝,所以内存中父子进程有属于自己的变量空间,只不过在用户层用同一个虚拟地址来标识,通过不同的页表映射,在物理内存中的不同位置存放。
地址空间并不是内存,而是操作系统内为进程专门设计的一种内核数据结构,里面包含的重点区域是关于各个区域的划分,所以地址空间中会存在大量的start和end还有其他的更多属性。
所谓的地址空间本质上是操作系统看待内存的一种方案
- 凡是非法的访问或者映射,操作系统都会识别到并终止此进程。
-地址空间和页表是操作系统来维护的,也就意味凡是使用地址空间 +页表进行映射,也一定要在操作系统的监管下来进行访问。
因为有地址空间和页表的存在,我们的物理内存中,可以对的数据进行任意位置的加载,物理内存的分配就可以做到进程的管理做到没有关系。
本质上,因为有地址空间的存在,所以上层申请空间,其实是在地址空间上申请的,物理内存可以甚至一个字节都不给你,而当真正进行对物理地址内存空间访问的时候,才执行内存相关的管理算法, 帮你申请内存,构建页表映射关系,然后让你进行内存的访问(这一切是由操作系统自动完成,用户零感知。)。
因为在物理内存中理论上可以任意位置加载,物理内存中的几乎所有的数据和代码在内存中是乱序的。
但是因为页表的存在,可以将地址空间上的虚拟地址和物理地址进行映射,是在进程的视角,所有的内存分布都是有序的。
因为由地址空间的存在,每个进程都认为字节拥有4GB空间(32位),并且每个区域是有序的,进而可以通过页表映射到不同的区域,来实现进程的独立性,每个进程不知道其他进程的存在。
💡补充知识: