Java、大数据开发学习要点(持续更新中…)
进程和线程的区别,可以用JVM进程内存模型举例对以下方面总结:
(1)从资源层面看:线程几乎不拥有系统资源(只有如程序计数器,一组寄存器和栈),并且同一进程的不同线程间共享进程资源。
(2)从系统调度层面看:线程是CPU调度的基本单元,而进程是除CPU外系统资源的分配单元。
(3)从系统开销层面看:创建进程需要操作系统为其分配或回收资源(内存空间、I/O资源等),开销较大。同样的在进程切换时,需要修改进程PCB信息、保存当前执行上下文和恢复另一进程的执行环境;而线程共享进程的许多资源,线程切换只会涉及少量寄存器、缓存内容,开销较小。
(4)从通信层面看:进程间通信需要依靠进程间通信需要通过进程间通信方法实现;而同一进程的不同线程由于共享地址空间,通信甚至不需要内核干预。
协程:
主流JVM实现是通过用户线程与内核线程的1比1模型设计实现的。即使在编程中我们常用线程池(几十到上百线程)来应对百万计的请求,其中的线程切换虽然是内核轻量级进程的切换,但频繁的上下文切换(寄存器、缓存等)仍然是开销巨大的。但试想将上下文切换的工作在用户态下由用户程序自己实现那么这部分开销就可以缩减。协程就是基于这种思想的实现,协程运行在线程之上,一个协程执行完成主动让出使另一个协程运行在此线程上。
其实现实际就是线程的一个代码片执行到需要请求其他资源而阻塞的情况,不让出CPU执行时间,而是将自己的栈空间记录保存下来;而后,跳转到下一个代码片,当被调用唤醒后,原代码片恢复原来的执行上下文并获得调用结果。(典型的场景是用协程实现生产者消费者模型,Java有Quasar框架来实现协程)
五状态转移图如下图所示:
当引入虚拟内存后,进程状态扩展为七种:关于进程调度参考此文章
(1)进程同步机制为了解决操作系统由于异步性导致的进程按不可预知的速度向前推进,需要同步机制实现进程间的制约。主要的方法是通过①信号量机制:设定一个记录型信号量,通过PV原语实现资源不足和足够的情况下进程阻塞和唤醒从而实现进程同步。②管程:一种特殊的软件模块,类似数据私有方法共有的类且同时只能有一个进程访问(Java中synchronized的实现就是类似于管程的实现)。③其他方法如:自旋锁、分布式系统(RPC)等。
(2)线程同步机制:线程同步可以联想到Java中锁的机制,主要方法是①互斥量:设定一个互斥对象,只有持有这个对象的线程才能访问共享资源(同步代码块、JUC并发包、ThreadLocal)。②信号量:设定同时访问共享资源的最大线程数量。③事件(Event) :wait/notify、park/unpark,通过通知操作的方式来保持多线程同步,还可以方便的实现多线程优先级的比较。
进程的通信方式:
- 共享存储:在内存中开辟一块空间,各进程互斥访问,可以避免管道和消息队列等方法从内核到用户空间的数据的四次拷贝,效率最高。
- 管道:在Linux实现中管道就是一个文件,半双工形式进行,写进程在写完后系统调用进程切换到读进程进行数据读取。(shell中的管道符号|,将前面命令的输出给到后面的命令)
- 消息队列:消息队列是存放在内核中的消息链表。传输结构化数据克服管道只能承载无格式字节流以及缓冲区大小受限等缺点。
- 信号量:信号量是一个计数器(常用记录型信号量,提供PV原语),多用于多进程对共享数据的互斥访问。
- 信号:信号是Linux的系统中用于进程间互相通信或者操作的一种机制,比如kill -9终止某个进程(kill -系列就是信号)。
- 套接字:主要用于跨计算机的进程通信。
线程的通信方式:
- 文件共享:线程间通过写数据到文件与从文件中读取数据来实现通信。
- 网络共享:通过网络编程的手段实现通过网络的线程通信。
- 变量共享:共享内存中的同一个变量或者对象。
- 线程协作API:wait/notify(同一对象的锁的持有者间调用,wait会释放对象锁,但对执行顺序还是有要求)、park/unpark(令牌许可机制,park等待令牌,unpark提供令牌,因此不要求两者的执行顺序,但是如果在同步代码中使用的话不会释放对象锁导致死锁发生)
批处理系统采用的算法:
算法 | 思想 | 是否可抢占 | 优点 | 缺点 | 是否会产生饥饿 |
---|---|---|---|---|---|
先来先服务(FCFS) | 在作业/进程调度时,每次调度都是从后备队列/就绪队列中选择最早到达的作业/进程为其服务(作业调度将作业调入内存、分配资源、创建进程;进程调度为之分配处理机) | 非抢占式 | 公平且实现简单 | 对长作业有利,但对短作业不利 | 否 |
最短作业优先(SJF) | 在作业/进程调度时,每次调度都是从后备队列/就绪队列中选择最短服务时间的作业/进程为其服务(对于进程调度时称为短进程优先SPF) | SJF和SPF是非抢占式的,但有最短剩余时间优先算法(SRTN)是抢占式的 | 平均周转时间短,进程等待时间短 | 对短作业有利,但不利于长作业,短作业的频繁到来可能导致长作业的饥饿 | 是 |
高响应比优先(HRRN) | 在作业/进程调度时,计算每个作业/进程的响应比,选择响应比最高的作业/进程为其服务 | 非抢占式 | 综合考虑了等待时间和运行时间,既考虑了作业的到达先后顺序,又能避免长作业不被响应的问题 | 每次调度前需要计算响应比增加系统开销 | 否 |
交互式系统采用的算法:
算法 | 思想 | 是否可抢占 | 优点 | 缺点 | 是否会饥饿 | 补充说明 |
---|---|---|---|---|---|---|
时间片轮转算法(RR) | 此算法只针对进程,以各个进程到达就绪队列的顺序(FCFS),轮流让各个进程执行一个固定大小的时间片。在时间片结束后,如果未执行完就立即剥夺处理机分配,重新进入就绪队列队尾等待 | 抢占式(时钟中断) | 公平;响应快 | 高频率的进程切换,系统开销较大;不区分任务紧急程度 | 否 | 时间片大小对算法的效果影响较大,时间片过大算法就向FCFS算法趋近,而时间片过小则进程切换过于频繁,系统开销巨大 |
优先级调度算法 | 调度时选择优先级最高的作业/进程进行调度,甚至还会用于I/O调度 | 均有 | 优先级区分了任务的重要程度;动态优先级调整可以灵活调整各作业/进程的偏好程度 | 高优先级的进程源源不断到来会导致低优先级进程的饥饿 | 是 | 进程的优先级设置中通常有静态优先级和动态优先级 |
多级反馈队列调度算法 | (1)设置多级就绪队列,各级队列优先级从高到低,时间片从小到大;(2)新进程首先进入第1级队列,按FCFS等待被分配,若时间片结束未进程未结束,进入下一级队列队尾(如果已在最后一级则重新返回这级队列队尾);(3)只有k级及之前的所有队列未空时,才为k+1级队列队首进程分配时间片 | 抢占式 | 对各进程较为公平(FCFS);每个新到达的进程可以快速被响应(RR);短进程只需要较少时间就能完成(SPF) ;不必估计进程运行时间;灵活调整对进程的偏好 | 可能会产生饥饿(源源不断的短进程进入) | 是 | 如何理解灵活调整进程偏好:比如I/O密集型进程由于阻塞重新放回就绪队列,此时I/O进程仍然会被放入原队列保持较高优先级 |
死锁是指,在并发环境下,进程因为资源竞争所造成的相互等待对方所占有的资源,导致各进程都阻塞的现象(可以引申到Java并发)。死锁产生有下面四个必要条件:
- 资源互斥
- 进程拥有资源不可抢占
- 进程各自保持拥有资源并申请其他资源
- 循环等待,多个进程形成资源循环等待环
处理策略主要是:预防死锁(破坏必要条件)、避免死锁(银行家算法使进程的资源请求在操作系统存在安全序列)、解除死锁(资源剥夺、终止进程、进程退回)。
操作系统中,为了拓展内存的存储空间,将部分磁盘的空间用于内存页的交换空间,Linux系统中的SWAP分区就是虚拟内存的实现。虚拟内存建立在内存分页或分段管理的基础上,根据局部性原理,将程序暂时不用的内存页放在外存中,当程序需要时,内存中存在空闲页框就通过调页将需要的页加载入内存中(涉及缺页中断请求),如果内存中没有空闲页框,则通过页面置换交换将当前内存中符合页面置换算法(LRU、FIFO、时钟置换、LFU等)的页面放入外存(涉及页面置换算法LRU等)。
这个博客老哥从浅至深总结的很好
注意:用户进程请求I/O在Linux中是缓存I/O机制,操作系统会将 I/O 的数据缓存在内存的页缓存中,也就是说,数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间,这种方式对CPU和内存开销是比较大的。而使用DMA可以使数据从IO缓存中直接放入指定的内存空间中,也就是我们所说的零拷贝机制。
几种I/O的方式:超详细总结
I/O复用的实现:
select,poll,epoll都是IO多路复用的机制。I/O多路复用就是通过一种机制,一个进程可以监视多个文件描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。但select,poll,epoll本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的(非阻塞式同步IO,内核缓冲中数据就绪后进程自己负责去读取)。
int select (int n, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
select 函数监视的文件描述符分3类,分别是writefds、readfds、和exceptfds。有描述符就绪(有数据可读、可写、或者有异常,底层是数组的形式),调用select函数(阻塞),或者超时(timeout指定等待时间,如果立即返回设为null即可),函数返回。当select函数返回后,可以通过遍历fdset,来找到就绪的描述符。
select目前几乎在所有的平台上支持,其良好跨平台支持也是它的一个优点。select的一个缺点在于单个进程能够监视的文件描述符的数量存在最大限制,在Linux上默认为1024,且监听事件类型较少。
int poll (struct pollfd *fds, unsigned int nfds, int timeout);
struct pollfd {
int fd; /* file descriptor */
short events; /* requested events to watch */
short revents; /* returned events witnessed */
};
poll函数运行的方法类似于select,但是用pollfd(链表)来记录监视文件的描述符。
总结select和poll的相似与区别:
两个函数都是在返回后轮询文件描述符来获取就绪的socket。本质区别在于select的描述符类型结构是数组实现,监听描述符数量受限于FD_SETSIZE;poll的描述符类型使用链表实现,没有描述符数量限制。poll提供的事件类型更多,且描述符利用率上比select高。
1)调用epoll_create()建立一个epoll对象(在epoll文件系统中为这个句柄对象分配资源)
2)调用epoll_ctl向epoll对象中添加所有连接的套接字
3)调用epoll_wait收集发生的事件的连接
当某个进程调用epoll_create方法后,Linux内核会创建一个eventpoll结构体,这个结构体中有两个成员与epoll的使用方式密切相关:
struct eventpoll{
....
/*红黑树的根节点,这颗树中存储着所有添加到epoll中的需要监控的事件*/
struct rb_root rbr;
/*双链表中则存放着将要通过epoll_wait返回给用户的满足条件的事件*/
struct list_head rdlist;
....
};
每一个epoll对象都有一个独立的eventpoll结构体,用于存放通过epoll_ctl方法向epoll对象中添加进来的事件。这些事件都会挂载在红黑树中,如此,重复添加的事件就可以通过红黑树而高效的识别出来(红黑树的插入时间效率是O(logn),其中n为树的高度)。而所有添加到epoll中的事件都会与FD建立回调关系,也就是说,当相应的事件发生时会调用这个回调方法。这个回调方法在内核中叫ep_poll_callback,它会将发生的事件添加到rdlist双链表中。
总结一下就是,epoll通过epoll_ctl()来注册一个文件描述符,一旦基于某个文件描述符就绪时,内核会采用类似callback的回调机制,迅速激活这个文件描述符,当进程调用epoll_wait() 时便得到通知。
相比于select/poll,epoll的优势就是无最大并发连接限制(文件描述符只受限于Linux打开文件最大数),同时回调和双向链表的检测就绪实现避免了遍历(活跃文件描述符才回调)。
LT(水平触发):进程通过epoll_wait()检测到事件到达时,收到通知,但进程可以不立即处理该事件,下次调用epoll_wait()还会再收到通知。
ET(边缘触发):进程收到通知后必须立即处理事件,减少了同一事件的触发次数,效率更高。
Linux中有个重要的概念:一切都是文件。Linux常规的文件系统ext系列一个文件由inode(索引)和文件块(block)组成。
关于block磁盘分配方式见此处
在 Linux 系统中,进程通过非常简单的方式来创建,fork 系统调用会创建一个源进程的拷贝(副本)。调用 fork 函数的进程被称为父进程,使用 fork 函数创建出来的进程被称为子进程。父进程和子进程都有自己的内存映像,父进程和子进程相互独立(但父子进程共享父进程创建子进程时打开的文件)。
进程创建的过程:内核为子进程开辟一块新的用户空间的进程描述符,然后从父进程复制大量的内容。为这个子进程分配一个 PID,设置其内存映射,赋予它访问父进程文件的权限,注册并启动。
参考