进程:
进程是系统进行资源分配和调度的⼀个独立单位,一个程序在一个数据集中的一次动态执行过程,可以简单理解为“正在执行的程序”。 进程一般由程序、数据集、进程控制块三部分组成 程序用来描述进程要完成哪些功能以及如何完成; 数据集则是程序在执行过程中所需要使用的资源; 进程控制块用来记录进程的外部特征,描述进程的执行变化过程,系统可以利用它来控制和管理进程,它是系统感知进程存在的唯一标志; 进程的局限是创建、撤销和切换的开销比较大
线程:
线程是进程的⼀个实体, 也叫轻量级进程,是CPU调度和分派的基本单位, 线程是比进程更小的能独立运行的基本单位.线程自己基本上不拥有系统资源,只拥有⼀点在运行中必不可少的资源(如程序计数器,⼀组寄存器和栈),但是它可与同属⼀个进程的其他的线程共享进程所拥有的全部资源
协成:
协程,又称微线程,纤程(Coroutine) 协程是一个线程执行,但执行有点像多线程 协程执行过程中,在协程内部可中断,然后转而执行别的协程,在适当的时候再返回来接着执行。协程之间不是调用者与被调用者的关系,而是彼此对称、平等的,通过相互协作共同完成任务。
协程的执行效率极高。 因为协程切换不是线程切换,而是由程序自身控制,因此,没有线程切换的开销,和多线程比,线程数量越多,协程的性能优势就越明显。不需要多线程的锁机制 因为只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不加锁,只需要判断状态就好了,所以执行效率比多线程高很多。 简单点说协程是进程和线程的升级版,进程和线程都面临着内核态和用户态的切换问题而耗费许多切换时间,而协程就是用户自己控制切换的时机,不再需要陷入系统的内核态务。
进程间的通信方式
管道(PIPE):管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系(父子进程)的进程间使用。另外管道传送的是无格式的字节流,并且管道缓冲区的大小是有限的(管道缓冲区存在于内存中,在管道创建时,为缓冲区分配一个页面大小)。有名管道 (FIFO): 有名管道也是半双工的通信方式,但是它允许无亲缘关系进程间的通信。信号(Signal): 信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。信号量(Semaphore):信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。消息队列(Message Queue):消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。共享内存(Shared Memory ):共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号量,配合使用,来实现进程间的同步和通信。套接字(Socket): 套解口也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同主机间的进程通信
具体分析
管道: 3种。管道是面向字节流,自带互斥与同步机制,生命周期随进程。 1)普通管道PIPE, 通常有两种限制,一是半双工,数据同时只能单向传输;二是只能在父子或者兄弟进程间使用., 2)命令流管道s_pipe: 去除了第一种限制,为全双工,可以同时双向传输, 3)命名管道FIFO, 去除了第二种限制,可以在许多并不相关的进程之间进行通讯。 ①无名管道:没有磁盘节点,仅仅作为一个内存对象,用完就销毁了。因此没有显示的打开过程,实际在创建时自动打开,并且生成内存iNode,其内存对象和普通文件的一致,所以读写操作用的同样的接口,但是专用的。因为不能显式打开(没有任何标示),所以只能用在父子进程,兄弟进程, 或者其他继承了祖先进程的管道文件对象的两个进程间使用【具有共同祖先的进程】 int pipe(int fd[2]);//由参数fd返回两个文件描述符,fd[0]为读而打开 fd[1]为写而打开 int read(fd[0], buff, int size); int write(fd[1], buff, int size); ②有名管道:任意两个或多个进程间通讯。因为它在文件目录树中有一个文件标示(FIFO) 实际不占据磁盘空间,数据缓存在内存上。它与普通文件类似,都遵循打开,读,写,关闭的过程,但读写的内部实现和普通文件不同,和无名管道一样。 命令:mkfifo a=filename //mkfifo(char *path,int flag)系统调用。 标识符与键:每个内核的IPC结构(消息队列、信号量或共享内存)都用一个非负整数标识符引用。 标识符是IPC对象的内部名。为了是多个进程间能够访问到同一IPC对象,需要提供一个外部名。即“键(key)”,键与每个IPC对象关联,并作为对象的外部名。键的数据类型为key_t,由内核变换成标识符。 内核对象:用于进程间通讯时,多进程能访问同一资源的记录,用标识符标识。。信号量: 1.临界资源:同一时刻,只能被一个进程访问的资源 2.临界区:访问临界资源的代码区 3.原子操作:任何情况下不能被打断的操作。 它是一个计数器,记录资源能被多少个进程同时访问。用于控制多进程对临界资源的访问(同步)),并且是非负值。主要作为进程间以及同一进程的不同线程间的同步手段。 操作:创建或获取,若是创建必须初始化,否则不用初始化。 int semget((key_t)key, int nsems, int flag);//创建或获取信号量 int semop(int semid, stuct sembuf*buf, int length);//加一操作(V操作):释放资源;减一操作(P操作):获取资源 int semct(int semid, int pos, int cmd);//初始化和删除 注:我们可以封装成库,实现信号量的创建或初始化,p操作,V操作,删除操作。。消息队列: 消息队列是消息的链表,是存放在内核中并由消息队列标识符标识。因此是随内核持续的,只有在内核重起或者显示删除一个消息队列时,该消息队列才会真正被删除。。消息队列克服了信号传递信息少,管道只能承载无格式字节流以及缓冲区受限等特点。允许不同进程将格式化的数据流以消息队列形式发送给任意进程,对消息队列具有操作权限的进程都可以使用msgget完成对消息队列的操作控制,通过使用消息类型,进程可以按顺序读信息,或为消息安排优先级顺序。 与信号量相比,都以内核对象确保多进程访问同一消息队列。但消息队列发送实际数据,信号量进行进程同步控制。 与管道相比,管道发送的数据没有类型,读取数据端无差别从管道中按照前后顺序读取;消息队列有类型,读端可以根据数据类型读取特定的数据。 操作:创建或获取消息队列, int msgget((key_tkey, int flag);//若存在获取,否则创建它 发送消息:int msgsnd(int msgid, void *ptr, size_t size, int flag); ptr指向一个结构体存放类型和数据 size 数据的大小 接受消息:int msgrcv(int msgid, void ptr, size_t size, long type, int flag); 删除消息队列: int msgctl(int msgid, int cmd, struct msgid_dsbuff);共享内存: 共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。 共享内存是最快的一种IPC,因为不需要在客户进程和服务器进程之间赋值。使用共享内存的唯一注意的是是多个进程对一给定的存储区的同步访问。【若服务器进程正在向共享存储区写入数据,则写完数据之前客户进程不应读取数据,或者客户进程正在从共享内存中读取数据,服务进程不应写入数据。。所以我们要对共享内存进行同步控制,通常是信号量。】 int shmget((key_t)key, size_t size, int flag); //size 开辟内存空间的大小,flag:若存在则获取,否则创建共享内存存储段,返回一个标识符。 void *shmat(int shmid, void *addr, int flag); //将共享内存段连接到进程的地址空间中,返回一个共享内存首地址 函数shmat将标识号为shmid共享内存映射到调用进程的地址空间中,映射的地址由参数shmaddr和shmflg共同确定,其准则为: (1) 如果参数shmaddr取值为NULL,系统将自动确定共享内存链接到进程空间的首地址。 (2) 如果参数shmaddr取值不为NULL且参数shmflg没有指定SHM_RND标志,系统将运用地址shmaddr链接共享内存。 (3) 如果参数shmaddr取值不为NULL且参数shmflg指定了SHM_RND标志位,系统将地址shmaddr对齐后链接共享内存。其中选项SHM_RND的意思是取整对齐,常数SHMLBA代表了低边界地址的倍数,公式“shmaddr – (shmaddr % SHMLBA)”的意思是将地址shmaddr移动到低边界地址的整数倍上。 int shmdt(void *ptr); //断开进程与共享内存的链接 进程脱离共享内存区后,数据结构 shmid_ds 中的 shm_nattch 就会减 1 。但是共享段内存依然存在,只有 shm_attch 为 0 后,即没有任何进程再使用该共享内存区,共享内存区才在内核中被删除。一般来说,当一个进程终止时,它所附加的共享内存区都会自动脱离。 int shmctl(int shmid, int cmd, struct shmid_ds *buff); //删除共享内存(内核对象) shmid是shmget返回的标识符; cmd是执行的操作:有三种值,一般为 IPC_RMID 删除共享内存段; buff默认为0. 如果共享内存已经与所有访问它的进程断开了连接,则调用IPC_RMID子命令后,系统将立即删除共享内存的标识符,并删除该共享内存区,以及所有相关的数据结构; 如果仍有别的进程与该共享内存保持连接,则调用IPC_RMID子命令后,该共享内存并不会被立即从系统中删除,而是被设置为IPC_PRIVATE状态,并被标记为”已被删除”(使用ipcs命令可以看到dest字段);直到已有连接全部断开,该共享内存才会最终从系统中消失。 需要说明的是:一旦通过shmctl对共享内存进行了删除操作,则该共享内存将不能再接受任何新的连接,即使它依然存在于系统中!所以,可以确知, 在对共享内存删除之后不可能再有新的连接,则执行删除操作是安全的;否则,在删除操作之后如仍有新的连接发生,则这些连接都将可能失败! 消息队列和管道基本上都是4次拷贝,而共享内存(mmap, shmget)只有两次。 4次:1,由用户空间的buf中将数据拷贝到内核中。2,内核将数据拷贝到内存中。3,内存到内核。4,内核到用户空间的buf. 2次: 1,用户空间到内存。 2,内存到用户空间。 消息队列、共享内存和管道都是内核对象,所执行的操作也都是系统调用,而这些数据最终是要存储在内存中执行的。因此不可避免的要经过4次数据的拷贝。但是共享内存不同,当执行mmap或者shmget时,会在内存中开辟空间,然后再将这块空间映射到用户进程的虚拟地址空间中,即返回值为一个指向逻辑地址的指针。当用户使用这个指针时,例如赋值操作,会引起一个从逻辑地址到物理地址的转化,会将数据直接写入对应的物理内存中,省去了拷贝到内核中的过程。当读取数据时,也是类似的过程,因此总共有两次数据拷贝。socket通信 适合同一主机的不同进程间和不同主机的进程间进行全双工网络通信。但并不只是Linux有,在所有提供了TCP/IP协议栈的操作系统中几乎都提供了socket,而所有这样操作系统,对套接字的编程方法几乎是完全一样的,即“网络编程”。
IO多路复用的方式
操作系统在处理io的时候,主要有两个阶段:
等待数据传到io设备io设备将数据复制到user space
我们一般将上述过程简化理解为:
等到数据传到kernel内核spacekernel内核区域将数据复制到user space(理解为进程或者线程的缓冲区)
select,poll,epoll都是IO多路复用的机制。I/O多路复用就通过一种机制,可以监视多个文件描述符**,一旦某个描述符就绪(一般是读就绪或者写就绪,还有异常就绪),能够通知程序进行相应的读写操作**。但select,poll,epoll本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而异步I/O则无需自己负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间。
fd:文件描述符(file descriptor)
select
单个进程就可以同时处理多个网络连接的io请求(同时阻塞多个io操作)。 基本原理就是程序呼叫select,然后整个程序就阻塞状态,这时候,kernel内核就会轮询检查所有select负责的文件描述符fd,当找到其中那个的数据准备好了文件描述符,会返回给select,select通知系统调用,将数据从kernel内核复制到进程缓冲区(用户空间)。
虽然服务器进程会被select阻塞,但是select会利用内核不断轮询监听其他客户端的io操作是否完成
select的几大缺点: (1)每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大 (2)同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大 (3)select支持的文件描述符数量太小了,默认是1024
poll
poll的原理与select非常相似,差别如下: 描述fd集合的方式不同,poll使用 pollfd 结构而不是select结构fd_set结构,所以poll是链式的,没有最大连接数的限制 poll有一个特点是水平触发,也就是通知程序fd就绪后,这次没有被处理,那么下次poll的时候会再次通知同个fd已经就绪。
细谈事件驱动–>epoll
epoll 提供了三个函数: int epoll_create(int size); 建立一個 epoll 对象,并传回它的id int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); 事件注册函数,将需要监听的事件和需要监听的fd交给epoll对象 int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout); 等待注册的事件被触发或者timeout发生
epoll解决的问题: epoll没有fd数量限制 epoll没有这个限制,我们知道每个epoll监听一个fd,所以最大数量与能打开的fd数量有关,一个g的内存的机器上,能打开10万个左右
epoll不需要每次都从用户空间将fd_set复制到内核kernel epoll在用epoll_ctl函数进行事件注册的时候,已经将fd复制到内核中,所以不需要每次都重新复制一次
select 和 poll 都是主动轮询机制,需要遍历每一个fd; epoll是被动触发方式,给fd注册了相应事件的时候,我们为每一个fd指定了一个回调函数,当数据准备好之后,就会把就绪的fd加入一个就绪的队列中,epoll_wait的工作方式实际上就是在这个就绪队列中查看有没有就绪的fd,如果有,就唤醒就绪队列上的等待者,然后调用回调函数。
虽然select、poll、epoll都需要查看是否有fd就绪,但是epoll之所以是被动触发,就在于它只要去查找就绪队列中有没有fd,就绪的fd是主动加到队列中,epoll不需要一个个轮询确认。 换一句话讲,就是select和poll只能通知有fd已经就绪了,但不能知道究竟是哪个fd就绪,所以select和poll就要去主动轮询一遍找到就绪的fd。而epoll则是不但可以知道有fd可以就绪,而且还具体可以知道就绪fd的编号,所以直接找到就可以,不用轮询。