操作系统复习
守护进程(daemon):在后台运行并且不受任何终端控制的进程。Unix操作系统有很多典型的守护进程(其数目根据需要或20—50不等),它们在后台运行,执行不同的管理任务。
写时赋值(copy-on-write):linux中开新进程一般都是先fork一个子进程(子进程和父进程的数据完全一致)出来,再用exec替换为新的想要执行的进程。这样效率较低的原因是:大多数情况我们都不想要开始复制的父进程数据,而是想替换为我们自己的数据。所以linux引入了c-o-w技术,即:
fork出来的子进程先不复制父进程的数据,而是只在虚拟空间中分配空间,不分配实际的物理空间。假设p1为父进程,p2是子进程,则fork之后的空间分配是这样的:
这样在真正需要替换数据的时候,再申请空间更改子进程的指向。这样省去了一次白白复制的时间。
进程是一个程序运行时在内存中对应的实体。进程包含至少一个线程。
进程是系统分配资源的最小单位。
线程是系统调度资源的最小单位。
线程之间共享代码段、常量区、静态区、堆区等等。各线程独享的有pc(程序计数器)、寄存器(不是指物理上的,是副本)、栈。
IPC(进程间通信,Inter Process Communication)
1.管道:
1.1匿名管道(单工):
调用pipe()函数创建管道;
init pipe(int fd[2]),fd[0] 为管道的读取段,f[1]为管道的写入端。
通过write()函数写入信息,int write(int handle,char *buf,unsigned len);
进程通过read()函数读取数据 int read(int handle,void *buf,unsigned len)
匿名管道只能由父进程向其子进程(不一定是直接子进程)传输数据。
1.2 命名管道(半双工):
在内核中申请一块固定大小的缓冲区,程序拥有写入和读取的权利,没有血缘关系的进程也可以进程间通信。
2.信号量 :
在内核中创建一个信号量集合(本质是个数组),数组的元素(信号量)都是1,使用P操作进行-1,使用V操作+1,
(1) P(sv):如果sv的值大于零,就给它减1;如果它的值为零,就挂起该进程的执⾏ (加入到等待队列)。
(2) V(sv):如果有其他进程因等待sv而被挂起,就让它恢复运⾏,如果没有进程因等待sv⽽挂起,就给它加1。
P即调用该资源,V为释放该资源。
PV操作用于同一进程,实现互斥。
PV操作用于不同进程,实现同步。
互斥量只能用于线程的互斥,信号量用于线程的同步/互斥。
互斥量值只能为0/1,信号量值可以为非负整数。
互斥量的加锁和解锁必须由同一线程分别对应使用,信号量可以由一个线程释放,另一个线程得到。
3.消息队列(全双工):
在内核中创建一队列,队列中每个元素是一个数据报,不同的进程可以通过句柄去访问这个队列。消息队列提供了⼀个从⼀个进程向另外⼀个进程发送⼀块数据的⽅法。
消息队列头生产者不断添入新数据,尾部接受者不断取走数据。
缺点:每个消息队列的总的字节数是有上限的(MSGMNB),系统上消息队列的总数也有⼀个上限(MSGMNI)
4.共享内存:
同一块物理内存映射到多个进程的虚拟空间。是最快的IPC方法,但要注意用信号量/互斥锁保证操作的原子性。
5.信号:
这个不太了解
6.socket通信:
实际上也是进程间通信,只不过是不同机器间的两个进程间进行通信。
这个很好理解了,就是通过套接字进行不同机器间的数据通信,当然这个速度和前面的没法比,是走网线的。
多线程同步的线程间通信:
1.读写锁(允许多个同时读,但只有一个可以写)
2.互斥锁(和条件变量一起使用)
3.信号量
4.信号
内存管理:
空闲内存可以用位图或者链表进行管理。
用户态陷入内核态的三种情况:
1、系统调用,比如open、write文件,输入输出等功能实际上都会产生系统调用,因为这类权限比较大,系统不放心交给用户去使用。
2、中断,比如debug用的断点,或者某硬件完成某一工作通知cpu来取走数据时也会发中断给cpu。当然中断有很多种类,可以在中断向量表里查看
3、异常,比如除0,堆栈溢出,访问未知内存等
gcc命令将源代码(.c文件)变为可执行文件的过程:
1、预处理:-e
比如什么宏定义啦,该替换替换。#idndef、#endif什么的。
引入所有#include包含的头文件。
去除注释,加行号。
经过这一步.c文件变成.i文件。
2、编译:-s
把源代码变成汇编代码
.i文件变成.s文件。
3、汇编:-c
把汇编代码变成二进制代码
.s文件变成.o文件
4、链接:
把各个.o文件整合到一起,组成一个可执行文件
它的工作就是把一些指令对其他符号地址的引用加以修正。链接过程主要包括了地址和空间分配、符号决议和重定向
这一步默认生成a.out文件
具体可以看这篇博客,讲的挺清楚的:https://blog.csdn.net/guaiguaihenguai/article/details/81160310
这里简单说一下静态链接和动态链接的区别:
1、静态链接:
函数和数据被编译进一个二进制文件。在使用静态库的情况下,在编译链接可执行文件时,链接器从库中复制这些函数和数据并把它们和应用程序的其它模块组合起来创建最终的可执行文件。
空间浪费:因为每个可执行程序中对所有需要的目标文件都要有一份副本,所以如果多个程序对同一个目标文件都有依赖,会出现同一个目标文件都在内存存在多个副本;
更新困难:每当库函数的代码修改了,这个时候就需要重新进行编译链接形成可执行程序。
运行速度快:但是静态链接的优点就是,在可执行程序中已经具备了所有执行程序所需要的任何东西,在执行的时候运行速度快。
2、动态链接:
动态链接的基本思想是把程序按照模块拆分成各个相对独立部分,在程序运行时才将它们链接在一起形成一个完整的程序,而不是像静态链接一样把所有程序模块都链接成一个单独的可执行文件。
共享库:就是即使需要每个程序都依赖同一个库,但是该库不会像静态链接那样在内存中存在多分,副本,而是这多个程序在执行时共享同一份副本;
更新方便:更新时只需要替换原来的目标文件,而无需将所有的程序再重新链接一遍。当程序下一次运行时,新版本的目标文件会被自动加载到内存并且链接起来,程序就完成了升级的目标。
性能损耗:因为把链接推迟到了程序运行时,所以每次执行程序都需要进行链接,所以性能会有一定损失。
孤儿进程:
父进程退出了,剩下的子进程就是孤儿进程了。Linux系统的最初进程init会把孤儿进程收养为自己的子进程并调用wait(),从而完成这些孤儿进程的收集回收工作。
僵尸进程:
子进程退出了,但父进程没有调用wait()收集其资源,那么该子进程就变为僵尸进程。僵尸进程的主要危害是:僵尸进程依然占据一个pid,如果产生过多的僵尸进程,系统将没有空闲的新pid号可以分配,导致没有办法开新进程。