文章目录
- 一、了解冯诺依曼体系结构
- 1、概念
- 2、对数据层面
- 3、实例
- 二、操作系统
- 1、概念
- 2、设计OS的目的
- 3、定位
- 4、操作系统怎么管理?
- 三、进程
- 1、概念
- 2、怎么管理进程
- 3、描述进程-PCB
- 4、描述进程怎么运行(粗略)
- 5、进程属性
- 6、创建子进程
- 7、创建多进程
- 8、进程状态
- 9、进程的优先级
- 10、进程切换
- 11、命令行参数
- 12、环境变量
- 13、虚拟地址空间
- 14、进程创建
- 15、进程终止
- 16、进程替换
一、了解冯诺依曼体系结构
1、概念
冯·诺依曼体系结构是由数学家冯·诺依曼在1945年首次提出的,它规定了计算机的基本组成部分以及各部分之间的数据流向。这套理论包括采用二进制逻辑、程序存储执行以及计算机由五个基本部分组成,即运算器、控制器、存储器、输入设备和输出设备。
- 运算器:用于完成各种算术运算、逻辑运算和数据传送等数据加工处理。它是计算机中进行数据加工的核心部件。
- 控制器:用于控制程序的执行,是计算机的大脑。它根据存放在存储器中的指令序列(程序)进行工作,并由一个程序计数器控制指令的执行。控制器具有判断能力,能根据计算结果选择不同的工作流程。运算器和控制器共同组成计算机的*处理器(CPU)。
- 存储器:用于记忆程序和数据。在冯·诺依曼体系结构中,程序和数据以二进制代码形式不加区别地存放在存储器中,存放位置由地址确定。这种设计使得程序和数据可以共享同一块存储空间,从而提高了计算机的灵活性和效率。(这里指的是内存)
- 输入设备:用于将数据或程序输入到计算机中。常见的输入设备包括键盘、鼠标等。
- 输出设备:用于将数据或程序的处理结果展示给用户。常见的输出设备包括显示器、打印机等。
2、对数据层面
不考虑缓存情况,这里的CPU能且只能对内存进行读写,不能访问外设(输入或输出设备),外设(输入或输出设备)要输入或者输出数据,也只能写入内存或者从内存中读取,即所有的设备只能和内存直接打交道。
3、实例
使用qq发信息
二、操作系统
1、概念
任何计算机系统都包含一个基本的程序集合,称为操作系统(OS)。笼统的理解,操作系统包括:
内核(进程管理,内存管理,文件管理,驱动管理)
其他程序(例如函数库,shell程序等等)
2、设计OS的目的
- 与硬件交互,管理所有的软硬件资源。
- 为用户程序(应用程序)提供一个良好的执行环境。
3、定位
在整个计算机软硬件架构中,操作系统的定位是:一款纯正的“搞管理”的软件。
4、操作系统怎么管理?
总结:
计算机管理硬件
- 描述起来,用struct结构体
- 组织起来,用链表或其他高效的数据结构
三、进程
1、概念
课本概念:程序的一个执行实例,正在执行的程序等。
内核观点:担当分配系统资源(CPU时间,内存)的实体。
2、怎么管理进程
和操作系统管理硬件一样:先描述,再组织。
3、描述进程-PCB
(1)进程信息被放在一个叫做进程控制块的数据结构中,可以理解为进程属性的集合。
(2)task_struct是PCB的一种:在Linux中描述进程的结构体叫做task_struct,task_struct是Linux内核的一种数据结构,它会被装载到RAM(内存)里并且包含着进程的信息。
(3)task_ struct内容分类
- 标示符: 描述本进程的唯一标示符,用来区别其他进程。
- 状态: 任务状态,退出代码,退出信号等。
- 优先级: 相对于其他进程的优先级。
- 程序计数器: 程序中即将被执行的下一条指令的地址。
- 内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针
- 上下文数据: 进程执行时处理器的寄存器中的数据[休学例子,要加图CPU,寄存器]。
- I/O状态信息: 包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。
- 记账信息: 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。
- 其他信息
4、描述进程怎么运行(粗略)
综上:进程 = 内核的数据结构 + 代码数据。
5、进程属性
(1)进程标识符
- 进程id:PID
- 父进程id: PPID
(2)查看进程信息
进入/proc系统文件查看
具体数字为进程PID,如查看PID为1的进程,cd /proc/1。
(3)进程中的exe和cwd
exe:指向的是磁盘中的可执行文件。
运行时可执行文件存在时:
运行时可执行文件不存在时:
cwd:指的是当前工作的路径,如创建文件时就会使用当前的工作路径。
通过ps指令进行查看进程
功能:
显示当前系统中的进程状态。
用法:
ps [选项][PID等信息]
常用选项:
ps -u [username]:显示指定用户的进程信息。
ps -p [PID]:显示指定PID的进程信息。
ps -C[cmdname]:显示指定命令名的进程信息。
ps -o [format]:自定义输出格式,例如ps -o pid,user,cmd只显示PID、用户和命令。
ps aux:这是一个非常常用的组合选项,a显示与终端相关的所有进程(包括其他用户的进程),u以用户为中心的格式显示进程信息,x显示没有控制终端的进程。
使用:
启动一个进程,通过ps结合grep指令打印进程信息
ps -ajx | head -1 && ps ajx | grep test01 | grep -v grep
在linux系统启动后任何进程都是由其父进程创建出来的。
6、创建子进程
(1)创建子进程函数
pid_t fork(void)
(包含在头文件#include <unistd.h>
中)。
(2)返回值
有两个返回值,对于父进程返回子进程PID,对于子进程返回 0(为什么出现两个返回值下面进行解释)。
(3)使用
getpid():获取当前进程PID
getppid():获取当前进程PPID
头文件和fork()一样
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<sys/types.h>
4 int main()
5 {
6 pid_t pid = fork();
7 printf("返回值:%d\n",pid);
8 while(1)
9 {
10
11 if(pid == 0)
12 {
13 printf("我是一个子进程,pid:%d ppid:%d\n",getpid(),getppid());
14 }
15 else
16 {
17 printf("我是一个父进程,pid:%d ppid:%d\n",getpid(),getppid());
18 }
19 sleep(1);
20 }
21
22
23
24 return 0;
25 }
解释为什么会出现两个返回值:
进程 = 内核数据结构+代码数据
一般对于父子进程来说:子进程拷贝父进程的内核数据结构(子进程的一些属性会修改),代码共享父子之间共享,数据独有一份(因为进程运行间具有很强的独立性,互不干扰)。
7、创建多进程
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<sys/types.h>
4
5 void func()
6 {
7 while(1)
8 {
9
10 sleep(1);
11 printf("我是一个子进程,pid:%d ppid:%d\n",getpid(),getppid());
12 }
13 }
14 int main()
15 {
16 int arr[10]; //记录子进程id
17 for(int i = 0; i < 10; i++)
18 {
19 pid_t pid = fork(); //创建子进程
20 arr[i] = pid;
21 if(pid == 0) //如果是子进程就走这个逻辑
22 func();
23 }
24
25 printf("我是一个父进程,pid:%d ppid:%d\n",getpid(),getppid());
26 printf("我的子进程:");
27 for(int i = 0; i < 10; i++) //输入子进程
28 {
29 printf(" %d ",arr[i]);
30 }
31 printf("\n");
32 while(1); //不让父进程结束
33 return 0;
34 }
创建这些进程后,谁先运行呢?
这是由os的调度器决定的。
8、进程状态
(1)进程状态图
时间片:
进程的时间片(Time Slice 或 TimeQuantum)是操作系统在多任务处理(Multitasking)或多线程处理(Multithreading)环境下,分配给每个进程或线程的一段固定时间,用于其执行。这种机制允许操作系统公平地分配CPU资源,确保所有进程或线程都能获得执行机会,从而避免某个进程或线程长时间占用CPU资源,导致其他进程或线程长时间等待。
(2)理解进程状态图
(3)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): 意味着进程在等待事件完成(这里的睡眠有时候也叫做可中断睡眠 (interruptible sleep))。
D磁盘休眠状态(Disk sleep)有时候也叫不可中断睡眠状态(uninterruptible sleep),在这个状态的 进程通常会等待IO的结束。
T停止状态(stopped): 可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运行。
t 跟踪(tracing stop)状态。:当进程正在被调试器(如gdb)跟踪时,如果调试器在进程中的某个位置设置了断点,当进程运行到该断点时,它会停下来并进入跟踪状态。此时,进程的状态就会显示为‘t’。
X死亡状态(dead):这个状态只是一个返回状态,你不会在任务列表里看到这个状态。
(4)Z(zombie)-僵尸进程
补充知识:
进程退出:
1、代码不会执行了,首先退出程序代码和数据。
2、进程退出要有退出信息,通过task_strcut保存.
3、操作系统要维护task_strcut,方便用户获取进程退出信息。
僵死状态(Zombies)是一个比较特殊的状态。当进程退出并且父进程没有读取到子进程退出的返回代码时就会产生僵死(尸)进程,此时需要维护子进程的task_strcut,等待父进程读取子进程退出的返回代码,当父进程读取后子进程就会自动退出。
僵死进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码。
所以,只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程进入Z状态。
如下面程序:
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<sys/types.h>
4
5 int main()
6 {
7 pid_t pid = fork();
8 while(1)
9 {
10 if(pid == 0)
11 {
12 printf("我是一个子进程,pid:%d ppid:%d\n",getpid(),getppid());
13 }
14 else if(pid > 0)
15 {
16 printf("我是一个父进程,pid:%d ppid:%d\n",getpid(),getppid());
17 }
18 sleep(1);
19 }
20
21 return 0;
22 }
使用指令kill -9 pid 结束子进程后子进程进入僵尸状态。
僵尸进程危害
进程的退出状态必须被维持下去,因为他要告诉关心它的进程(父进程),你交给我的任务,我办的怎么样了。可父进程如果一直不读取,那子进程就一直处于Z状态?是的!
维护退出状态本身就是要用数据维护,也属于进程基本信息,所以保存在task_struct(PCB)中,换句话 说,Z状态一直不退出,PCB一直都要维护?是的!
那一个父进程创建了很多子进程,就是不回收,是不是就会造成内存资源的浪费?是的!因为数据结构对象本身就要占用内存,想想C中定义一个结构体变量(对象),是要在内存的某个位置进行开辟空
间! 内存泄漏?是的!
进程等待
进程等待的必要性:
对于上述的子进程退出父进程一直不读取子进程退出的返回码所导致的僵尸进程,父进程等待子进程退出然后读取子进程退出的返回码就是一个很好的解决办法。
如何进行进程等待?
wait()方法。
返回值: 成功返回被等待进程pid,失败返回-1。
参数: 输出型参数,获取子进程退出状态,不关心则可以设置成为NULL。
waitpid方法
返回值:
当正常返回的时候waitpid返回收集到的子进程的进程ID; 如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0; 如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;
参数:
pid: pid=-1,等待任一个子进程。与wait等效。 Pid>0.等待其进程ID与pid相等的子进程。
status:
WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
options: WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的ID。
status参数解析:
- 低7位:表示子进程退出时的终止信号编号。在Linux中,信号是一种软件中断,用于通知进程某个事件的发生。信号编号是一个整数,每个信号都有一个唯一的编号。
- 第8位:称为coredump标志位。当该位为1时,表示子进程因为接收到某些信号(如SIGSEGV)而产生了核心转储文件。核心转储文件是操作系统在程序异常终止时,将程序当时的内存状态记录下来并保存在一个文件中的行为。
0:表示子进程没有产生核心转储文件。
1:表示子进程产生了核心转储文件
求coredump位的值:((status >> 7) & 0x1- 次低8位:代表子进程的退出状态码,即进程退出时的退出码。通常,0表示成功完成,而非0的值表示出现了错误或异常情况。这个退出状态码可以被其他进程、shell脚本或程序捕获并进行处理。
- 求退出信息
WIFEXITED(status):检查子进程是否正常退出。如果子进程是因为接收到信号而终止的,则该函数返回0;否则返回非0值。
WEXITSTATUS(status):如果子进程正常退出,该函数返回子进程的退出状态码(即次低8位的值)。
WIFSIGNALED(status):检查子进程是否因为接收到信号而终止。如果是,则返回非0值;否则返回0。
WTERMSIG(status):如果子进程因为接收到信号而终止,该函数返回导致子进程终止的信号的编号。
wait/waitpid退出情况分析:
- 如果子进程已经退出,调用wait/waitpid时,wait/waitpid会立即返回,并且释放资源,获得子进程退出信息。
- 如果在任意时刻调用wait/waitpid,子进程存在且正常运行,则进程可能阻塞。
- 如果不存在该子进程,则立即出错返回。
阻塞式等待
直到子进程结束才继续向下执行其他代码。
1 #include<stdio.h>
2 #include<unistd.h>
3 #include <sys/wait.h>
4 #include<sys/types.h>
5
6 void func()
7 {
8 printf("我是一个子进程\n");
9 }
10
11
12 int main(int argc ,char * argv[])
13 {
14 pid_t pid = fork();
15
16 if(pid == 0)
17 {
18 func();
19 sleep(5);
20 }
21 else if(pid > 0)
22 {
23 int status = 0;
24 pid_t ret = waitpid(-1, &status, 0);//阻塞式等待,等待5S
25 if(ret > 0)
26 {
27 printf("等待成功,子进程id:%d\n",ret);
28 }
29 else if(ret == 0)
30 {
31 printf("没有子进程等待\n");
32 }
33 else
34 {
35 printf("出现错误\n");
36 }
37 }
38 return 0;
39 }
非阻塞等待
循环的等待子进程退出,在等待的过程父进程可以做一些自己的工作。
1 #include<stdio.h>
2 #include<unistd.h>
3 #include <sys/wait.h>
4 #include<sys/types.h>
5
6 void func()
7 {
8 while(1)
9 {
10 printf("我是一个子进程\n");
11 sleep(1);
12 }
13 }
14
15
16 int main(int argc ,char * argv[])
17 {
18 pid_t pid = fork();
19
20 if(pid == 0)
21 {
22 func();
23 }
24 else if(pid > 0)
25 { while(1)
26 {
27 int status = 0;
28 pid_t ret = waitpid(-1, &status, WNOHANG);//非阻塞等待
29
30 if(ret > 0)
31 {
32 printf("等待成功\n");
33 break;
34 }
35 else if(ret < 0)
36 {
37 printf("等待子进程失败\n");
38 }
39 else
40 {
41 printf("进程还没退出\n");
42 }
43 sleep(1);
44 }
45 }
46 return 0;
47 }
(5)孤儿进程
父进程如果提前退出,那么子进程后退出,进入Z之后,那该如何处理呢?父进程先退出,子进程就称之为“孤儿进程”,孤儿进程被1号init进程领养,当然要有init进程回收喽。
如下面程序:
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<sys/types.h>
4
5 int main()
6 {
7 pid_t pid = fork();
8 while(1)
9 {
10 if(pid == 0)
11 {
12 printf("我是一个子进程,pid:%d ppid:%d\n",getpid(),getppid());
13 }
14 else if(pid > 0)
15 {
16 printf("我是一个父进程,pid:%d ppid:%d\n",getpid(),getppid());
17 }
18 sleep(1);
19 }
20
21 return 0;
22 }
使用指令kill -9 pid 结束父进程后:
9、进程的优先级
(1)概念
- cpu资源分配的先后顺序,就是指进程的优先权(priority)。
- 优先权高的进程有优先执行权利。配置进程优先权对多任务环境的linux很有用,可以改善系统性能。
- 还可以把进程运行到指定的CPU上,这样一来,把不重要的进程安排到某个CPU,可以大大改善系统整体性能。
(2)查看进程的优先级
运行程序后,输入指令 ps -l
查看
关注下面两个信息
PRI :代表这个进程可被执行的优先级,其值越小越早被执行
NI :代表这个进程的nice值
PRI and NI
- PRI也还是比较好理解的,即进程的优先级,或者通俗点说就是程序被CPU执行的先后顺序,此值越小 进程的优先级别越高。
- 那NI呢?就是我们所要说的nice值了,其表示进程可被执行的优先级的修正数值,
- PRI值越小越快被执行,那么加入nice值后,将会使得PRI变为:PRI(new)=PRI(old)+nice
这样,当nice值为负值的时候,那么该程序将会优先级值将变小,即其优先级会变高,则其越快被执行。- 所以,调整进程优先级,在Linux下,就是调整进程nice值 nice其取值范围是-20至19,一共40个级别。
PRI vs NI
- 需要强调一点的是,进程的nice值不是进程的优先级,他们不是一个概念,但是进程nice值会影响到进程的优先级变化。
- 可以理解nice值是进程优先级的修正修正数据
(3)修改进