基础
大端小端规则
大端: 将高位字节存储在低地址,低位字节存储在高地址
小端: 将低位字节存储在低地址,高位字节存储在高地址
一般网络字节序为大端字节序,主机字节序为小端。
linux提供了几个函数可以进行网络字节序和主机字节序的转化。
ntohs() ntohl() ntohll() htons() htonl() htonll()
为什么会有大端小端
对于计算机来说, 对于数据从低位开始计算更容易,所以一般计算机内部都是小端字节序
磁盘调度算法
1. 先来先服务
按照磁盘请求的顺序进行调度。
优点是公平和简单。缺点也很明显,因为未对寻道做任何优化,使平均寻道时间可能较长。
2. 最短寻道时间优先
优先调度与当前磁头所在磁道距离最近的磁道。
虽然平均寻道时间比较低,但是不够公平。如果新到达的磁道请求总是比一个在等待的磁道请求近,那么在等待的磁道请求会一直等待下去,也就是出现饥饿现象。具体来说,两端的磁道请求更容易出现饥饿现象。
3. 电梯扫描算法
电梯总是保持一个方向运行,直到该方向没有请求为止,然后改变运行方向。
电梯算法(扫描算法)和电梯的运行过程类似,总是按一个方向来进行磁盘调度,直到该方向上没有未完成的磁盘请求,然后改变方向。
因为考虑了移动方向,因此所有的磁盘请求都会被满足,解决了 SSTF 的饥饿问题。
进程管理
进程与线程
进程是对运行时程序的封装,每个进程拥有独立的内存空间,是系统进行资源调度和分配的基本单位。
线程是进程的子任务,是CPU调度和分派的基本单位。用于保证程序的实时性,实现进程内部的并发。每个线程拥有独自的虚拟处理器,独立的寄存器组,指令计数器,和处理器状态,栈段,每个线程完成自己的任务,但是共用地址空间。
区别:
一个线程只能属于一个进程,但一个进程允许有多个线程,一个进程可以分配的线程数量取决于进程分配虚拟空间的大小和线程栈段的大小。
进程在运行时拥有自己独立的内存单元,线程共享进程的内存。
进程编程调试可靠性高,但是创建销毁的开销很大,线程编程相反,开销小但是编程调试较为复杂。
通信方式不同
进程间通信方式:
名称及方式 |
---|
管道(pipe):允许一个进程和另一个与它有共同祖先的进程之间进行通信 |
命名管道(FIFO):类似于管道,但是它可以用于任何两个进程之间的通信,命名管道在文件系统中有对应的文件名。命名管道通过命令mkfifo或系统调用mkfifo来创建 |
消息队列(MQ):消息队列是消息的连接表,包括POSIX消息对和System V消息队列。有足够权限的进程可以向队列中添加消息,被赋予读权限的进程则可以读走队列中的消息。消息队列克服了信号承载信息量少,管道只能成该无格式字节流以及缓冲区大小受限等缺点; |
信号量(semaphore):信号量主要作为进程间以及同进程不同线程之间的同步手段; |
共享内存(shared memory):它使得多个进程可以访问同一块内存空间,是最快的可用IPC形式。这是针对其他通信机制运行效率较低而设计的。它往往与其他通信机制,如信号量结合使用,以达到进程间的同步及互斥 |
信号(signal):信号是比较复杂的通信方式,用于通知接收进程有某种事情发生,除了用于进程间通信外,进程还可以发送信号给进程本身 |
zSocket:它是更为通用的进程间通信机制,可用于不同机器之间的进程间通信 |
线程间通信方式:
线程间通信方式 |
---|
信号:类似进程间的信号处理 |
锁机制:互斥锁、读写锁和自旋锁 |
条件变量:使用通知的方式解锁,与互斥锁配合使用 |
信号量:包括无名线程信号量和命名线程信号量 |
进程调度的具体过程
常见的进程调度算法
- 先来先服务算法,只考虑就绪的先后,按就绪队列给进程分配资源
- 短作业优先算法, 对预计处理时间短的作业优先分配资源
- 轮转法,让每个进程在就绪队列中等待的时间与享受服务时间成正比例
- 多级反馈队列算法, 设置多个就绪队列分别赋予优先级,较高的优先级队列分配较少的时间片
- 最短剩余时间优先 shortest remaining time next(SRTN)
- 优先级调度 为每个进程分配一个优先级,按优先级进行调度。
一个进程可以创建多少线程,和什么有关?
一个进程可以创建的线程数由可用虚拟空间和线程的栈的大小共同决定,只要虚拟空间足够,那么新线程的建立就会成功。
在x86 32位系统的情况下,进程的用户可用虚拟地址空间是2G,系统占用2G,默认分配栈大小为1M时,创建线程的最大数量就为2G/1M=2048个。
产生死锁的条件
- 互斥
- 占有和等待
- 不可抢占
- 环路等待
死锁的解决策略
鸵鸟策略
死锁检测与死锁恢复
抢占资源
回滚恢复
杀死进程
死锁预防
破坏互斥条件
破坏占有等待条件
破坏不可抢占条件
破坏环路等待条件 给资源统一编号,进程只能按编号顺序来请求资源。
死锁避免 - 银行家算法
协程
协程是用户态的一种轻量级线程,协程拥有自己的上下文和栈,协程调度时可以把上下文和栈保存在其他地方,协程能保留上一次调用时的状态,再次运行时能直接进入上次的状态。
协程的好处:
- 无需线程上下文切换的开销
- 无需原子操作锁定及同步的开销
- 方便切换控制流,简化编程模型
线程之间什么是共享的,什么是不共享的。
堆,全局变量,静态变量是共享的。
栈与寄存器是不共享的。
一个进程启动时的内存管理是怎样的?
僵尸进程和孤儿进程
僵尸进程
子进程退出后需要父进程的确认收到才能在进程表中删除,而如果在自己成退出后,父进程并没有对其消亡进行确认,该进程就保存进程表中,占用资源。
- 可以使用
ps aux | grep Z
查看当前的僵尸进程。 - 使用
kill -s SIGCHLD pid
删除该父进程的所有僵尸进程
- 可以使用
孤儿进程
当父进程死亡后,进程就变为了孤儿进程,由init进程代为收养。同时由init进程对其资源释放。
父进程如何知道子进程的存亡, 子进程如何知道父进程的存亡
父进程可以使用waitpid查看子进程是否已经销毁,当其子进程销毁后,父进程会收到SIGCHD信号
子进程只需查看其ppid是否是1,也就是其父进程是否是init,是否被init收养。
守护进程
指在后台运行的,没有控制终端与之相连的进程。它独立于控制终端,周期性地执行某种任务。Linux的大多数服务器就是用守护进程的方式实现的,如web服务器进程http等
创建守护进程要点:
(1)让程序在后台执行。方法是调用fork()产生一个子进程,然后使父进程退出。
(2)调用setsid()创建一个新对话期。控制终端、登录会话和进程组通常是从父进程继承下来的,守护进程要摆脱它们,不受它们的影响,方法是调用setsid()使进程成为一个会话组长。setsid()调用成功后,进程成为新的会话组长和进程组长,并与原来的登录会话、进程组和控制终端脱离。
(3)禁止进程重新打开控制终端。经过以上步骤,进程已经成为一个无终端的会话组长,但是它可以重新申请打开一个终端。为了避免这种情况发生,可以通过使进程不再是会话组长来实现。再一次通过fork()创建新的子进程,使调用fork的进程退出。
(4)关闭不再需要的文件描述符。子进程从父进程继承打开的文件描述符。如不关闭,将会浪费系统资源,造成进程所在的文件系统无法卸下以及引起无法预料的错误。首先获得最高文件描述符值,然后用一个循环程序,关闭0到最高文件描述符值的所有文件描述符。
(5)将当前目录更改为根目录。
(6)子进程从父进程继承的文件创建屏蔽字可能会拒绝某些许可权。为防止这一点,使用unmask(0)将屏蔽字清零。
(7)处理SIGCHLD信号。对于服务器进程,在请求到来时往往生成子进程处理请求。如果子进程等待父进程捕获状态,则子进程将成为僵尸进程(zombie),从而占用系统资源。如果父进程等待子进程结束,将增加父进程的负担,影响服务器进程的并发性能。在Linux下可以简单地将SIGCHLD信号的操作设为SIG_IGN。这样,子进程结束时不会产生僵尸进程。
copy-on-write 写时复制
写时复制是一种优化策略,当存在多个调用者请求相同资源时, 他们会共同获取相同的指针指向内存上的资源,而不是直接复制一份,仅当调用进程有写入需求时,操作系统才会真正的去分配一块资源来复制一份副本进行写入改动。
管道和套接字的区别
信号量的具体原理
信号量是为了进程的互斥与同步,多个进程通过相互传递信号进行合作,发明的一种信号设施。
其本质就是一个整数,当有进程想要使用该资源,就将通知信号量-1,再进行使用,使用结束后将信号量加回去,当发现信号量小于零时就代表没有资源可用,将进程挂起等待资源释放后在进行使用。否则直接进行使用无需等待。
多进程架构与多线程架构有什么区别
操作系统实现锁的方法
缓存一致性
保持缓存中的数据同步修改
原子操作如何实现
处理器使用基于对缓存加锁或总线加锁的方式来实现多处理器之间的原子操作。
所谓总线锁就是使用处理器提供的一个LOCK#信号,当一个处理器在总线上输出此信号时,其他处理器的请求将被阻塞住,那么该处理器可以独占共享内存。
所谓“缓存锁定”是指内存区域如果被缓存在处理器的缓存行中,并且在Lock操作期间被锁定,那么当它执行锁操作回写到内存时,处理器不在总线上声言LOCK#信号,而是修改内部的内存地址,并允许它的缓存一致性机制来保证操作的原子性,因为缓存一致性机制会阻止同时修改由两个以上处理器缓存的内存区域数据,当其他处理器回写已被锁定的缓存行的数据时,会使缓存行无效
OS中都有什么锁
互斥锁
mutex(mutual exclusive)即互斥量(互斥体)。也便是常说的互斥锁。 mutex是最常见的多线程同步方式。多线程共享一个互斥量,然后线程之间去竞争。得到锁的线程可以进入临界区执行代码。
读写锁
读写锁是一种 读共享,写独占的锁。
读写锁的特性:
当读写锁被加了写锁时,其他线程对该锁加读锁或者写锁都会阻塞(不是失败)。
当读写锁被加了读锁时,其他线程对该锁加写锁会阻塞,加读锁会成功。
自旋锁
自旋就是在共享资源的状态不满足时, 自旋锁持续不停的检测状态,它和其他锁的区别就在于不会使线程休眠,也就是不会产生上下文切换,但是会浪费cpu。
内核态和用户态
内核态与用户态是操作系统的两种运行级别 ,在内核态下CPU可执行任何指令,在用户态下CPU只能执行非特权指令。当CPU处于内核态,可以随意进入用户态;而当CPU处于用户态时,用户从用户态切换到内核态只有在系统调用和中断两种情况下发生,一般程序一开始都是运行于用户态,当程序需要使用系统资源时,就必须通过调用软中断进入内核态。
Linux进程的4GB地址空间,3G-4G部分大家是共享的,是内核态的地址空间,这里存放在整个内核的代码和所有的内核模块,以及内核所维护的数据。
内存管理
分段存储和分页存储的区别以及优缺点
页式管理的基本原理是将各进程的虚拟空间划分为若干个长度相等的页。把内存空间按页的大小划分为片或者页面,然后把页式虚拟地址与内存地址建立一一对应的页表,并用相应的硬件地址转换机构来解决离散地址变换问题
段式管理的基本思想是把程序按内容或过程函数关系分成段,每段有自己的名字。 段是信息的逻辑单位,它含有一组其意义相对完整的信息 一个用户作业或者进程所包含的段对应一个二维线性虚拟空间,也就是一个二维虚拟存储器。段式管理程序以段为单位分配内存,然后通过地址映射机构把段式虚拟地址转换为实际内存物理地址。
- 分页是出于系统管理的需要,分段是出于用户应用的需要
- 页大小是系统固定的,而段大小则通常不固定。
- 分页是一维的,程序员只需用一个记忆符,即可表示一个地址;而分段是二维的,程序员在标识一个地址时,既要给出段名,又需给出段内地址。
- 通常段比页大,因而段表比页表短,可以缩短查找时间,提高访问速度
- 页和段都有存储保护机制。但存取权限不同:段有读、写和执行三种权限;而页只有读和写两种权限
虚拟内存的段页式存储
地址变换中,有快表和没快表,有什么区别?
地址变换过程 | 访问一个逻辑地址的访存次数 | |
---|---|---|
基本地址变换机构 | ①算页号、页内偏移量 ②检查页号合法性 ③查页表,找到页面存放的内存块号 ④根据内存块号与页内偏移量得到物理地址 ⑤访问目标内存单元 | 两次访存 |
具有快表的地址变换机构 | ①算页号、页内偏移量 ②检查页号合法性 ③查快表。若命中,即可知道页面存放的内存块号,可直接进行⑤;若未命中则进行④ ④查页表,找到页面存放的内存块号,并且将页表项复制到快表中 ⑤根据内存块号与页内偏移量得到物理地址 ⑥访问目标内存单元 | 快表命中,只需一次访存 快表未命中,需要两次访存 |
页面置换方法
- 最优页面置换算法 从主存中移出最长时间不需要访问的页面
- 先进先出页面置换算法(FIFO)移除内存中驻留时间最长的页面
- 时钟页面置换算法(clock)
- 最近最少使用页面置换算法(LRU)
- 工作集算法
虚拟内存
虚拟内存提供了三个重要的能力:
- 将主存看作一个存储在磁盘上的地址空间的缓存,根据需要在主存和磁盘间传送数据,更高效的使用了主存。
- 为进程提供相同的地址空间,从而简化了内存管理。
- 保护了每个进程的空间不受其他进程破坏。
为了更好的管理内存,操作系统将内存抽象成地址空间。每个程序拥有自己的地址空间,这个地址空间被分割成多个块,每一块称为一页。
这些页被映射到物理内存,但不需要映射到连续的物理内存,也不需要所有页都必须在物理内存中。当程序引用到不在物理内存中的页时,由硬件执行必要的映射,将缺失的部分装入物理内存并重新执行失败的指令。
虚拟内存的好处
简化链接
linux中每个进程都使用类似的内存布局, 这样可以大大简化链接器的设计,链接器可以生成完全独立于物理内存中代码和数据的链接完全的可执行文件
简化加载
把目标文件中 .text 和 .data 段加载到一个新创建的进程中 ,只需要为他们分配虚拟页即可,虚拟内存系统会按照需要自动地调入数据页,而不需要加载器来操作
简化共享
将多个进程的虚拟内存地址映射到相同的物理页,实现多个进程之间的数据共享。
简化内存分配
当进程要求额外的堆空间时, 比如调用malloc, 需要分配连续的地址空间,但是通过虚拟内存,可以将连续的虚拟地址映射到不连续的物理地址下。
虚拟内存的代价
- 虚存内存管理需要建立很多数据结构,需要占用很大的内存空间。
- 虚拟地址到物理地址的转换,增加了指令空间。
- 页面的置换需要硬盘io,需要消耗时间。
抖动你知道是什么吗?它也叫颠簸现象
刚刚换出的页面马上又要换入内存,刚刚换入的页面马上又要换出外存,这种频繁的页面调度行为称为抖动,或颠簸。产生抖动的主要原因是进程频繁访问的页面数目高于可用的物理块数(分配给进程的物理块不够)
为进程分配的物理块太少,会使进程发生抖动现象。
内存溢出与内存泄漏
内存溢出 out of memory,是指程序在申请内存时,没有足够的内存空间供其使用,出现out of memory
内存泄露 memory leak,是指程序在申请内存后,无法释放已申请的内存空间
内存泄漏最终会导致内存溢出.
linux
Linux相关
[toc]
常用命令
ps 查看进程
free 显示内存状态
df 显示磁盘分区上的磁盘空间的占用情况, 可以查看磁盘占用了多少空间,还能使用多少空间
du 显示每个文件和目录的磁盘使用空间,文件的大小
netstat 显示网络状态
tcpdump 抓包工具
ipcs 检查系统上共享内存等IPC的分配
ipcrm手动解除系统上共享内存的分配
ln 建立链接 -s为软连接 否则为硬链接
free 可以显示Linux系统中空闲的、已用的物理内存及swap内存,及被内核使用的buffer。
head
find
sed
动作 | 说明 | 备注 |
---|---|---|
'p' | 打印 | |
'i' | 在指定行之前插入内容 | 类似vim里的大写O |
'a' | 在指定行之后插入内容 | 类似vim里的小写o |
'c' | 替换指定行所有内容 | |
'd' | 删除指定行 |
awk
lsof
-p 显示指定进程打开的所有文件描述符
-t 显示打开该文件描述符的所有进程的pid
查询端口占用
netstat -ntulp | grep 80
lsof -i:80
实现日志滚动输出
tail -f
查看某一文件夹大小
du -s mydir
如何修改文件句柄数
ulimit -n <nums> 更改最大句柄数 ulimit -a 查看linux 有关参数, 对linux进行调优 对于所有进程均生效的方法: 在/etc/security/limits.conf添加参数,修改后保存即生效 soft nofile 65536 hard nofile 65536
linux如何限制进程CPU占比
使用nice命令运行,会优先调度优先级较高的进程,分配更多资源给优先级高的进程。
nice proc
系统调用的过程
- 程序通过调用函数库中的外壳函数,发起系统调用。
- 函数将通过堆栈传入外壳函数的参数置入指定的寄存器。
- 将系统调用的编号复制到EAX
- 外壳函数执行中断指令0x80,处理器从用户态进入到内核态
- 内核调用system_call
- 在内核栈中保存寄存器的值
- 审核系统调用编号的有效性
- 以编号索引找到对应的系统调用服务,并执行
- 从内核栈恢复寄存器的值,将系统调用返回值置于栈中
- 返回用户态
文件系统
文件系统的构成
在linux中文件系统由以下几层组成:
![](\picture\file system(1).png)
- 用户空间, 用户可见的文件系统, 也就是常见的一些文件操作
- 内核空间, 内核空间使用VFS系统屏蔽底层不同文件系统的实现,为上层提供统一的接口方便内核中系统调用。这也是体现linux一切皆文件的最重要的一层。
- 物理设备层。
文件存储结构
操作系统将磁盘分为两个区域,一个是数据区, 用来存储文件的具体数据,一个是inode table区,用来存储具体文件的索引表,就是inode table。
inode
为了加速文件检索的速度和节省物理空间,linux使用inode来存储每个文件的所有信息以及物理存储位置。
每个可以访问的存储块都存在至少一个inode节点来对其进行索引。为了节省空间,一个文件对应的空间未必是物理连续的, 而是由很多单位物理块存储,inode中存储着该文件所有块的物理地址。
c语言inode的具体数据结构
struct inode { struct hlist_node i_hash; /* 哈希表 */ struct list_head i_list; /* 索引节点链表 */ struct list_head i_dentry; /* 目录项链表 */ unsigned long i_ino; /* 节点号 */ atomic_t i_count; /* 引用记数 */ umode_t i_mode; /* 访问权限控制 */ unsigned int i_nlink; /* 硬链接数 */ uid_t i_uid; /* 使用者id */ gid_t i_gid; /* 使用者id组 */ kdev_t i_rdev; /* 实设备标识符 */ loff_t i_size; /* 以字节为单位的文件大小 */ struct timespec i_atime; /* 最后访问时间 */ struct timespec i_mtime; /* 最后修改(modify)时间 */ struct timespec i_ctime; /* 最后改变(change)时间 */ unsigned int i_blkbits; /* 以位为单位的块大小 */ unsigned long i_blksize; /* 以字节为单位的块大小 */ unsigned long i_version; /* 版本号 */ unsigned long i_blocks; /* 文件的块数 */ unsigned short i_bytes; /* 使用的字节数 */ spinlock_t i_lock; /* 自旋锁 */ struct rw_semaphore i_alloc_sem; /* 索引节点信号量 */ struct inode_operations *i_op; /* 索引节点操作表 */ struct file_operations *i_fop; /* 默认的索引节点操作 */ struct super_block *i_sb; /* 相关的超级块 */ struct file_lock *i_flock; /* 文件锁链表 */ struct address_space *i_mapping; /* 相关的地址映射 */ struct address_space i_data; /* 设备地址映射 */ struct dquot *i_dquot[MAXQUOTAS]; /* 节点的磁盘限额 */ struct list_head i_devices; /* 块设备链表 */ struct pipe_inode_info *i_pipe; /* 管道信息 */ struct block_device *i_bdev; /* 块设备驱动 */ unsigned long i_dnotify_mask; /* 目录通知掩码 */ struct dnotify_struct *i_dnotify; /* 目录通知 */ unsigned long i_state; /* 状态标志 */ unsigned long dirtied_when; /* 首次修改时间 */ unsigned int i_flags; /* 文件系统标志 */ unsigned char i_sock; /* 可能是个套接字吧 */ atomic_t i_writecount; /* 写者记数 */ void *i_security; /* 安全模块 */ __u32 i_generation; /* 索引节点版本号 */ union { void *generic_ip; /* 文件特殊信息 */ } u; };
链接
在linux中一个文件被分为用户数据与元数据两部分。用户数据就是记录文件真实内存存在的物理区块,元数据则记录着与文件相关的附加属性,创建时间,所属用户组等等。
文件名又为用户标识着文件。每次使用文件时,文件名指向一个inode,而inode指向真正的物理区块。
linux中链接分为硬链接和软链接。
硬链接
硬链接是指将该文件名直接指向一个另一个文件的inode,当删除原文件后,由于该文件名了解该文件inode,所以不会受到影响。每次创建硬链接会使inode中记录的引用计数+1,当引用计数减少到0,则释放该inode。
硬链接是不能跨文件系统建立的。
软链接
创建软链接会真正的分配一个inode存储指向原文件的文件名,也就是读取该inode,则直接跳转到原文件的目录下进行查找原文件从而进行操作。所以,软链接是可以跨文件系统建立的。
linux 下建立一个文件的步骤
- 存储文件属性,首先找到一个空闲的inode, 把文件属性记录在其中。
- 存储文件数据申请空间块,在自由块中寻找足够的块。
- 记录存储情况,将块地址填入inode
- 添加文件名到该目录下,并将文件名与inode建立连接。
通用IO模型
主要的系统调用
int open(const char *pathname, int flag)
ssize_t read(int fd, void *buffer, size_t count)
ssize_t write(int fd, void *buffer, size_t count)
int close(int fd)
off_t lseek(int fd, off_t offset, int whence)
修改文件的读写偏移量,Whence可选
SEEK_SET
从文件头部开始offset个字节,SEEK_CUR
从当前偏移量开始,向后调整offset个字节,SEEK_END
从文件尾部开始offset个字节int fcntl(int fd, int cmd, ...)
cmd的设置, F_GETFL 获取当前文件的访问模式和状态标志 F_SETFL 传入一个flag 设置fd的新标志位与访问模式。
int dup(int oldfd)
打开一个文件描述符与oldfd指向同一个文件句柄
int dup2(int oldfd, int newfd)
为oldfd重新创建一个文件描述符,指定其文件描述符为newfd,若newfd打开则关闭它再复制
ssize_t pread(int fd, void *buffer, size_t count, off_t offset)
从offset指定偏移位开始读写
ssize_t pwrite(int fd, void *buffer, size_t count, off_t offset)
从offset指定偏移位开始读写
void *mmap(void start, size_t length, int port, int flags, int fd)
申请一段内存空间,可以作为进程通信的共享内存,也可以将文件直接映射到其中。
int munmap(void *start, size_t length)
释放由mmap申请的内存。
文件空洞
write可以在任何地方写入数据,从文件结尾后到再次写入数据的区间,被称为文件空洞
文件空洞不占用任何磁盘空间,直到后续在文件空洞中写入了数据
文件空洞的优势就在于占用空间较少,虽然其名义上的空间大小不变,但内核实际为其分配的空间要更大。
fcnl
Core Dump
当程序运行的过程中异常终止或崩溃,操作系统会将程序当时的内存状态记录下来,保存在一个文件中,这种行为就叫做Core Dump即核心转储。
开启Core Dump
ulimit -c unlimited
当前shell生效
修改文件中/etc/secruity/limits.conf
五种IO模型
Unix io存在五种模型
阻塞式IO
应用进程被阻塞,直到数据从内核缓冲区中返回进程缓冲区才返回
非阻塞式IO
进行系统调用后,内核返回一个错误码,进程不断轮询查看io是否完成
IO复用
使用select,poll,epoll等待处理数据,并可以同时监听多个socket
信号驱动式IO
立即返回,当数据准备完成内核向进程发送信号,信号处理程序来拷贝数据
异步IO
立即返回,应用进程继续执行,内核会在所有操作完成之后向进程发送信号
什么是同步io什么是异步io
同步IO和异步IO的区别就在于第二个步骤是否阻塞。
如果实际的IO读写阻塞请求进程,那么就是同步IO,因此阻塞IO、非阻塞IO、IO服用、信号驱动IO都是同步IO。
如果不阻塞,而是操作系统帮你做完IO操作再将结果返回给你,那么就是异步IO。
阻塞IO和非阻塞IO的区别在于第一步,发起IO请求是否会被阻塞,如果阻塞直到完成那么就是传统的阻塞IO,如果不阻塞,那么就是非阻塞IO。
select() poll() epoll() 区别
这三种方式是在IO多路复用中常见的几种方式
select
使用无差别轮询方式进行查看是否有io事件发生,支持的文件描述符较少,默认是1024,可以修改内核参数来改变最大的文件描述符数量,但是会影响效率
select 的 timeout 参数精度为 1ns,而 poll 和 epoll 为 1ms,因此 select 更加适用于实时性要求比较高的场景 ,可移植性更好,几乎被所有主流平台所支持。
poll
poll本质上与select没有差别,只是它是使用链表来存储的,所以对最大连接数没有限制。若对精度没有要求,选择poll
epoll
epoll是一种时间复杂度为O(1)的方式,相对于select和poll,epoll会更加灵活,没有描述符的限制,它将用户关心的描述符放在内核中的一个事件表中,这样进行io时,仅需copy一次即可。
epoll使用三个函数实现io复用
epoll_create(size)
size会告诉内核这个监听数量有多大,但是size并非是一个限制,而是对初始内部数据分配的一个建议
epoll_ctl
对指定描述符修改,添加,删除它的监听事件。
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
等待io事件,最多返回maxevent个事件。
epoll 维护了一颗红黑树,作为内核事件表,用于收集描述符,维护一个就绪队列,用来存放有就绪队列的文件描述符,每个epoll事件都有一个epollevent结构体,存放着epoll_ctl方法添加进来的事件,并将其insert到红黑树中,红黑树可以高效的识别出重复的事件。当有IO事件发生时,内核会将发生io的文件放入就绪队列中,从而实现O(1)的查询。
若有大量的描述符需要同时轮询,使用epoll。
EPOLL可监听的事件类型
- EPOLLIN:套接字可读
- EPOLLOUT:套接字可写
- EPOLLRDHUP:对端关闭了套接字,或者对端关闭了写
- EPOLLPRI:套接字上有紧急数据到达
- EPOLLHUP:对端挂断了套接字
epollET模式中的epolloneshot
此规定操作系统最多触发其上注册的一个可读或者可写或者异常事件,且只触发一次,如此无论线程再多,只能有一个线程或进程处理同一个描述符。当然处理完之后线程要重置这个epolloneshot事件,进而当此描述符有事件可读时让其他进程可以处理此描述符。
这样,在epoll对fd进行读写时,fd再次发生写事件,系统不会再给fd开辟一条线程再次处理
讲一下LT和ET的区别
LT是电平触发,ET是边沿触发
LT的工作方式,当队列中当前文件描述符没有处理或没有完成,则下一次epoll会再次提醒,即程序可以不立即处理该事件。
ET的工作方式在下一次epoll是不会提醒的,它要求应用程序收到一次提醒后,必须将事件都处理完成。
LT是只要某个文件描述符在readable/writeable,epoll_wait都会返回该socket,而ET是只有在变化时才会返回socket。
硬链接和软链接的区别
- 硬链接指通过索引节点来进行连接 ,硬链接就是多个文件名指向同一个文件的索引节点。当删除原文件后,硬链接仍然可用
- 软链接也是符号链接,它更像一个快捷方式,它指向原文件,当删除原文件后,则软链接不可用。
网络编程
服务端编程API
int getaddrinfo(const char *host, const char *service, const struct addrinfo *hints, struct addrinfo **result); //通过给出host和service两个socket地址的组成部分,getaddrinfo返回result,指向一个addrinfo的链表头节点,接下来遍历这个链表,依次尝试每个套接字地址知道调用socket和connect成功,使用freeaddrinfo释放链表头节点即可。 //addrinfo结构中包含了各种api使用的参数,这样可以使我们编写任何版本ip的程序。 int socket(int domain, int type, int protocol); //domain : 使用哪个协议族 一般为 PF_INET (IPv4) PF_INET6 (ipv6) //type : 使用什么服务类型 SOCK_STREAM 流服务即TCP, SOCK_DGRAM 数据报服务即UDP //protocol : 选择一个具体的协议, 一般设为0 表示使用默认协议 // 系统调用成功返回一个socket文件描述符,失败返回-1 设置errno int bind(int sockfd, const struct sockaddr* my_addr, socklen_t addrlen); //将myaddr所指的socket地址分配给未命名的sockfd文件描述符,addrlen参数指出该socket地址长度。 //成功返回0 失败返回-1 int listen(int sockfd, int backlog); // 指定监听的socket文件描述符, backlog为内核监听队列最长的长度。 int accept(int listenfd, struct sockaddr *addr, socklen_t *addrlen); //listenfd为监听描述符, addr用来获取远端描述符,该地址长度有addrlen指出。 // accept成功返回一个socket文件描述符,否则返回-1 int connect(int clientfd, const struct sockaddr *serv_addr, socklen_t addrlen); // connect尝试与套接字地址为serv_addr的服务器建立一个因特网连接。
reactor和proactor
Reactor和Proactor是两种io多路复用的模式
- Reactor采用的是同步io,proactor采用的是异步io。
- Reactor模式,本质就是当IO事件(如读写事件)触发时,通知我们主动去读取,也就是要我们主动将socket接收缓存中的数据读到应用进程内存中。
- Proactor模式,我们需要指定一个应用进程内的buffer(内存地址),交给系统,当有数据包到达时,则写入到这个buffer并通知我们收了多少个字节。
Reactor 工作流程:
- 主线程向epoll内核事件表中注册socket上的读就绪事件
- 主线程调用epoll_wait等待socket上有数据可读
- 当socket有数据可读,epoll_wait通知主进程,主线程将socket可读事件放入请求队列
- 唤醒请求队列上的某个工作线程,开始从socket读取数据,处理客户需求,并向epoll内核事件表中注册写就绪事件
- 主线程调用epoll_wait等待主线程可写
- socket可写时,epollwait通知主线程,主线程将socket放入请求队列
- 唤醒某个工作进程 向socket上写入服务器处理客户请求的结果
优点
1)响应快,不必为单个同步时间所阻塞,虽然Reactor本身依然是同步的;
2)编程相对简单,可以最大程度的避免复杂的多线程及同步问题,并且避免了多线程/进程的切换开销;
3)可扩展性,可以方便的通过增加Reactor实例个数来充分利用CPU资源;
4)可复用性,reactor框架本身与具体事件处理逻辑无关,具有很高的复用性;
IPC工具
管道
int pipe(int fd[2]); //
管道的性质
fd[0] 和 fd[1] 构成了管道的两端, 向fd[1]输入的数据可以由fd[0]读出,管道是单工的,无法反向传输,若想实现双工,使用两个管道即可,管道的读取与写入的顺序是完全相同的,无法使用lseek来随机读取。
管道是面向字节流的,即数据在管道中是不存在消息或消息边界的概念的,从管道中读取数据可以读取任意大小的数据块,只要满足管道本身的容量限制, 默认大小是65536,可以通过Fcntl修改
fcntl(fd, F_SETPIPE_SZ, size)
可以修改命名管道的容量上限管道缓冲区存在于内核中,每次写入和读出都需要陷入内核态进行操作,所以数据传输速度较慢。
保证写入不超过POP_BUF字节的操作是原子的
管道操作
父子进程通信
在fork()期间,子进程会继承父进程的操作符,将不使用的文件描述符关闭,比如将父进程的fd[0]关闭,子进程的fd[1]关闭,这时就可以在父子进程间进行通信,即父进程可向子进程传输数据。
不能两个进程都不关闭读入描述符,可能会造成两个进程的数据竞争。
相关进程通信
由想要通信的进程共同的祖先进程创建管道,而后除了两个进程需要使用的管道端口外,其他的管道文件描述符全部关闭。这样就可以直接传送数据。
关闭未使用的文件描述符
关闭写入描述符
所有进程的写入描述符关闭后,read才能够读到文件结束符,否则read检测到仍然存在打开的写入描述符,就会阻塞进程等待数据。
关闭读入描述符
当一个进程试图向一个管道中写入数据但是没有任何一个管道读入符打开的时候,内核会向写入进程发送SIGPIPE信号,及时处理这种情况,而若存在一个不使用的读入描述符,那么内核不会发送信号,无法进行处理。
进程同步
进程同步使用的是read的互斥性。父进程创建子进程之前创建一个管道,所有子进程会继承这个管道描述符,让子进程完成其工作后再关闭这些描述符,期间父进程一直被read阻塞,当所有进程完成任务退出后,才能重新进入运行。
为了防止子进程意外退出,可以令所有子进程在管道中写入一字节数据再退出,父进程进行读入分析。
- 使用管道进行同步和信号同步的优缺点
- 管道可以用来一个进程与多个相关进程的同步,而信号不能
- 信号可以被广播到进程组所有成员处。
- 使用管道进行同步和信号同步的优缺点
连接过滤器
使用dup操作,关闭标准输入输出文件并复制管道输入符到STDOUT, 复制管道输出符到STDIN,这样就形成了过滤器
STDOUT -> pfd[1] -> pfd[0] -> STDIN
FIFO
int mkfifo(const char* pathname, mode_t mode)
FIFO与管道类似,其最大差别就是FIFO在文件系统中存在一个名称,并且其打开方式和普通文件是一样的。一旦FIFO被创建,任何进程都能打开它。
消息队列
共享内存的实现及api
int shmget(key_t key, size_t size, int shmflg); //用来创建或打开共享内存 void *shmat(int shmid, const void *shmaddr, int shmflg); //将共享内存段连接到进程地址空间 int shmdt(const void *shmaddr); //将共享内存与当前进程脱离 int shmctl(int shmid, int cmd, struct shmid_ds *buf); //用于控制共享内存
mmap
直接分配虚拟内存,而不映射到物理内存,当真正操作当前内存才通过缺页中断请求分页调入页表缓存
伙伴系统
Linux伙伴系统的引入为内核提供了一种用于分配一组连续的页而建立的一种高效的分配策略,并有效的解决了外碎片问题。
Linux中的内存管理的“页”大小为4KB。
把所有的空闲页分组为11个块链表,每个块链表分别包含大小为1,2,4,8,16,32,64,128,256,512和1024个连续页框的页块。最大可以申请1024个连续页,对应4MB大小的连续内存。每个页块的第一个页的物理地址是该块大小的整数倍。
当向内核请求分配(2^(i-1),2^i]数目的页块时,按照2^i页块请求处理。如果对应的块链表中没有空闲页块,则在更大的页块链表中找。当分配的页块中有多余的页时,伙伴系统根据多余的页框大小插入到对应的空闲页块链表中。
当释放单页的内存时,内核将其置于CPU高速缓存中,对很可能出现在cache的页,则放到“快表”的列表中。在此过程中,内核先判断CPU高速缓存中的页数是否超过一定“阈值”,如果是,则将一批内存页还给伙伴系统,然后将该页添加到CPU高速缓存中。
当释放多页的块时,内核首先计算出该内存块的伙伴的地址。内核将满足以下条件的三个块称为伙伴:(1)两个块具有相同的大小,记作b。(2)它们的物理地址是连续的。(3)第一块的第一个页的物理地址是2*(2^b)的倍数。如果找到了该内存块的伙伴,确保该伙伴的所有页都是空闲的,以便进行合并。内存继续检查合并后页块的“伙伴”并检查是否可以合并,依次类推。
全部评论
(1) 回帖