距离上一次利用高并发技术实现360度行车记录仪功能已经过去半年了。开始写一系列关于系统编程和网络编程内容进行总结。
温故而知新,欢迎大家讨论学习。
第一次复习时间2021-09-02
补充点: 虚拟地址空间的概念
僵尸进程和孤儿进程更为具体的描述
1 进程相关概念
一些比较基本的概念,如果学过操作系统,这些内容都很常见,可以适当跳过…
- 单道程序设计
- 多道程序设计
- 微观串行、宏观并行 时间片
- 同步 和 异步
- 并发和并行
相关术语参考链接(重)
1.1 程序和进程
“程序(Program)”是一个静态的概念,一般对应于操作系统中的一个可执行文件
执行中的程序叫做进程(Process),是一个动态的概念。现代的操作系统都可以同时启动多个进程。比如:我们在用酷狗听音乐,也可以使用eclipse写代码,也可以同时用浏览器查看网页。
1.2 并发
并发是多个任务交替执行的,多个任务之间可能还是串行的。所有的并发处理都有排队等候,唤醒和执行这三个步骤。所以并发是宏观的观念,在微观上他们都是序列被处理的,只不过资源不会在某一个上被阻塞(一般是通过时间片轮转),所以在宏观上多个几乎同时到达的请求同时在被处理。如果是同一时刻到达的请求也会根据优先级的不同,先后进入队列排队等候执行。并发针对的是多个请求,比如:一个CPU,一个web服务,同时涌入多个请求,CPU需要交替切换的执行多个请求,而不是一个请求执行完成之后再执行下一个请求。并发的实质是一个物理CPU(也可以是多个物理CPU)在若干个程序之间多路复用,并发性是对有限物理资源强制行使 多用户共享以提高效率。
1.3 虚拟内存和虚拟地址空间
参考链接
虚拟内存:虚拟内存是一种逻辑上扩充物理内存的技术。基本思想是用软、硬件技术把内存与外存这两级存储器当做一级存储器来用。虚拟内存技术的实现利用了自动覆盖和交换技术。简单的说就是将硬盘的一部分作为内存来使用。
虚拟地址空间:在32位的i386 CPU的地址总线的是32位的,也就是说可以寻找到4G的地址空间。我们的程序被CPU执行,就是0x000000000xFFFFFFFF这一段地址中。高1G的空间为内核空间,由操作系统调用,低3G的空间为用户空间,由用户使用。
CPU在寻址的时候,是按照虚拟地址来寻址,然后通过MMU(内存管理单元)将虚拟地址转换为物理地址。因为只有程序的一部分加入到内存中,所以会出现所寻找的地址不在内存中的情况(CPU产生缺页异常),如果在内存不足的情况下,就会通过页面调度算法来将内存中的页面置换出来,然后将在外存中的页面加入到内存中,使程序继续正常运行。
原图链接
0902补充一个比较好的总结
为了在多进程环境下,使得进程之间的内存地址不受影响,相互隔离,于是操作系统就为每个进程独立分配一套
虚拟地址空间
,每个程序只关心自己的虚拟地址就可以,实际上大家的虚拟地址都是一样的,但分布到物理地址内存是不一样的。作为程序,也不用关心物理地址的事情。
每个进程都有自己的虚拟空间,而物理内存只有一个,所以当启用了大量的进程,物理内存必然会很紧张,于是操作系统会通过内存交换技术
,把不常使用的内存暂时存放到硬盘(换出)(这个就是虚拟内存)
,在需要的时候再装载回物理内存(换入)。
那既然有了虚拟地址空间,那必然要把虚拟地址「映射」到物理地址,这个事情通常由操作系统来维护。
那么对于虚拟地址与物理地址的映射关系,可以有分段和分页
的方式,同时两者结合都是可以的。
内存分段是根据程序的逻辑角度,分成了栈段、堆段、数据段、代码段等,这样可以分离出不同属性的段,同时是一块连续的空间。但是每个段的大小都不是统一的,这就会导致内存碎片和内存交换效率低的问题。
于是,就出现了内存分页,把虚拟空间和物理空间分成大小固定的页
,如在 Linux 系统中,每一页的大小为 4KB。由于分了页后,就不会产生细小的内存碎片。同时在内存交换的时候,写入硬盘也就一个页或几个页,这就大大提高了内存交换的效率。
再来,为了解决简单分页产生的页表过大的问题,就有了多级页表
,它解决了空间上的问题,但这就会导致 CPU 在寻址的过程中,需要有很多层表参与,加大了时间上的开销。于是根据程序的局部性原理,在 CPU 芯片中加入了 TLB,负责缓存最近常被访问的页表项,大大提高了地址的转换速度。
Linux 系统主要采用了分页管理,但是由于 Intel 处理器的发展史,Linux 系统无法避免分段管理。
于是 Linux 就把所有段的基地址设为 0,也就意味着所有程序的地址空间都是线性地址空间(虚拟地址),相当于屏蔽了 CPU 逻辑地址的概念,所以段只被用于访问控制和内存保护。另外,Linxu 系统中虚拟空间分布可分为用户态和内核态两部分,其中用户态的分布:代码段、全局变量、BSS、函数栈、堆内存、映射区。
1.4 进程控制块PCB
我们知道,每个进程在内核中都有一个进程控制块(PCB)来维护进程相关的信息,Linux内核的进程控制块是 task_struct 结构体。
/usr/src/linux-headers-3.16.0-30/include/linux/sched.h
文件中可以查看 struct task_struct 结构体定义。其内部成员有很多,我们重点掌握以下部分即可:
补充
文件id查看指令:
ps aux
1.5 进程状态
进程基本的状态有 5 种。分别为新建态,就绪态,运行态,阻塞态与终止态。其中新建态为进程准备阶段,常与就绪态结合来看。
引起进程状态转换的具体原因如下:
- NULL→新建态:执行一个程序,创建一个子进程。
- 新建态→就绪态:当操作系统完成了进程创建的必要操作,并且当前系统的性能和虚拟内存的容量均允许。
- 运行态→终止态:当一个进程到达了自然结束点,或是出现了无法克服的错误,或是被操作系统所终结,或是被其他有终止权的进程所终结。
- 运行态→就绪态:运行时间片到;出现有更高优先权进程。
- 运行态→等待态:等待使用资源;如等待外设传输;等待人工干预。
- 就绪态→终止态:未在状态转换图中显示,但某些操作系统允许父进程终结子进程。
- 等待态→终止态:未在状态转换图中显示,但某些操作系统允许父进程终结子进程。
- 终止态→NULL:完成善后操作。
2 进程控制
2.1 fork/getpid/getppid
2.1.1 函数原型
pid_t fork(void);
失败返回-1;成功返回:① 父进程返回子进程的 ID(非负) ②子进程返回 0pid_t getpid(void);
获取当前进程 IDpid_t getppid(void);
获取当前进程的父进程 ID
补充:初学者常常有个问题,就是子进程的执行范围:其实子进程只会继续执行fork函数之后的部分…
2.1.2 案例一 打印父子进程id
fork getpid getppid的使用
# include<stdio.h>
# include<stdlib.h>
# include<unistd.h>
int main()
{
pid_t pid ;
pid = fork();
if(pid<0)
{
perror("fork error");
}
else if(pid==0)
{
printf("I am child pid =%d my father pid=%d\n",getpid(),getppid());
}
else
{
printf("I am father pid =%d my father pid =%d\n",getpid(),getppid());
}
return 0;
}
2.1.3 案例二 顺序打印5个进程
通过命令行参数指定创建进程的个数,每个进程休眠 1S 打印自己是第几个被创建的进程。
# include<stdio.h>
# include<stdlib.h>
# include<unistd.h>
int main()
{
pid_t pid;
int i;
for(i=0;i<5;i++)
{
pid =fork();
if(pid==0)
{