操作系统
- 进程管理
- 进程
- 线程
- 协程
- 多线程资源抢占
- 多线程死锁
- Python GIL (Global Interpreter Lock) 问题
- 全局解释器锁
- 用多进程规避这个问题
- 虽说是假多线程,但是能用来做 io 依赖的任务还是可以的
- 并发并行
- 内存管理
- 内存泄漏
- 自动内存管理(GC 垃圾回收)
- 循环引用
- 非托管资源
- 驱动程序(硬件管理)
- 文件系统(冯诺依曼结构的电脑实际上可以不包含外存)
- 操作系统:
驱动(硬件的抽象)-》
进程(操作系统的抽象,独立的资源和内存)-》
线程 (更容易资源共享,更容易使用多核)
一、进程管理
qq、pycharm、firefox
等程序都是独立的进程,一个程序就是一个进程,管理进程就是管理在操作系统中运行的程序。
为什么会有进程呢?
先有电脑,然后在电脑上写程序,程序是和电脑绑死的,如果电脑硬件更新,就需要重新写代码,所以出现了操作系统,将代码置于操作系统之上。更新驱动,就可以跑运行在不同的硬件上,不需要改代码。
以前的电脑是单任务,一次只能执行一个程序。后来需要在电脑上运行多个程序,这些程序自己管理自己,保证别人不会抢占你的内存空间,不让别人把你关掉等等。后来操作系统进化出了进程的概念,相当于是虚拟的操作系统的抽象,在进程里面有一份虚拟的内存空间和执行顺序。操作系统是对硬件的一个抽象,那么进程就相当于对操作系统的抽象。进程就变成了基本的运行单元,进程之间相互隔离,数据不能共享。
进程调度由操作系统进行控制,操作系统内部会有一张表,表里面有进程id(pid)以及进程的地址和运行状态,那么操作系统就可以根据这张表对其调度。
单核在物理成面一次只能执行一个程序,但是它的计算速度非常快,可以轮流执行(时间分片)不同的程序,所以人们感觉程序在同时执行。
为什么会有线程呢?
最开始单进程只是运行在单核上面,出现多核后,让一个程序完全占领这些核很麻烦,因为如果有八个核,开八个进程,这些进程的数据是相互隔离的,不方便交互访问。就出现了线程,线程可以同时跑在不同的核里面,充分利用核资源,还可以共享数据(共享变量)。进程之间的通信类似于浏览器与服务器之间的通信差不多,需要进过转义等。进程之间数据的共享在数据库或redies;
线程是进程的进一步划分,线程之间的数据是共享的。线程的调度也是操作系统进行的,由于线程的资源是共享的,那么就可能导致抢占资源的问题或死锁。
线程 进程
对于四核cpu,程序可以开四个线程,每个核上跑一个线程,这样四个线程可以同时运行。如果开八个线程,每个核上两个,那么这两个就是按照时间片段来执行。
进程:一个执行的程序
线程:程序的子操作(视频不仅有画面还有声音)
处理多线程就是异步,单线程就是同步
同步是阻塞模式,异步是非阻塞模式。
简单比喻:
http://www.ruanyifeng.com/blog/2013/04/processes_and_threads.html
https://www.liaoxuefeng.co/wiki/0014316089557264a6b348958f449949df42a6d3a2e542c000/0014319272686365ec7ceaeca33428c914edf8f70cca383000
- 定义
进程:具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位。程序是一个没有生命的实体,只有处理器赋予程序生命时,它才能成为一个活动的实体,线程是其活动的小单位。
线程:进程的一个实体,在引入线程的操作系统中,通常都是把进程作为分配资源的基本单位,而把线程作为独立运行和独立调度的基本单位。线程自己不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。
多线程主要是为了节约CPU时间,发挥利用,根据具体情况而定。线程的运行中需要使用计算机的内存资源和CPU。
关系
一个线程可以创建和撤销另一个线程;同一个进程中的多个线程之间可以并发执行。
相对进程而言,线程是一个更加接近于执行体的概念,它可以与同进程中的其他线程共享数据,但拥有自己的栈空间,拥有独立的执行序列。区别
进程和线程的主要差别在于它们是不同的操作系统资源管理方式。进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个进程中的不同执行路径。线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间,一个线程死掉就等于整个进程死掉,所以多进程的程序要比多线程的程序健壮,但在进程切换时,耗费资源较大,效率要差一些。但对于一些要求同时进行并且又要共享某些变量的并发操作,只能用线程,不能用进程。
- 简而言之,一个程序至少有一个进程,一个进程至少有一个线程.
- 线程的划分尺度小于进程,基本上不拥有系统资源,故对它的调度所付出的开销就会小得多,能更高效的提高系统内多个程序间并发执行的程度。使得多线程程序的并发性高。
- 另外,进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率。进程间相互独立,同一进程的各线程间共享。某进程内的线程在其它进程不可见。
- 线程在执行过程中与进程还是有区别的。每个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口。但是线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。
- 从逻辑角度来看,多线程的意义在于一个应用程序中,有多个执行部分可以同时执行。但操作系统并没有将多个线程看做多个独立的应用,来实现进程的调度和管理以及资源分配。这就是进程和线程的重要区别。
- 优缺点
线程和进程在使用上各有优缺点:线程执行开销小,但不利于资源的管理和保护;而进程正相反。同时,线程适合于在SMP机器上运行,而进程则可以跨机器迁移;
加锁和死锁
对于代码 A 和 代码 B,如果先执行代码 A,再执行代码 B,会得到我们想要的结果,如果开多线程,而且在 A 内设置时间暂停,那么就会中断执行 A 去执行 B,那么最终结果就会变化。
加锁:
如果两段代码A和B(加时间停顿),如果必须执行完A后,才能执行B,执行完B后,接着执行A,那么可以给A和B分别加锁,那么代码 A、B 就不能被分开执行了(原子性),这样可以保证执行的顺序,如果搞不好就会造成死锁。
死锁:
如果在代码A、B内加锁后没有释放锁,那么执行A期间会执行B,执行B期间会执行A,但是内有释放锁,所以不能交叉执行,就会出现死锁现象。
python GIL(Global Interpreter Lock)
全局解释器锁
python 虽然是多线程程序,但有个全局解释器锁,会导致同一时刻只能有一个线程在执行。
理想的多线程:
python的多线程:每次实际只有一个线程再执行(纵轴是时间)
多线程比较难写,容易资源抢占或死锁,所以用 GIL 规避这些问题。那么既然不能享受多线程带来的遍历,在多核电脑下,怎么充分利用这些核呢,采用多进程!开多个进程可以在不同的核上面执行。虽然是假线程,但还是有一定作用的。
并发和并行
你吃饭吃到一半,电话来了,你一直到吃完了以后才去接,这就说明你不支持并发也不支持并行。你吃饭吃到一半,电话来了,你停了下来接了电话,接完后继续吃饭,这说明你支持并发。你吃饭吃到一半,电话来了,你一边打电话一边吃饭,这说明你支持并行。并发的关键是你有处理多个任务的能力,不一定要同时。并行的关键是你有同时处理多个任务的能力。所以我认为它们最关键的点就是:是否是『同时』。
当有多个线程在操作时,如果系统只有一个CPU,则它根本不可能真正同时进行一个以上的线程,它只能把CPU运行时间划分成若干个时间段,再将时间段分配给各个线程执行,在一个时间段的线程代码运行时,其它线程处于挂起状态,这种方式我们称之为并发 (Concurrent
)。
当系统有一个以上CPU时,则线程的操作有可能非并发.当一个CPU执行一个线程时,另一个CPU可以执行另一个线程,两个线程互不抢占CPU资源,可以同时进行,这种方式我们称之为并行 (Parallel
)。
http://blog.csdn.net/java_zero2one/article/details/51477791
线程安全
线程安全问题都是由全局变量及静态变量引起的。若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,都需要考虑线程同步,否则的话就可能影响线程安全。
多线程访问时,采用了加锁机制,当一个线程访问该类的某个数据时,进行保护,其他线程不能进行访问直到该线程读取完,其他线程才可使用。不会出现数据不一致或者数据污染。 线程不安全就是不提供数据访问保护,有可能出现多个线程先后更改数据造成所得到的数据是脏数据。
比如一个 ArrayList 类,在添加一个元素的时候,它可能会有两步来完成:1. 在 Items[Size] 的位置存放此元素;2. 增大 Size 的值。
在单线程运行的情况下,如果 Size = 0,添加一个元素后,此元素在位置 0,而且 Size=1;
而如果是在多线程情况下,比如有两个线程,线程 A 先将元素存放在位置 0。但是此时 CPU 调度线程A暂停,线程 B 得到运行的机会。线程B也向此 ArrayList 添加元素,因为此时 Size 仍然等于 0 (注意哦,我们假设的是添加一个元素是要两个步骤哦,而线程A仅仅完成了步骤1),所以线程B也将元素存放在位置0。然后线程A和线程B都继续运行,都增加 Size 的值。
那好,我们来看看 ArrayList 的情况,元素实际上只有一个,存放在位置 0,而 Size 却等于 2。这就是“线程不安全”了。
协程
- 一开始大家想要同一时间执行那么三五个程序,大家能一块跑一跑。特别是
UI
什么的,别一上计算量比较大的玩意就跟死机一样。于是就有了并发,从程序员的角度可以看成是多个独立的逻辑流。内部也可以是多cpu
并行,也可以是单cpu
时间分片,能快速的切换逻辑流,看起来像是大家一块跑的就行。 - 但是一块跑就有问题了。我计算到一半,刚把多次方程解到最后一步,你突然插进来,我的中间状态咋办,我用来储存的内存被你覆盖了咋办?所以跑在一个 cpu 里面的并发都需要处理上下文切换的问题。进程就是这样抽象出来个一个概念,搭配虚拟内存、进程表之类的东西,用来管理独立的程序运行、切换。
- 后来电脑上有了好几个 cpu,大家都别闲着,一人跑一进程。就是所谓的并行。
- 因为程序的使用涉及大量的计算机资源配置,把这活随意的交给用户程序,非常容易让整个系统分分钟被搞跪,资源分配也很难做到相对的公平。所以核心的操作需要陷入内核 (kernel),切换到操作系统,让老大帮你来做。
- 有的时候碰着 I/O 访问,阻塞了后面所有的计算。空着也是空着,老大就直接把 CPU 切换到其他进程,让人家先用着。当然除了 I\O 阻塞,还有时钟阻塞等等。一开始大家都这样弄,后来发现不成,太慢了。为啥呀,一切换进程得反复进入内核,置换掉一大堆状态。进程数一高,大部分系统资源就被进程切换给吃掉了。后来搞出线程的概念,大致意思就是,这个地方阻塞了,但我还有其他地方的逻辑流可以计算,这些逻辑流是共享一个地址空间的,不用特别麻烦的切换页表、刷新 TLB,只要把寄存器刷新一遍就行,能比切换进程开销少点。
- 如果连时钟阻塞、 线程切换这些功能我们都不需要了,自己在进程里面写一个逻辑流调度的东西。那么我们即可以利用到并发优势,又可以避免反复系统调用,还有进程切换造成的开销,分分钟给你上几千个逻辑流不费力。这就是用户态线程。
- 从上面可以看到,实现一个用户态线程有两个必须要处理的问题:一是碰着阻塞式I\O会导致整个进程被挂起;二是由于缺乏时钟阻塞,进程需要自己拥有调度线程的能力。如果一种实现使得每个线程需要自己通过调用某个方法,主动交出控制权。那么我们就称这种用户态线程是协作式的,即是协程。
而协程因为是非抢占式,所以需要用户自己释放使用权来切换到其他协程,因此同一时间其实只有一个协程拥有运行权,相当于单线程的能力。
阻塞I/O、非阻塞I/O I/O多路复用
https://www.cnblogs.com/skiler/p/6852493.html
下面举一个例子,模拟一个tcp服务器处理30个客户socket。假设你是一个老师,让30个学生解答一道题目,然后检查学生做的是否正确,你有下面几个选择:
- 第一种选择:按顺序逐个检查,先检查A,然后是B,之后是C、D。。。这中间如果有一个学生卡主,全班都会被耽误。这种模式就好比,你用循环挨个处理socket,根本不具有并发能力。
- 第二种选择:你创建30个分身,每个分身检查一个学生的答案是否正确。 这种类似于为每一个用户创建一个进程或者线程处理连接。
- 第三种选择,你站在讲台上等,谁解答完谁举手。这时C、D举手,表示他们解答问题完毕,你下去依次检查C、D的答案,然后继续回到讲台上等。此时E、A又举手,然后去处理E和A。。。 这种就是IO复用模型,Linux下的select、poll和epoll就是干这个的。
将用户socket对应的fd注册进epoll,然后epoll帮你监听哪些socket上有消息到达,这样就避免了大量的无用操作。此时的socket应该采用非阻塞模式。这样,整个过程只在调用select、poll、epoll这些调用的时候才会阻塞,收发客户消息是不会阻塞的,整个进程或者线程就被充分利用起来,这就是事件驱动,所谓的reactor模式。
二、内存管理
程序运行的时候,储存数据和代码的地方。断电就没了,比硬盘块十倍左右。操作系统负责给程序分配内存。
内存泄露:
如果创建变量后不删除,那么变量将会越来越多,占用的内存越来越大,最终会导致内存泄露。
解决办法:
- 定时重启
- 自动内存管理(GC 垃圾回收)
语言内机制会自动释放变量,引用次数为 0 时,会被自动释放(java,python)。
不能自动管理的:- 循环引用
A 引用 B,B 引用 A - 非托管资源
代码里面的字典列表是托管资源,受语言控制。打开的文件,连接的数据库是非托管资源,这些资源在不使用的时候需要被手动释放,语言不能自动判断你使用不使用。
- 循环引用
三、驱动程序(硬件管理)
每个设备(硬件)都有自己的驱动程序,什么情况下发生了什么事情,代表什么意思,将自己的动作转换成操作系统能识别的数据。
四、文件系统
文件储存于磁盘上,磁盘有很多盘片,就像光盘一样,厚是因为有很多层。每个盘片有很多磁,在某边(右)会有个磁头,通过磁头可以读取盘片上的磁信息。光盘是在盘片上涂化学物质,让其变得凹凸不平,比如凹代表0,凸代表1,这样以二进制代表数据。磁盘靠磁来存数据。
我们读取数据并不关心数据存在哪,操作系统给我们提供了 readfile
功能,我们只需要调用这个功能,然后给这个函数提供一个路径就可以了。操作系统就能去指定位置读取信息了。这就是文件系统。
会将存储空间分为几个部分,每个部分有非常多的小格子,文件系统会给这些小格子有意义的格式,一般分为三个部分:
- 控制信息:一般在前面,为第一部分,包括文件系统的版本,不同的操作系统版本不一样,包含这个区域有多大,放了多少文件,还剩多少文件等。
- 索引:第二部分,储存数据的地址,索引定长,文件不定长,索引指向数据的真正地址,执行删除操作时,删除的是索引,而非真正的数据,是假删除。加数据就是先加索引,然后在另一块区域存储真正数据。
- 数据存储区域
磁盘格式化就是将控制信息和索引删除掉。
异步和多线程
多核出现后,一个cpu上面有四核,那么可以同时执行四个任务,如果 一个程序开四个进程的话,
由于进程相互隔离,那么就不能共享数据。那么就出现了线程,开四个线程,分别同时跑在四个核上,
还可以共享数据。
同步:abcd顺序执行
异步:js的回调,乱序执行,a卡了可以执行b。和多线程没啥关系。
1. 单线程同步和异步:
同步:顺序执行
异步:
先执行T1,马上返回执行T2,然后执行T1的callback,乱序,保证整个过程不卡死,不浪费时间。
适合爬虫,比如下载某个网页需要一分钟,那么下载期间可以去执行其他任务,
下载完成后再去执行前一个任务;
2. 多线程的同步和异步
多线程:有四个任务,可以同时执行
多线程同步:单个线程执行任务顺序不变(T1-T5)
多线程异步:单个线程里面执行的任务乱序,但是上述3个线程可以同步执行,代表着三个核;
多线程 + 异步 能够充分利用 cpu 的每一个核。如果不用异步,可以开更多的线程充分利用cpu的核。
一个核可以开多个线程,划分时间片来执行不同线程。(大多数软件采用方式)
gunicorn
服务器会有多个请求,gunicorn 会帮我们规避 PYTHON GIL,会用多个 worker 实现多进程,这样能中分利用多核。 master 会自动分发请求给 worker(进程)。
不同进程之间相互隔离,这就导致了 csrf_token 不能共享,会导致程序错误。可以通过 redies 或 存数据库解决。
supervisor gunicorn
- supervisor:守护进程(程序挂了自动拉起来)
- gunicorn:管理进程(缓解操作系统压力)
- 管理进程:master(分发任务)
- 工作进程:worker(执行任务)
Gunicorn 一般用来管理多个进程,有进程挂了Gunicorn可以把它拉起来,防止服务器长时间停止服务,还可以动态调整 worker 的数量,请求多的时候增加 worker 的数量,请求少的时候减少,这就是所谓的 pre-fork 模型。(worker 貌似就是进程,不是很确定,因为我们在使用的过程中没有发现进程数量有变化。。。)。
Unix/Linux操作系统提供了一个fork()系统调用,它非常特殊。普通的函数调用,调用一次,返回一次,但是fork()调用一次,返回两次,因为操作系统自动把当前进程(称为父进程)复制了一份(称为子进程),然后,分别在父进程和子进程内返回。
子进程永远返回0,而父进程返回子进程的ID。这样做的理由是,一个父进程可以fork出很多子进程,所以,父进程要记下每个子进程的ID,而子进程只需要调用getppid()就可以拿到父进程的ID。
多线程实际上是多个线程之间轮流执行的,就是将一个时间段分成若干个时间片,每个线程只运行一个时间片,由于时间片极短,而且电脑运行极快,线程之间切换也极快,几乎可以看做是并行运行的,也就是说可以看成是同时运行的。但实际却不是同时运行。这是假多线程,python就是这样。真多线程是线程s可以同时执行。