文章目录
进程概念
1.冯诺依曼体系
我们常见的计算机,如笔记本。我们不常见的计算机,如服务器,大部分都遵守冯诺依曼体系。

输入单元:包括键盘, 鼠标,扫描仪, 写板等。
中央处理器(CPU):含有运算器和控制器等。
输出单元:显示器,打印机等。

该图越靠近下面运行速度越慢,容量越大,价格越便宜。
在硬件数据流动角度,在数据层面:
1.cpu不直接和外设打交道,只和内存打交道。
2.外设不直接把数据传给cpu,而是先传给内存。
程序运行,为什么要加载到内存?
程序=代码+数据,程序和数据都要被cpu访问,cpu只会从内存中访问,程序没有被加载到内存时,
是在硬盘中(外设)。
2.操作系统
2.1操作系统概念
操作系统os(Operator System),是一个软件,对软硬件进行资源管理。
广义认识:内核(进程管理,内存管理,文件管理,驱动管理)和外壳周边程序(例如函数库,shell程序等等)。
狭义认识:只是操作系统的内核。
2.2操作系统的结构
操作系统树状结构
2.3为什么要有操作系统?
1.对硬件进行管理(手段)。
2为用户提供一个良好的(高效,稳定,安全)运行环境(目的)。
2.4如何理解操作系统
学校里面校长对学生管理,并不是校长直接面对学生管理,而是校长对学生的数据进行管理。比如学校有教务系统,
校长只需要对教务系统学生信息进行管理。任何管理都是先描述,即创建结构体对成员进行描述,在组织,即通过数
据结构进行管理。
3.系统调用和库函数概念
1.在开发角度,操作系统对外会表现为一个整体,但是会暴露自己的部分接口,供上层开发使用,这部分
由操作系统提供的接口,叫做系统调用。
2.系统调用在使用上,功能比较基础,对用户的要求相对也比较高,所以,有心的开发者可以对部分系统
调用进行适度封装,从而形成库,有了库,就很有利于更上层用户或者开发者进行二次开发。
4.进程
4.1进程概念
1.课本概念:程序的一个执行实例,正在执行的程序等。
2.内核观点:担当分配系统资源(CPU时间,内存)的实体。
4.2描述进程
1.进程信息被放在一个叫做进程控制块的数据结构中,可以理解为进程属性的集合。
2.课本上称之为PCB(process control block),Linux操作系统下的PCB是task_struct。
3.进程=pcb+代码。
4.3task_struct PCB
task_struct PCB
{
//所有属性。
task_struct PCB*next;//内存指针
}
PCB就是对进程的描述,每一个进程就有一个PCB,对进程管理就成了对链表管理。
4.4进程调度

其中内存中PCB以链表形式被存于task_ queue中,cpu对进程调度就是对该队列进行访问。
调度运行本质就是让进程控制块task_struct进行排队
5.进程查看
查看进程:
1.ps axj | head -1 //查看进程栏信息。
2.ps axj | grep mypress //查看myoress进程信息。
合起来ps axj | grep mypress && ps axj | head -1 //



使用while :; do ps ajx | head -1 && ps ajx | grep Process | grep -v grep; sleep 1; done指令//可实现每秒刷新查看进程信息的功能。
6.通过系统调用获取进程标示符
进程id(PID)
父进程id(PPID)
获得进程id:getpid()
获得父进程id:getppid()

7.创建和关闭进程
7.1创建进程
使用fork函数

7.2fork函数使用
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main ()
{
int bin = fork ();//这里代码就会创建一个子进程,并且返回两个pid_t值,一个是给父进程,该值大于1,
// 另一个是给子进程,该值等于0。
if(bin==0)//让子进程只执行该代码。
{
while(1)
{
printf("I am child,my id:%d,my parent id:%d\n",getpid(),getppid());
sleep(1);
}
}
else if(bin>0)//让父进程只执行该代码。
{
while(1)
{
printf("I am parent,my id:%d,my parent id:%d\n",getpid(),getppid());
sleep(1);
}
}
return 0;
}

我们发现程序存在两个进程,并且子进程的父id是原进程的id。
7.3关闭进程
1.在终端输入:kill -9 pid 关闭进程。
2.只能终止前台进程:CTRL + c。
7.4进程工作路径
进程在启动时会记录当前在那个路径,当我们fopen("log.txt","w")(log.txt不存在);创建一个文件时,就
会在当前记录路径下创建文件。我们可以通过chdir更改进程工作目录:

chdir("/home/zbb/linux");//字符串为更改后路径。
8.进程状态
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): 意味着进程在等待事件完成(这里的睡眠有时候也叫做可中断睡眠(interruptible
sleep))。
D磁盘休眠状态(Disk sleep)有时候也叫不可中断睡眠状态(uninterruptible sleep),在这个状态的进程
通常会等待IO的结束。
T停止状态(stopped): 可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可以通过发
送 SIGCONT 信号让进程继续运行。
X死亡状态(dead):这个状态只是一个返回状态,你不会在任务列表里看到这个状态。
8.1 R运行状态(running)
进程运行的状态
8.2 S睡眠状态(sleeping)
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main ()
{
while(1)
{
printf("hello world\n");
}
return 0;
}

观察上述进程STAT,发现进程状态为S,即睡眠状态,进程的睡眠状态(Sleep State) 是进程生命周期中的一种
核心状态,指进程暂时放弃 CPU 使用权,等待特定事件(如资源就绪、时间到、信号触发等)发生后,才能被重新调度
执行的状态。上述代码中,cpu快速运行代码,但是printf,系统需要调用外设资源,因此cpu等待资源就绪。
且该状态可被ctrl c中断,即是可中断睡眠。
8.3 T/t暂停状态(stopped)
kill -l查看控制进程状态的指令:

kill -18 pid :将暂停的进程继续运行。
kill -19 pid :将运行的进程暂停。
T: 是用户通过作业控制主动暂停的进程;
t: 是被调试器跟踪并暂停的进程。
8.4 D磁盘休眠状态(Disk sleep)
D磁盘休眠状态(Disk sleep)也叫不可中断睡眠状态(uninterruptible sleep)。在 Linux 操作系统中,
处于该状态的进程通常正在等待磁盘 I/O 操作完成。相关介绍如下:
特点:与普通睡眠状态不同,处于 D 磁盘休眠状态的进程不能被外部信号唤醒,也不能被轻易杀死,因为 I/O 操
作的完成是进程继续执行的必要条件,任何外部信号都不能中断这一过程。若强制终止,可能会导致磁盘数据不一致,破
坏文件系统的完整性。
8.5 Z僵尸进程(Zombie)
进程已终止,但内核仍保留其部分信息(如进程 ID、退出状态、资源使用统计等)未释放的状态。
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main ()
{
int num = fork();
int a=5;
if(num>0)
{
while(1)
{
printf("I am process\n");
sleep(1);
}
}
else if(num==0)
{
while(a)
{
printf("I am child\n");
a--;
sleep(1);
}
}
return 0;
}

我们发现子进程结束后,子进程状态是Z,该状态是僵尸进程。进程 = 内核数据结构 + 进程的代码和数据 ,进
程已经运行完毕,但是需要维持自己的退出信息,未来让父进程进行读取,如果没有父进程读取,僵尸进程会一直存在!
kill -9 无法关掉僵尸进程。
8.6 孤儿进程(Orphan)
孤儿进程(Orphan Process) 是指父进程先于子进程退出,导致子进程失去父进程管理的一类特殊进程。这类进程看似 “无主”,但操作系统会通过特定机制避免其失控,最终确保资源被正常回收。
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main ()
{
int num = fork();
int a=5;
if(num>0)
{
while(a)
{
printf("I am parent,my pid=%d,ppid=%d\n",getpid(),getppid());
sleep(1);
a--;
}
}
else if(num==0)
{
while(1)
{
printf("I am child,my pid=%d,ppid=%d\n",getpid(),getppid());
sleep(1);
}
}
return 0;
}


我们发现,父进程结束后,子进程还存在,因为进程间是独立的。子进程的父进程变成了1,即被OS本身进行领养了
,系统要确保子进程正常被回收。
8.7 进程的并发和并行
-
并发(Concurrency)
定义:指在同一时间段内,多个进程 / 线程被交替执行(宏观上看起来像同时进行),但在同一时刻,只有一个任务在 CPU 上运行。 -
并行(Parallelism)
定义:指在同一时刻,多个进程 / 线程在不同的 CPU 核心上同时执行(真正意义上的 “同时进行”)。
8.8 进程的阻塞和挂起
- 阻塞暂停:阻塞状态的存在是为了提高 CPU 利用率,当进程无法继续执行时(如等待 I/O),操作系统会将 CPU 资源分配给其他可运行的进程,而不是让 CPU 处于空闲状态。

比如代码中有scanf时,进程等待键盘响应时,为了不影响后面进程运行,该进程的task_struct,会被放到进程
调用的硬件结构体中,一个struct waitqueue等待队列中,当操作系统检测到了硬件的响应,才会重新将该进程的tas
k_struct放回到struct runqueue中,重新被CPU运行。
- 挂起状态:在操作系统中,进程的挂起状态(Suspended State) 是进程生命周期中的一种特殊状态,指进程暂时脱离 CPU 调度队列,即使 CPU 空闲也无法被调度执行的状态。其核心特征是进程的执行被 “暂停”,且暂停原因与 CPU 是否空闲无关,需通过显式的 “唤醒” 操作才能恢复到可调度状态。

如上图当内存吃紧时,OS会把暂时不用的进程,的代码换出磁盘的Swap分区。当进程需要使用时,在其入运行队列
前将其代码换入内存中。
挂起的本质是以时间换空间。
9.进程切换的有关内容
9.1 进程切换的临时数据
- CPU对进程切换时,会将进程的临时数据完整保存 + 精准恢复,即先将当前进程的临时据(进程上下文)完整保存至内存,再加载待运行进程的上下文,确保切换前后进程逻辑不中断、数据不丢失。
- 当内核决定切换进程时,首先将当前运行进程(旧进程)的全部上下文完整保存至其专属的 “进程控制块(PCB)” 中。
- CPU内部所有临时数据,叫做进程的上下文。
9.2 CPU内的寄存器:
- 寄存器本身是硬件,具有数据的存储能力,CPU的寄存器硬件只有一套。
- CPU内的数据,可以有多套,有几个进程,就有几套和该进程对应的上下文数据。
10. 进程的优先级
10.1 什么是优先级
- 概念:指定进程获取某种资源的先后顺序。Linux中优先级数字越小,优先级越高。
- 优先级vs权限:有优先级了,说明我们已经有获得资源的权限了,只需要按顺序获取。
10.2 为什么要有优先级?
- 原因:进程访问的资源(CPU)始终都是有限的,系统中进程大部分情况都是有较多的。
10.3 Linux优先级的特点&&查看方式
使用指令ps -al:

上图中:
PRI:进程优先级。
NI:进程优先级的修正数据,NICE值,NICE值范围为[-20,19],新的PRI = 80 +nice,达到对于进程优
先级动态修正过程。
11. 命令行参数
11.1引入(命令行参数)
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main (int argc,char *argv[])
{
int i;
for( i=0;i<argc;i++)
{
printf("argv[%d]->%s\n",i,argv[i]);
}
return 0;
}
我们给上述代码main函数加上参数,这些参数是什么呢?

我们执行该指令,发现代码运行结果为图上,
char *argv[]:这个参数将我们输入的命令,以空格为界分成子串存储在该数组中。
int argc:这个参数记录我们子串的数量。
这些参数就叫命令行参数。
11.2 命令行参数作用
本质:命令行参数本质是交给我们程序不同的选项,用来定制不同的程序功能。命令中会携带很多的选项。
12. 环境变量
12.1. 环境变量中PATH
为什么我们运行ls,这种命令不需要代文件地址;而我们运行process这种自己写的可执行程序需要带上具体地址。


这是因为,在 Linux 系统中,环境变量是一组动态命名的值,用于控制系统和应用程序的行为。如环境变量中有
一个叫PATH,可执行程序的搜索路径,用冒号分隔。(echo $PATH打印里面内容)

PATH里面是一个个地址用 ‘:’ 隔开,bash在执行命令时,需要找到命令,bath会去PATH中查找。
12.2. 内存层面上更改环境变量
系统中有很多配置文件,在我们登录Linux时,配置文件会被加载到bath(内存)中,因此我们上面查看的PATH是
在内存层面上。但是因为配置文件没改变,所以当我们重新登陆系统时,bath会被重新写入配置文件中的环境变量。
内存层面上更改PATH:
PATH=$PATH:地址

这样我们自己的可执行程序就可以直接使用文件名运行了。
12.3 更改操作系统中配置文件中的环境变量
vim /home/zbb.bash_profile \\家目录下,有个叫.bash_profile的隐藏文件。

在家目录下,使用vim .bash_profile打开该配置文件:

在第10行代码后面加上“:地址名” 。就可以从配置文件上更改环境变量PATH。
12.4 其他环境变量
PATH:可执行程序的搜索路径,用冒号分隔
HOME:当前用户的主目录
USER:当前用户名
LANG:系统语言和编码设置
PWD:当前工作目录
SHELL:当前使用的 shell
LOGNAME:当前用户的登录名
12.5 环境变量的操作
1. 直接执行 env:列出当前所有环境变量(名称 = 值的形式),包括系统级(如 PATH、HOME)和用户级变量。
比如:env | grep PATH,查找PATH有关的环境变量信息。
2. echo $:打印变量值(语法需补全,$ 后需跟变量名)。
3. export 变量名=值:直接定义并导出环境变量(一步完成 “定义 + 提升”),如export PATH=$PATH:/op
t4./bin(该操作是写入内存中的,不是配置文件中)。
4. unset 变量名:删除指定的单个变量,如:export PATH=$PATH:/opt/binunset PATH 。
5. 变量名=值:定义本地变量 ,如:var=123 。
13.结合程序系统理解环境变量
13.1 环境变量表
头文件<unsitd>中,会存在一个extern char **environ:

运行代码:
int main ()
{
extern char** environ;
int i;
for( i=0;environ[i];i++)
{
printf("environ[%d]->%s\n",i,environ [i]);
}
return 0;
}
结果:

char **environ:environ 是一个指针,指向一个数组;数组中的每个元素也是一个指针,指向一个字符串(即
单个环境变量,格式为 KEY=VALUE,如 PATH=/usr/bin)。
结论:
bash进程启动的时候,默认会给我子进程形成两张表:argv[]命令行参数表,env[] 环境变量表。bash通过各种
方式交给子进程。而我们上面的char **environ就指向env[]这个表。
环境变量具有全局属性,因为环境变量会被父进程传给子进程。
int main(char *env[])\\我们也可以通过这种方式访问环境变量。
char *path = getenv("PATH");\\我们也可以通过这种方式,获取环境变量,失败了返回NULL。
13.2 内建命令
在计算机领域,内建命令(Built-in Command) 指由操作系统的命令行解释器(Shell,如 Linux 的 Bash、
Windows 的 CMD/PowerShell)直接集成的命令,无需调用外部可执行文件(.exe、.bin 等)即可运行,直接在ba
sh中以函数的形式执行。
14. 地址空间
14.1 引入
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main ()
{
pid_t num=fork();
int i=0;
int g_val=1000;
if(num==0)
{
while(1)
{
printf ("I am child,pid:%d,ppid:%d,g_val:%d,&g_val:%p\n",getpid(),getppid(),g_val,&g_val);
sleep(1);
int val=g_val;
if(i==5)
{
g_val=500;
printf("child of g_val:%d->%d\n",val,g_val);
}
i++;
}
}
else
{
while(1)
{
printf ("I am father,pid:%d,ppid:%d,g_val:%d,&g_val:%p\n",getpid(),getppid(),g_val,&g_val);
sleep(1);
}
}
return 0;
}

上述代码运行结果我们发现,子进程的g_val改变成了500,父进程的g_val还是1000,这个我们可以理解,因为父
子进程具有独立性。但是为什么g_val的地址都是一样的,&g_val:0x7ffcbf81de60?同一个地址又是怎么存两个不一
样的值呢?
14.2 虚拟地址

上面父子进程中g_val的地址0x7ffcbf81de60,并不是真正的物理地址,而是操作系统中的虚拟地址。当创建子
进程时,子进程会把父进程的很多内核数据结构浅拷贝一份。当子进程对拷贝的数据只读时,他和父进程的虚拟空间是指
向同一个物理地址。当对数据写时,使子进程页表中0x7ffcbf81de60地址重新指向一个新的物理地址,然后对新的物理
地址中内容修改。
14.3 地址空间理解
Linux 为每个进程提供了 “独立且连续” 的虚拟地址空间,但这个空间并非直接对应物理内存,而是通过操作系统
和硬件(MMU,内存管理单元)的协作,将虚拟地址 “映射” 到实际的物理地址。这种设计从根本上解决了物理内存资源
竞争、地址安全隔离、内存利用率低等问题。
14.4 进一步理解页表
CPU访问进程物理内存中数据是要结合虚拟地址和进程的页表。页表中还存有数据的访问权限,比如对于const 数
据,页表中的权限信息只有读,没有写。
15. Linux中进程调度队列

nr_active
bitmap[5]
queue[140]
其中queue[140],[60,99] 可以存放进程,cpu按照该队列分时运行进程。注意这里的细节:为什么队列 [60,
99] 可存进程呢?我们前面学优先级时,知道NICE值范围是[-20,19],而进程值等于80+NICE,所有进程范围为[60,
99]。分时访问指的是,CPU在很短时间访问进程队列queue,确保每个进程能被访问到。
bitmap [5] 的类型为long,说明有5*4*32个比特位,如果进程队列中某个位置存放有进程,就在这些比特位对
应的位置改为1,cpu访问bitmap[0]等于0,说明队列0到31的位置没有存放进程,以此类推,可以大大提高cpu查找进
程的速度。
nr_active是用来访问进程队列中的进程的。
15.2 Linux中进程调度队列工作原理

上面*active是活跃进程,它指向的是上面的运行队列;*expired是过期进程,指向的是下面的运行队列。他们
的工作原理是:CPU只会访问活跃进程中的进程,被访问的进程会重新放入到过期进程的队列中,活跃进程里的进程只
出不进;而过期进程里的进程只进不出。当活跃进程中进程访问完了,*active和*expired指向的内容交换,重复运
行。
在系统当中查找一个最合适调度的进程的时间复杂度是一个常数,不随着进程增多而导致时间成本增
加,我们称之为进程调度O(1)算法!
955

被折叠的 条评论
为什么被折叠?



