Unix中select,poll,epoll详解

网络应用需要处理的问题无非两类,网络I/O和数据计算问题。
在处理计算密集型任务的时候,期间会有一些网络IO操作(如写数据库的操作,非本机),此时若使用同步IO,则会造成大量的IO等待,造成CPU使用率较低。所以此时考虑其他IO模型如异步模型。

Unix下网络I/O模型包括五类:

  • 阻塞式IO
  • 非阻塞式IO
  • 多路复用IO
  • 信号驱动IO(边缘触发)
  • 异步IO

其中多路复用I/O机制是通过select,poll以及epoll进行监视。这里暂时只介绍多路复用IO,若想了解其他IO模型,参考《Unix网络编程》第六章

多路复用I/O模型

网络I/O的本质是socket的读取,socket在linux系统中被抽象为流,所以I/O操作可以理解为对流的操作。这个操作包括两个阶段:

  • 等待流数据准备就绪(wait for data be ready)
  • 从内核相进程复制数据

由于非阻塞调用的过程中,轮训占据了大部分的过程,所以轮训会占据大量的CPU时间。如果轮训不是进程的用户态,而是有人帮忙就好了。多路复用正好处理这样的问题。

多路复用的过程:多路复用有两个特别的系统调用select和poll。select调用是内核级别的,select轮训相对于非阻塞的轮训区别在于:前者可以等待多个socket,当其中一个socket数据准备好了,就能返回进行可读,然后进程再进行recvform系统调用,将数据由内核拷贝到进程中,拷贝的过程是阻塞的。

多路复用有两种阻塞,select或poll调用之后,会阻塞进程,与第一种阻塞不同在于,此时的select不是等到socket数据全部到达再处理, 而是有了一部分数据就会调用用户进程来处理。如何知道有一部分数据到达了呢?监视的事情交给了内核,内核负责数据到达的处理。也可以理解为”非阻塞”吧。

类比钓鱼过程:在钓鱼的时候,我们雇了一个帮手,他可以同时抛下多个鱼竿,任何一个鱼竿的鱼一上钩,他就会拉杆。他只负责帮我们钓鱼,并不处理,所以我们在一旁等着,等他收杆之后,我们再进行处理。

多路复用既然可以处理多个IO,也就带来了新的问题:多个IO的顺序不能保证

多路复用的特点多路复用通过一种机制一个进程能同时等待多个IO文件描述符,内核监视这些文件描述符(socket描述符),其中任意一个进入读就绪状态时,select,poll.epoll函数就可以返回。对于监视的方式,有可以分为select,poll,和epoll三种方式。

select函数详解

### 函数原型

1
2
3
#include <select.h>
#include <time.h>
int select(int maxfdp1, fd_set* readset, fd_set* writeset, fd_set* except_set, const struct timeval* timeout);

函数功能

该函数允许进程指示内核等待多个事件中的其中一个发生,并只在有一个或多个事件发生或者经历了一段时间之后才唤醒它。

其中等待的事件类型包括三种:指定集合中的描述符处于可读状态,执行集合中的描述符处于可写状态,指定集合中的描述符有异常未处理。

描述符就绪的条件如下:

可读就绪

当描述符满足下列四个条件中的其中一个,表示该描述符已经准备好读

  • 该套接字接收缓冲区的字节数大于等于套接字接收缓冲区的低水位标记的大小。一般对于TCP和UDP该值默认为1,我们也可以通过SO_RCVLOWAT套接字选项设置该套接字的低水位标记。
  • 该连接读半部关闭(接受了FIN的TCP连接),此时函数返回0。
  • 该套接字是一个监听套接字且完成的连接数不为0。对于这种套接字,accept通常不会阻塞。
  • 其上有一个套接字错误待处理.对这种套接字的读操作将不阻塞病返回-1。

可写就绪

当描述符满足下列四个条件中的其中一个,表示该描述符已经准备好写

  • 该套接字的发送缓冲区的可用空间字节大于等于套接字发送缓冲区的低水位标记大小。TCP和UDP的默认大小一般为2048。可以使用SO_SNDLOWAT套接字选项来设置该套接字的低水位标记
  • 该链接的写半部关闭
  • 使用非阻塞式connect的套接字建立连接,或者connect以失败告终
  • 其上有一个套接字错误未处理

函数参数

  • maxfdp1 : fd_set中最大的描述符+1(特别注意不要忘了+1),如readset中有{1,2,4},writeset中有{5,7,9},except_set中有{2,3,6,10},则此时的maxfdp1为11
  • readset : 需要监听的满足可读条件的描述符集合
  • writeset : 需要监听的满足可写条件的描述符集合
  • except_set : 需要监听的满足异常的描述符集合
  • timeout : 等待的时间,若超过此时间,函数返回

timeout的三种情况

  • timeout=NULL,等待时间无限长,即不限等待时间
  • timeout->sec=0,timeout->usec=0。此时不等待,函数立即返回
  • timeout->sec!=0 || timeout->usec != 0。此时为等待时间

函数返回值

  • 当监视的相应的文件描述符集合中存在满足条件的描述符时,比如说读文件描述符集中有数据到来时,内核IO根据状态修改文件描述符集,并返回一个大于0的数
  • 当没有满足条件的描述符且设置的timeval超时时,select函数返回0
  • 出错返回负数

若存在满足条件的描述符时,内核会将满足条件的描述符置位,并将其他描述符清0.这时,我们可以通过FD_ISSET来判断当前描述符是否满足条件.
如:
假设set为8位表示,起始为0000 0000。此时将{1,2,5}设置到读文件描述符集合中,即:

1
2
3
FD_SET(1, &readset);
FD_SET(2, &readset);
FD_SET(3, &readset);

置位以后set的位为:0000 0111

当调用select函数,并文件描述符2准备就绪时,此时select函数返回大于0的值,set的值变为:0000 0010。此时使用FD_ISSET可以检测到文件描述符2已经就绪。

fd_set相关操作

1
2
3
4
void FD_ZERO(fd_set* set); //将fd_set清空,一般声明fd_set第一步现先将其清空
void FD_SET(int fd, fd_set* set); //将某个fd置位
void FD_CLR(int fd, fd_set* set); //清空某个fd
int FD_ISSET(int fd, fd_set* set); //判断fd是否在set中

select函数底层实现原理

select底层实现的大致原理是,通过轮训文件描述符集中的文件描述符,检查描述符是否达到条件,若达到符合的相关条件则返回,否则轮训,但是当轮训的机制虽然是死循环,但是不是一直轮训,当内核轮训一遍文件描述符之后,会调用schedule_timeout函数挂起,等待fd设备或定时器来唤醒自己,然后再继续循环体看看哪些fd可用,以此提高效率。

若要了解详细的select实现原理参考如下博客:
http://janfan.cn/chinese/2015/01/05/select-poll-impl-inside-the-kernel.html
http://zhangyafeikimi.iteye.com/blog/248815

select函数的特点

select和poll为水平触发,epoll即支持水平触发也支持边缘触发。

缺点:

  • 最大并发数限制:从上面可以看出,被监听的描述符集合的大小受fe_set大小的限制,所以select监听的描述符的个数是有限制的,一般默认个数为1024或4096个等。
  • 效率问题:从select的底层实现可以看出,select每次调用都会线性扫描全部的fd集合,这样效率会出现线性下降,当把FD_SETSIZE增大可能会出现超时.
  • 内核用户空间拷贝问题:从select实现源码中不难看出,描述符集合以及timeout参数都是通过内存拷贝的方式从用户空间拷贝到了内核空间,也是会影响函数的性能。

poll函数详解

函数原型

1
2
3
4
5
6
7
8
9
10
#include <poll.h>
#include <time.h>
struct pollfd{
int fd; //file descriptor
short events; //被监听的事件状态(即select中监听当前描述符是否可写或者可读或者异常等等)
shor revents; //函数返回时该文件描述符的状态
};
int poll(struct pollfd* fds, unsigned long nfds, int timeout);

函数功能

poll的函数功能与select功能基本类似。但是poll函数可监听的文件描述符的个数基本没有限制,poll管理多个文件描述符的方式与select一致,都是轮训,并且都是讲文件描述符数组从用户空间复制到内核空间。

函数参数

  • fds : 被监听的描述符的数组
  • nfds : 数组中描述符的个数,这个大小足以监听linux所有的文件描述的符
  • timeout : 等待的时间

events的合法事件:

1
2
3
4
5
6
7
8
POLLIN --- 有数据可读(普通或优先级带数据)等价于POLLRDNORM||POLLRDBAND
POLLRDNORM --- 有普通数据可读
POLLRDBAND --- 有优先级带数据可读
POLLPRI --- 有高优先级数据可读
POLLOUT --- 有数据可写(普通数据)等价于POLLWRNORM
POLLWRNORM --- 有普通数据可写
POLLWRBBAND --- 有优先级带数据可写

初此之外,revents返回的事件还有:

1
2
3
POLLER --- 发生错误
POLLHUP --- 发生挂起
POLLNVAL --- 指定的描述符非法,没有打开

POLLIN | POLLPRI等价于select的读事件

POLLOUT | POLLWRBBAND 等价于select的写事件

函数返回值

  • 若监听的描述符满足条件,返回revents域不为0的文件描述符的个数
  • 若没有描述符满足条件且已过超时时间,poll返回0
  • 出错返回-1,并设置errno为以下值:
1
2
3
4
5
EBADF --- 一个或多个结构体中的描述符无效
EFAULTfds --- 指针指向的地址超出进程的地址空间
EINTR --- 请求的时间之前产生一个信号,调用可以重新发起
EINVALnfds --- 参数超出PLIMIT_NOFILE的值
ENOMEM --- 可用内存不足,无法完成请求

poll函数优缺点

优点

  • poll函数不需要计算最大的文件描述符加1.
  • poll函数监听的文件描述符的个数不受限制
  • poll相对于select函数应付大数目的描述符的效率较高。

缺点:

  • poll函数没有解决select轮训所有文件描述符的问题
  • poll函数和select相同都是将文件描述符信息从用户空间拷贝到内核空间。

epoll函数详解

函数原型

epoll相关数据结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//epoll_data保存触发事件相关的数据。(数据类型与具体使用方式有关)
typedef union epoll_data{
void* ptr;
int fd;
_uint32_t u32;
_uint64_t u64;
} epoll_data_t;
//保存感兴趣的事件和被触发的事件
struct epoll_event{
_uint32_t events;
epoll_data_t data;
};

其中events是一个美剧类型的集合,我们可以使用”|”来增加感兴趣的事件。枚举类型的值包括下面:

  • EPOLLIN : 表示关联的fd可以进行读操作
  • EPOLLOUT :表示关联的fd可以进行写操作
  • EPOLLRDHUP(2.6.17之后):表示套接字关闭了连接,或关闭了正写的一半的连接
  • EPOLLPRI : 表示关联的fd有紧急优先事件可以进行读操作。
  • EPOLLERR : 表示关联的fd发生了错误,epoll_wait会一直等待这个事件,所以一般没有必要设置这个属性
  • EPOLLHUP : 表示关联的fd被挂起,epoll_wait会一直等待这个事件,所以一般没有必要设置这个属性
  • EPOLLET : 设置关联的fd为ET的工作方式,即边缘触发
  • EPOLLONESHOT : 设置关联的fd为one-shot工作方式,表示只监听一次事件,如果要再次监听,需要把socket放入到epoll队列中。

epoll相关的函数有三个。

1
2
3
int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event* event);
int epoll_wait(int epfd, struct epoll_event* events, int maxevents, int timeout);
  • epoll_create : 创建一个epoll句柄,注意创建epoll句柄会占用一个文件描述符,在使用完之后需要关闭。否则可能会导致文件描述符耗尽。
    • size : size为最大的监听文件描述符数,监听的文件描述符的个数不能超过size可以手动指定,但是这个数值可以达到系统可以开的最大的文件描述符数。
  • epoll_ctl : epoll的事件注册函数,它不同于select的是,它不是在监听事件的时候告诉内核要监听什么类型的时间,而是先注册要监听的事件类型。
    • epfd : epoll文件描述符,即epoll_ create的返回值,表示该epoll描述符注册事件
    • op : 注册事件的类型包括以下三类。
      • EPOLL_CTL_ADD : 注册行的fd到epfd中
      • EPOLL_CTL_MOD : 修改已经注册的fd的事件类型
      • EPOLL_CTL_DEL : 删除已经注册的fd
    • fd : 注册的文件描述符
    • event : 注册的时间的类型,告诉内核需要监听什么事件,类型包括上面几种。
  • epoll_wait : 收集epoll监控的时间中已经就绪的事件,若调用成功,返回就绪的文件描述符的个数,返回0表示超时。
    • epfd : epoll的文件描述符
    • events : 已经就绪的事件集合.内核不分配内存,需要程序自己分配内存传给内核,内核只负责将书复制到这里
    • maxevents : events数组的大小。
    • timeout : 超时时间。

水平触发(LT)与边缘触发(ET)

epoll的默认工作模式是水平触发(LT)。NGINX使用的epoll的ET工作模式

水平触发(level_triggered):当被监控的文件描述符上有可读可写事件的时,epoll_wait()会通知处理程序去读写。如果程序没有一次性把缓冲区的数据读完或者写完,那么下次调用epoll_wait的时候,他还会通知你该文件描述符仍可读写,如果你一直不去读写,它会一直通知你。如果系统中有大量你不需要读写的文件描述符,而他们每次都返回,这样会大大降低处理程序检索自己关心的就绪文件描述符的效率。

边缘触发(edge-triggered):当被监控的文件描述符上有读写事件发生时,epoll_wait会通知处理程序去读写,如果数据没有一次性读写完,那么下次你再调用epoll_wait的时候,它不会通知你,只有等到下一次发生读写事件的时候,它才会通知你。这种模式比水平触发的效率高,系统不会充斥大量你不关心的文件描述符。

注意:epoll工作在ET模式的时候,必须使用非阻塞的套接字,以避免由于一个文件句柄的阻塞读/阻塞写把多个文件描述符的任务饿死。最好以下面两种方式调用epoll接口

  • 基于非阻塞文件句柄
  • 只有当read/write返回值为EAGAIN时才需要挂起。但这不是说每次都需要循环读,直到读到产生EAGAIN才结束,只要读取到的长度小于预期的长度就说明缓冲区的数据我们已经读完了。

epoll族函数底层实现

epoll的使用方法上面已经有详细的描述,借口也简单易用。首先我们通过epoll_create创建一个epoll文件描述符,然后在epoll文件描述符上注册需要监听的事件,最后使用epoll_wait等待准备就绪的文件描述符。然而在每一步的过程中,内核都做了哪些操作?底层的实现方式是怎么样的?

内核使用了slab机制,为epoll提供了高效快速的数据结构。在内核中,epoll向内核注册了一个文件系统,用于存储被监控的文件描述符的信息。epoll在被内核初始化的时候(操作系统启动),同时会开辟出epoll自己的告诉cache区,用于安置我们需要监控的fd信息,这些fd信息会以红黑树的结构保存在内核cache中,以支持快速的查找,插入删除操作。这个内核高速cache区,就是建立连续的物理内存页,然后在之上建立slab层,简单的说,就是物理上分配好你想要的size的内存对象,每次使用时都是使用空闲的已分配好的对象。

epoll fd在内核中对应的数据够如下:

1
2
3
4
5
6
7
8
9
struct eventpoll {
spin_lock_t lock; //对本数据结构的访问
struct mutex mtx; //防止使用时被删除
wait_queue_head_t wq; //sys_epoll_wait() 使用的等待队列
wait_queue_head_t poll_wait; //file->poll()使用的等待队列
struct list_head rdllist; //事件满足条件的链表
struct rb_root rbr; //用于管理所有fd的红黑树(树根)
struct epitem *ovflist; //将事件到达的fd进行链接起来发送至用户空间
}

当向系统中添加一个fd时,就创建一个epitem结构体,这是内核管理epoll的基本数据结构:

1
2
3
4
5
6
7
8
9
10
11
12
struct epitem {
struct rb_node rbn; //用于主结构管理的红黑树
struct list_head rdllink; //事件就绪队列
struct epitem *next; //用于主结构体中的链表
struct epoll_filefd ffd; //这个结构体对应的被监听的文件描述符信息
int nwait; //poll操作中事件的个数
struct list_head pwqlist; //双向链表,保存着被监视文件的等待队列,功能类似于select/poll中的poll_table
struct eventpoll *ep; //该项属于哪个主结构体(多个epitm从属于一个eventpoll)
struct list_head fllink; //双向链表,用来链接被监视的文件描述符对应的struct file。因为file里有f_ep_link,用来保存所有监视这个文件的epoll节点
struct epoll_event event; //注册的感兴趣的事件,也就是用户空间的epoll_event
}
  1. 当调用epoll_create的时候,会首先在epoll内存中为分配一个eventpoll的内存大小,以保存当前的epoll描述符(epfd)结构,然后在这块内存上打开一个epoll文件。
  2. 当调用epoll_ctl的时候,如果增加fd(socket),则检查在红黑树中是否存在,存在立即返回,不存在则添加到红黑树上,然后向内核注册回调函数,用于当中断事件来临时向准备就绪list链表中插入数据。当我们调用epoll_ctl往里塞入百万个fd时,epoll_wait仍然可以飞快的返回,并有效的将发生事件的fd给我们用户。这是由于我们在调用epoll_create时,内核除了帮我们在epoll文件系统里建了个file结点,在内核cache里建了个红黑树用于存储以后epoll_ctl传来的fd外,还会再建立一个list链表,用于存储准备就绪的事件。
  3. 调用epoll_wait的时候立即返回准备就绪链表中的数据即可。

如此,一颗红黑树,一张准备就绪fd链表,少量的内核cache,就帮我们解决了大并发下的fd(socket)处理问题。

如果需要了解更详细的epoll底层实现,参考一下链接:
http://www.cnblogs.com/apprentice89/p/3234677.html

epoll特点

  • 没有文件描述符个数限制
  • 使用注册监听时间的方式,无需每次wait时都将时间从用户空间拷贝到内核空间,节省了内存拷贝的时间。
  • 使用回调机制,无需轮训所有的文件描述符检查状态。
  • 返回值只有准备就绪的文件描述符,检查准备就绪的文件描述符也不需要轮训。

Comment and share

汇编基础之寄存器及汇编指令

参考链接:
http://www.cnblogs.com/technology/archive/2010/05/16/1736782.html
http://anonymalias.github.io/2017/01/09/ucontext-theory/

最近在学习协程方面的知识,在协程的一种实现方式中有一种是用汇编实现的,所以这里再把汇编知识复习一下。

寄存器

寄存器按照其用途可分为以下4类:

  • 数据寄存器
  • 指针及变址寄存器
  • 段寄存器
  • 控制寄存器

数据寄存器

数据寄存器包括4个16位的寄存器(AX,BX,CX,DX)或者8个8位的寄存器(AH,AL,BH,BL,CH,CL,DH,DL),这些寄存器都是用来暂时存放操作数,运算结果或者其他信息,但同时又具有某些专门的用途。

  • AX(累加寄存器):算术运算中的主要寄存器,在乘除运算中用来指定除数和被除数,也是乘除运算后积和商的默认存储单元。,另外IO指令均使用该寄存器与IO设备传送信息
  • BX(基址寄存器):指令寻址时常用作基址寄存器,存入偏移量或者偏移量的构成部分。
  • CX(计数寄存器):在循环指令操作或者串处理指令中隐含计数
  • DX(数据寄存器):在双字节运算中,与AX构成32位操作数,DX为高16位。在某些IO指令中,DX被用来存放端口地址

指针及变址寄存器

这些寄存器都是16位的寄存器,用来存放16为的操作数或者中间结果,但是更常见的是存放偏移量或者位移量

  • SP(堆栈指针寄存器):指向栈顶的位置,与SS寄存器一起组成栈顶数据的物理地址
  • BP(基址指针寄存器):系统默认其指向堆栈中的某一单元,即提供栈中该单元偏移量,加段前缀后,BP可作为非堆栈段的地址指针。一般用于识别栈帧的起始位置。
  • SI(源变址寄存器):与DS联用, 指示数据段中某操作的偏移量. 在做串处理时, SI指示源操作数地址, 并有自动增量或自动减量的功能. 变址寻址时, SI与某一位移量共同构成操作数的偏移量
  • DI(目的变址寄存器):与DS联用, 指示数据段中某操作数的偏移量, 或与某一位移量共同构成操作数的偏移量. 串处理操作时, DI指示附加段中目的地址, 并有自动增量或减量的功能

段寄存器

  • CS(代码段):存放当前程序的指令代码
  • DS(数据段):存放程序所涉及的源数据以及结果
  • SS(堆栈段):以先进后出原则的数据区
  • ES(附加段):辅助数据区,存放串或者其他数据

控制寄存器

  • IP(指令寄存器): 存放下一条要执行的指令的偏移量
  • FR(控制标志位):
    • CF(进位标识位):进行加减运算时, 如果最高二进制位产生进位或错位, CF则为1, 否则为0. 程序设计中, 常用条件转移指令JC, JNC指令据此标志位实现转移
    • PF(奇偶标志位):操作结果中二进制位1的个数为偶数是, PF为1, 某则为0
    • AF(辅助进位标志位):运算时半字节产生进位或借位时, AF为1, 某则为0. 主要用于BCD码的调整
    • ZF(零标志位):运算结果为0时, ZF为1, 否则为0
    • SF(符号标志位):当运算结果的最高位为1时, SF为1, 否则为0. 最高位表示符号数的正和负
    • TF(跟踪标志位):用于调试程序时进入单步方式工作. TF=1时, 每条指令执行完后产生一个内部中断, 让用户检查指令运行后寄存器, 存储器和各标志位的内容. TF=0时, CPU工作正常, 不产生内部中断
    • IF(中断允许标志位):IF=1同时中断屏蔽寄存器的相应位为0, 允许系统响应可屏蔽中断, 反之, 不接收外部发出的中断请求
    • DF(方向位标志位):用于控制串操作时地址指针位移方向. 当DF=1时, 指针向高地址方向移动
    • OF(溢出标志位):算术运算时结果超出系统所能表示的数的范围. 溢出时, OF=1

注意:上述为16位处理其器中的寄存器的名字,在32和64位系统中,寄存器中名字有相应的变化,具体看下面(32位在前面加E,64在前面加R,64位系统的中含有16个64位的通用寄存器):

i386常用的16个寄存器

  • EAX、EBX、ECX、EDX这四个寄存器,主要就是用来暂时存放计算过程中所用的操作数、结果或其它信息。
  • ESP为堆栈指针寄存,它和堆栈段寄存器SS共同决定了当前的栈指针,每当执行push,pull操作时,或者因为某些原因(如中断),CPU自动将数据入栈时,就会使用该指针来找到堆栈栈顶(或栈底)的位置,然后执行压栈、出栈的操作。系统管理软件,如操作系统会根据其分配的栈空间地址来设定这两个寄存器的值。
  • EBP称为基址指针寄存器,它和ESP都可以与堆栈段寄存器SS联用来确定堆栈中的某一存储单元的地址,ESP用来指示段顶的偏移地址,而EBP可作为堆栈区中的一个基地址以便访问堆栈中的信息。
  • ESI(源变址寄存器)和EDI(目的变址寄存器)一般与数据段寄存器DS联用,用来确定数据段中某一存储单元的地址。这两个变址寄存器有自动增量和自动减量的功能,可以很方便地用于变址。在串处理指令中,ESI和EDI作为隐含的源变址和目的变址寄存器时,ESI和DS联用,EDI和附加段ES联用,分别达到在数据段和附加段中寻址的目的。
  • EIP指令指针寄存器,它用来存放代码段中的偏移地址。在程序运行的过程中,它始终指向下一条指令的首地址。它与段寄存器CS联用确定下一条指令的物理地址。当这一地址送到存储器后,控制器可以取得下一条要执行的指令,而控制器一旦取得这条指令就马上修改EIP的内容,使它始终指向下一条指令的首地址。那些跳转指令,就是通过修改EIP的值来达到相应的目的的
  • FLAGS标志寄存器,又称PSW(program status word),即程序状态寄存器。这一个是存放条件标志码、控制标志和系统标志的寄存器。
  • 段寄存器:一共六个,分别是CS代码段,DS数据段,ES附加段,SS堆栈段,FS以及GS这两个还是附加段。
    EFLAGS寄存器中的IF位表示是否允许中断,为1允许,否则不允许。
  • TR寄存器:用来指向当前任务的TSS段
  • IDTR寄存器:用来指向当前IDT(中断表述符表或者说是中断向量表),因为在保护模式下,IDT的起始地址可以在任何位置,而不仅限于地址0。
  • GDT和LDT : 前者是全局描述符表,位置由GDTR寄存器确定,后者是局部描述符表,位置由LDTR寄存器确定,具体使用哪一个,取决于段选择码中的TI位。

汇编指令

汇编指令格式

汇编指令的格式如下:

1
[标号:] 指令助记符[[目的操作数][,源操作数]][;注释]

  • 指令助记符:如MOV,ADD之类标识传送,加法。不区分大小写
  • 目的操作数:作用有两个,1.参与指令操作2,暂时存储操作结果
  • 源操作数:主要提供原始数据或操作对象。面向所有寻址方式
  • 注释:用分号隔开

汇编指令中常见的符号:

  • imme:立即数
  • DST:目的操作数
  • SRC:源操作数
  • mem:存储器操作数
  • OPR:操作数
  • reg:通用寄存器
  • EA:偏移地址
  • Sreg:段寄存器
  • Port:端口地址
  • Lable:标号

汇编指令可以分成六类:

  • 数据传送指令
  • 算数运算指
  • 逻辑运算与移位指令
  • 串操作指令
  • 程序控制指令
  • 处理器控制指令

    数据传送指令

    数据传送指令

  • MOV DST,SRC(传送指令):把源操作数的内容送入目的操作数
    • 立即数做源操作数时,立即数的长度必须小于等于目的操作数的长度
    • 操作数分别为reg,reg或reg,sreg或sreg,sreg或reg,sreg时,两者的长度必须保持一致
    • 立即数不能作为目的操作数
    • CS和IP寄存器不能做目的操作数,不允许用立即数为段寄存器赋值
    • 不能将一个段寄存器的内容直接送到另一个段寄存器中, 可借助通用寄存器或PUSH, POP指令实现这一要求
  • PUSH SRC(压栈指令): 将一个字数据压入当前栈顶, 位移量disp=-2的地址单元. 数据进栈时, 栈指针SP首先向低地址方向移动两个字节位置, 接着数据进栈, 形成新的栈顶
  • POP DST(出栈指令):弹出栈顶元素, 后将栈顶指针向栈底方向移动一个字
  • XCHG OPR1, OPR2(交换指令):交换指令: 将这两个操作数交换

地址传送指令

  • LEA DST, SRC(装载有效指令):该指令将源操作数的偏移量OA装载到目的操作数中
  • LDS DST, SRC(装载数据段指针指令):将当前数据段中的一个双字数据装入到一个通用寄存器SI(双字数据的低字)和数据段寄存器DS(双字数据的高字)中
  • LES DST,SRC(装载附加段指针指令):将附加数据段中的一个32位地址数据指针(附加段指针)送到DI(低字)和ES(高字)寄存器中

标志传送指令

  • LAHF(标志寄存器送AH指令): 将标志寄存器的低字节送入AH中
  • SAHF(AH送标志寄存器指令): 将AH寄存器内容送标志寄存器FR的低字节
  • PUSHF(标志进栈指令): 标志寄存器进栈
  • POPF (标志出栈指令): 标志寄存器出栈

累加器专用传送指令

  • IN AL, Port:从端口读入数据, 存放在AL中
  • OUT PORT,AL:传送AL中的数据到端口
  • XLAT OPR或XLAT:用于将AL中当前内容转换为一种代码

算术运算指令

加法指令

  • ADD DST, SRC:DST+SRC的和存放到DST中去
  • ADC DST, SRC:带进位加法指令, DST+SRC+CF
  • INC DST:增1指令

减法指令

  • SUB DST, RSC:DST-SRC, 存放到DST中
  • SBB DST, SRC:带借位减法指令, DST-SRC-CF
  • DEC DST :减1指令
  • NEG DST:求补指令, 求补码
  • CMP OPR1, OPR2:比较指令

乘法指令

  • MUL SRC:无符号数乘指令, AL*SRC, 结果放入AX中
  • IMUL SRC:有符号数乘指令, AL*SRC, 结果放入AX中

除法指令

  • DIV SRC :无符号数除指令, AX/SRC, 商放入AL中, 余数放在AH中
  • IDIV SRC:符号数除指令, AX/SRC, 上放入AL中, 余数放在AH中
  • CBW, CWD:都是符号扩展指令. 将AL的符号扩到AX中; 将AX的符号扩到DX

逻辑运算与移位指令

逻辑运算指令

  • NOT OPR:逻辑非指令
  • AND OPR:逻辑与指令
  • OR OPR:逻辑或指令
  • XOR OPR :逻辑异或指令

移位指令:

  • SHL DST, CNT:逻辑左移
  • SHR DST, CNT:逻辑右移
  • SAL DST, CNT:算术左移
  • SAR DST, CNT:算术右移

循环移位指令

  • ROL DST, CNT:循环左移
  • ROR DST, CNT:循环右移
  • RCL DST, CNT:带进位循环左移
  • RCR DST, CNT:带进位循环右移

串操作指令

  • MOVS:串传送指令
  • CMPS:串比较指令
  • SCAS:串扫描指令
  • LODS:装入串指令
  • STOS:存储串指令

控制转移指令

转移指令:

  • JMP:无条件转移指令
  • JX:条件转移指令(JC/JNC, JZ/JNZ, JE/JNE, JS/JNS, JO/JNO, JP/JNP…)

循环指令

  • LOOP 标号:该指令执行时, 技术寄存器CXX首先减1, 然后判断CX, 若为0, 跳出循环

条件循环指令

  • LOOPZ/LOOPE, LOOPNZ/LOOPNE:前者用于找到第一个不为0的事件, 后者用于找到第一个为0的事件

子程序调用指令

  • CALL 子程序名:段内直接调用
  • RET

中断指令

  • INT N(中断类型号):软中断指令
  • IRET:中断返回指令

处理器控制指令

标志处理指令:

  • CLC:进位标志CF置0
  • CMC:进位标志CF求反
  • STC:进位标志值1
  • CLD:方向标志置0
  • STD:方向标志置1
  • CLI:中断允许标志置0
  • STI:中断允许标志置1

其他处理器控制指令:

  • NOP:空操作
  • HLT:停机
  • WAIT:等待
  • ESC:换码
  • LOCK:封锁

Comment and share

C++协程及其原理

协程的几种实现方式及原理

协程又可以称为用户线程,微线程,可以将其理解为单个进程或线程中的多个用户态线程,这些微线程在用户态进程控制和调度.协程的实现方式有很多种,包括

  1. 使用glibc中的ucontext库实现
  2. 利用汇编代码切换上下文
  3. 利用C语言语法中的switch-case的奇淫技巧实现(protothreads)
  4. 利用C语言的setjmp和longjmp实现

实际上,无论是上述那种方式实现协程,其原理是相同的,都是通过保存和恢复寄存器的状态,来进行各协程上下文的保存和切换。

协程较于函数和线程的优点

  • 相比于函数:协程避免了传统的函数调用栈,几乎可以无限地递归
  • 相比与线程:协程没有内核态的上下文切换,近乎可以无限并发。协程在用户态进程显式的调度,可以把异步操作转换为同步操作,也意味着不需要加锁,避免了加锁过程中不必要的开销。

进程,线程以及协程的设计都是为了并发任务可以更好的利用CPU资源,他们之间最大的区别在于CPU资源的使用上:

  • 进程和线程的任务调度是由内核控制的,是抢占式的;
  • 协程的任务调度是在用户态完成,需要代码里显式地将CPU交给其他协程,是协作式的

由于我们可以在用户态调度协程任务,所以我们可以把一组相互依赖的任务设计为协程。这样,当一个协程任务完成之后,可以手动的进行任务切换,把当前任务挂起(yield),切换到另一个协程区工作.由于我们可以控制程序主动让出资源,很多情况下将不需要对资源进行加锁。

Comment and share

ucontext族函数详解

the ucontext_t type is a structure type suitable for holding the context for the user thread of execution. A thread’s context include stack,saved registersm a list of block signals

上述为ncontext_t结构体的定义,ucontext机制是GNU C库提供的一组创建,保存,切换用户态执行上下文的API,从上面的描述可以看出ucontext_t结构体使得用户在程序中保存当前的上下文成为可能。我们也可以利用此实现用户级线程,即协程。

ucontext_t以及ucontext族函数

ucontext_t结构体

ucontext_t结构体定义,一个ucontext_t至少包括以下四个成员,可能依据不同系统包括其他不同的成员。

1
2
3
4
5
6
7
8
#include <ucontext.h>
typedef struct ucontext_t {
struct ucontext_t* uc_link;
sigset_t uc_sigmask;
stack_t uc_stack;
mcontext_t uc_mcontext;
...
};

类成员解释:

  • uc_link:为当前context执行结束之后要执行的下一个context,若uc_link为空,执行完当前context之后退出程序。
  • uc_sigmask : 执行当前上下文过程中需要屏蔽的信号列表,即信号掩码
  • uc_stack : 为当前context运行的栈信息。
  • uc_mcontext : 保存具体的程序执行上下文,如PC值,堆栈指针以及寄存器值等信息。它的实现依赖于底层,是平台硬件相关的。此实现不透明。

ucontext族函数

ucontext族函数主要包括以下四个:

1
2
3
4
5
#include <ucontext.h>
void makecontext(ucontext_t* ucp, void (*func)(), int argc, ...);
int swapcontext(ucontext_t* olducp, ucontext_t* newucp);
int getcontext(ucontext_t* ucp);
int setcontext(const ucontext_t* ucp);

  • makecontext:初始化一个ucontext_t,func参数指明了该context的入口函数,argc为入口参数的个数,每个参数的类型必须是int类型。另外在makecontext之前,一般需要显示的初始化栈信息以及信号掩码集同时也需要初始化uc_link,以便程序退出上下文后继续执行。
  • swapcontext:原子操作,该函数的工作是保存当前上下文并将上下文切换到新的上下文运行。
  • getcontext:将当前的执行上下文保存在ucp中,以便后续恢复上下文
  • setcontext : 将当前程序切换到新的context,在执行正确的情况下该函数直接切换到新的执行状态,不会返回。

注意:setcontext执行成功不返回,getcontext执行成功返回0,若执行失败都返回-1。若uc_link为NULL,执行完新的上下文之后程序结束。

简单实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <ucontext.h>
#include <iostream>
#include <unistd.h>
void newContextFun() {
std::cout << "this is the new context" << std::endl;
}
int main() {
char stack[10*1204];
//get current context
ucontext_t curContext;
getcontext(&curContext);
//modify the current context
ucontext_t newContext = curContext;
newContext.uc_stack.ss_sp = stack;
newContext.uc_stack.ss_size = sizeof(stack);
newContext.uc_stack.ss_flags = 0;
newContext.uc_link = &curContext;
//register the new context
makecontext(&newContext, (void(*)(void))newContextFun, 0);
swapcontext(&curContext, &newContext);
printf("main\n");
return 0;
}

Comment and share

C/C++语言内存对齐

内存对齐:在计算机中,内存空间按照字节划分,理论上可以从任何起始地址访问任何类型的变量。但实际上在访问特定类型的变量的时候需要从特定的地址开始,这就需要各种类型的数据按照一定的规则在空间上排列,而不是顺序的一个接一个的存放,这就是内存对齐,也叫字节对齐。

内存对齐的作用:

  • 可移植性:因为不同平台对数据的在内存中的访问规则不同,不是所有的硬件都可以访问任意地址上的数据,某些硬件平台只能在特定的地址开始访问数据。所以需要内存对齐。
  • 性能原因:一般使用内存对齐可以提高CPU访问内存的效率。如32位的intel处理器通过总线访问内存数据,每个总线周期从偶地址开始访问32位的内存数据,内存数据以字节为单位存放。如果32为的数据没有存放在4字节整除的内存地址处,那么处理器需要两个总线周期对数据进行访问,显然效率下降很多;另外合理的利用字节对齐可以有效的节省存储空间。

默认内存对齐影响因素:与平台架构(位数)和编译器的默认设置有关。

总线周期:CPU通过总线和存储器或者IO设备进行一次数据传输需要的时间,通常为四个或者多个时钟周期组成。

内存对齐规则

  1. 整体类型的对齐规则:若设置了内存对齐为m个字节,类中的最大成员的对齐字节为n,则该数据类型的对齐字节为p=min(m,n)。(一般32位机器的默认pack为4位;64位机器的默认pack为8位,程序中可以显式设置pack的大小)
  2. 类型中成员的对齐规则:类中的第一个成员放在offset为0的位置;对于其他成员,若设置了内存对齐为m个字节,假设该数据成员的对齐字节数(即当前成员所占的字节数)为k,则该数据成员的起始位置是min(m,k)的整数倍。
  3. 整体对齐规则:最后整个类型的大小为p=min(m,n)的整数倍。
  4. 当设置对齐字节数大于类中最大成员的对齐字节数的时候,这个设置实际不产生任何效果;当设置对齐字节数为1时,类的大小就是简单的把所有成员大小相加。

实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>
#include <stddef.h>
using namespace std;
//前提条件:32位机器,当前编译器默认pack为4
struct T {
int a;
short b;
int c;
double d;
};
int main() {
cout << offsetof(T, a) << endl
<< offsetof(T, b) << endl
<< offsetof(T, c) << endl
<< offsetof(T, d) << endl;
return 0;
}

根据上面的分析:

  • a为第一个成员,offset为0。
  • b为short,对齐字节数为2,所以其对齐字节为min(2,4)=2,故offset为4
  • c为int,对齐字节数为4,所以其对齐字节数为min(4,4)=4,故offset为8
  • d为double,对齐字节数为8,故对齐字节数为min(4,8)=8,故offset为12
  • 总的大小为20,是,min(8,4)的倍数.

使用pragma pack修改系统默认pack

修改系统的默认pack可以使用系统函数pragma的pack参数,但是修改之后的pack一定是2的n次幂

1
2
3
#pragma pack(16) //修改pack修改为16
#pragma pack() //恢复系统的默认pack
#pragma pack(show) //返回系统当前的pack,由警告信息显示,注意gcc不支持。只有VS支持

此外pack还有push,pop其他参数可选,但是不同的编译器对这些参数的实现有不同的含义,如果需要了解可以参考对应的资料。

Comment and share

异步刷盘与同步刷盘的区别

存储系统往往追求尽量高的吞吐,无论是传统的MySQl还是分布式存储系统Hbase,在写入的时候会尽量提升系统的吞吐。一般来说,提升系统性能的方式是先将数据写入内存中,然后再刷盘到磁盘中进行持久化。

刷盘对应于MySQL中的fsync和hbase中的flush,在mysql中标识将写入的redo log以及bin Log持久化到磁盘中,这样就保证mysql可以持续的对外提供数据服务;在hbase中表示将MainStore中的数据持久化到磁盘中。一般来说刷盘的方式有同步刷盘和异步刷盘两种。

同步刷盘和异步刷盘的区别如下:

  • 同步刷盘:当数据写如到内存中之后立刻刷盘(同步),在保证刷盘成功的前提下响应client。
  • 异步刷盘:数据写入内存后,直接响应client。异步将内存中的数据持久化到磁盘上。

同步刷盘和异步输盘的优劣:

  • 同步刷盘保证了数据的可靠性,保证数据不会丢失。
  • 同步刷盘效率较低,因为client获取响应需要等待刷盘时间,为了提升效率,通常采用批量输盘的方式,每次刷盘将会flush内存中的所有数据。(若底层的存储为mmap,则每次刷盘将刷新所有的dirty页)
  • 异步刷盘不能保证数据的可靠性.
  • 异步刷盘可以提高系统的吞吐量.
  • 常见的异步刷盘方式有两种,分别是定时刷盘和触发式刷盘。定时刷盘可设置为如每1s刷新一次内存.触发刷盘为当内存中数据到达一定的值,会触发异步刷盘程序进行刷盘。

Comment and share

Linux ps命令使用详解

ps命令常见使用方法有两种,另外我们可以通过一写参数对ps结果进行筛选和过滤。分别是:

1
2
ps -ef
ps aux

那这两个命令有什么区别呢?下面将对这两个命令进行详细说明和解释。

ps -ef

返回信息的格式:

1
2
3
4
5
6
7
8
UID PID PPID C STIME TTY TIME CMD
root 1 0 0 9月24 ? 00:00:25 /sbin/init
root 2 0 0 9月24 ? 00:00:00 [kthreadd]
root 3 2 0 9月24 ? 00:00:00 [ksoftirqd/0]
root 5 2 0 9月24 ? 00:00:00 [kworker/0:0H]
root 7 2 0 9月24 ? 00:09:13 [rcu_sched]
root 8 2 0 9月24 ? 00:00:00 [rcu_bh]
...

对以上各列信息进行解释:

  • uid : 用户id
  • pid : 当前进程的id
  • ppid : 当前进程的父进程id
  • C : 进程占用CPU的百分比
  • STIME : 进程启动时间
  • CMD : 进程启动命令

上述返回值比较容易理解,不一一解释

ps aux

返回信息的格式:

1
2
3
4
5
6
7
8
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.0 0.0 120060 5392 ? Ss 9月24 0:25 /sbin/init
root 2 0.0 0.0 0 0 ? S 9月24 0:00 [kthreadd]
root 3 0.0 0.0 0 0 ? S 9月24 0:00 [ksoftirqd/0]
root 5 0.0 0.0 0 0 ? S< 9月24 0:00 [kworker/0:0H]
root 7 0.1 0.0 0 0 ? S 9月24 9:14 [rcu_sched]
root 8 0.0 0.0 0 0 ? S 9月24 0:00 [rcu_bh]
root 9 0.0 0.0 0 0 ? S 9月24 0:00 [migration/0]

对以上信息解释:

  • user : 用户名
  • pid : 进程id
  • %CPU : 进程运行占用CPU的百分比
  • %MEM : 进程占用内存的百分比
  • VSZ : 该进程使用的虚拟内存量,表示一个程序完全驻留在内存中占用的虚拟内存量
  • RSS : 该进程占用的固定内存量
  • TTY : 终端名
  • START : 进程开启时间
  • STAT : 进程的状态
  • TIME : 该进程实际使用的CPU时间
  • COMMAND : 进程运行指令

上述比较重要的信息主要包括VSZ,RSS以及STAT。
VSZ表示一个程序完全驻留在内存中占用的内存量,其包括程序链接的动态链接库的大小,栈的大小以及代码段的大小。(手动算过,但是栈的大小很迷,貌似数组在栈中的大小与同类型变量的大小相同,待验证)。
RSS包括运行过程中实际被加载到内存中的动态链接库,可执行文件以及栈的大小。
STAT为当前进程的运行状态。其包括多种状态,具体解释如下所述:

  • D : 不可中断(通常是IO进程)
  • R : 正在运行或者在队列中的进程
  • S : 处在休眠状态的进程
  • T : 停止或者被追踪
  • Z : 僵尸进程
  • W : 进入内存交换
  • X : 死掉的进程
  • < : 优先级较高的进程
  • n : 优先级较低的进程
  • s : 包含子进程
  • + : 位于后台进程组

可以在上述命令的基础上对查询结果进行筛选,如我们想要对内存占用量进行排序,方法如下:

1
ps aux --sort -pMEM

Comment and share

makefile自动变量与隐晦规则推导

makefile的使用可以大大简化程序编译的过程,不过对于新手来说makefile的执行规则理解起来还是很让人迷糊的。其中最重要的原因是makefile中使用了大量的隐晦规则和自动变量来简化makefile的编写.本节将记录一下makefile自动变量和隐晦规则的推导过程以及makefile的执行流程。

1
2
%.o:%.c
$(CC) -c $(CFLAGS) -o $@ $<

对于上面的推导规则,makefile是怎么将%c文件编译汇编成.o文件的?下面将针对这个问题进行讲解

自动变量

makefile中的自动变量实质上是对一类变量的简写,当我们在模式规则中对这类变量处理的时候可以直接使用自动变量简化makefile代码的编写。自动变量包括如目标文件,依赖文件等。下面以实例的方式列出了一些常用的自动变量:

1
2
a: a.o b.o c.o d.o
g++ -c $(CFLAGS) -o $@ $<
  • $@: 表示模式规则中的目标文件,对于上面的模式规则,$@表示a
  • $<: 表示依赖中的第一个文件.对于上述规则,$<表示a.o
  • $^: 表示所有依赖文件的集合,对于上述规则$^表示a.o b.o c.o d.o
  • $+: 表示所有依赖文件的集合(不去重).对于上述规则$+表示a.o b.o c.o d.o
  • $%: 仅当目标是函数库文件(.a)文件时,表示规则中目标成员名。如一个目标是(test.a(a.o)),此时$%表示a.o, $@表示test.a
  • $?: 所有比目标新的依赖目标的集合
  • $*: 表示目标规则中%以及%之前的部分。如若目标文件为”src/test.o”,目标文件模式为”src/%.o”,此时$*表示”src/test”。

隐晦规则自动规则推导

使用makefile的makefile的隐晦自动规则推导功能也可以让我们的makefile的代码大大简化。使用隐晦规则,我们没必要为每一个类似的规则生成都去写类似的规则。makefile会自动推导依赖文件,并根据隐含规则推导出生成当前目标的命令。
如下面的makefile:

1
2
3
4
5
6
7
8
target: a.o b.o c.o
g++ -o $@ $^ $(LIB) $(INC) $(LINKER)
a.o: a.c
g++ -c $(CFLAGS) -o a.o a.c
b.o: b.c
g++ -c $(CFLAGS) -o b.o b.c
c.o: c.c
g++ -c $(CFLAGS) -o c.o c.c

上述规则没有使用隐晦规则,对于每一个.o文件的生成都写了一条规则语句.若使用隐晦规则推导,上述makefile可写为如下:

1
2
3
4
5
6
7
SOURCE := $(shell find ./ -type f -name *.c)
OBJECTS := $(patsubst *.c,*.o,$(SOURCE))
target : $(OBJECTS)
g++ -o $@ $^ $(LIB) $(INC) $(LINKER)
%.o:%.c
$(CC) -c $(CFLAGS) -o $@ $<

从上面的隐晦规则可以看出,对于.o文件的生成命令,makefile都可以由隐晦规则” $(CC) -c $(CFLAGS) -o $@ $<”推导出。即如当目标需要a.隐晦规则将推导出命令”g++ -c $(CFLAGS) -o a.o a.c”用于生成该目标.

makefile执行过程

由上一节中的makefile简单说一下makefile的执行规则。

1
2
3
4
5
6
7
SOURCE := $(shell find ./ -type f -name *.c)
OBJECTS := $(patsubst *.c,*.o,$(SOURCE))
target : $(OBJECTS)
g++ -o $@ $^ $(LIB) $(INC) $(LINKER)
%.o:%.c
$(CC) -c $(CFLAGS) -o $@ $<

makefile中首先声明了变量SOURCE和OBJECTS,SOURCE是当前文件夹下的所有.c文件的集合,OBJECTS是所有.c文件对应的目标文件.o的集合.

执行过程:
首先,目标target依赖所有的目标文件.o,即a.o,b.o,c.o。当需要依赖a.o时,makefile会根据隐晦规则自动推导出生成a.o文件的命令,(“g++ -c $(CFLAGS) -o a.o a.c”),生成a.o;类似的也会根据同样的过程生成b.o和c.o文件,这三个文件生成之后,再根据上述规则生成target。

makefile中常用函数

wildcard函数

函数参数:一个正则表达式
函数功能:wildcard的中文意思是通配符,它的功能类似于正则表达式,用于展开一列所有符合其参数描述的文件名,文件之间用空格分割。

实例:

1
SOURCE=$(wildcard *.cpp)

此时SOURCE的值为所有的以.cpp为后缀的文件集合,以空格隔开。

patsubst函数

函数参数: 第一个是需要匹配的样式,第二个是表示用什么替换它,第三个被处理的以空格隔开的字符串。
函数功能:其功能是一个匹配替换的函数(pattern substitute)。

实例:

1
2
#TARGET表示与SOURCE中同名的目标文件.
TARGET = $(patsubst *.cpp, *.o, $(SOURCE))

subst函数

1
$(subst <from>,<to>,<text>)

功能:字符串替换,将text中的from 替换为to
实例:

1
2
#返回值:this is b
$(subst a, b, this is a)

strip函数

1
$(strip <string>)

功能:去除头部和尾部的空格
实例:

1
2
#源字符串为"a b c ",返回值为"a b c"
$(strip a b c )

filter函数

1
$(filter <pattern...>,<text...>)

功能:过滤特定模式的字符串
实例:

1
2
#如source为a.c b.s c.h,则下列的返回值为a.c b.s
$(filter *.c *.s, $(source))

word函数

1
$(word <n>,<text>)

函数功能:取单词函数。取出中的第n个单词(从1开始),若越界,返回为空

实例:

1
2
#此函数返回值为bar
$(word 2,foo bar fun)

words函数

1
$(words <text>)

功能:统计字符串中单词的个数,返回个数
实例:

1
2
#此函数返回值为3
$(word foo bar fun)

wordlist函数

1
$(wordlist <s>,<e>,<text>)

功能:取中s-e个单词
实例:

1
2
#返回bar fun
$(wordlist 2,3, foo bar fun)

firstword函数

1
$(firstword <text>)

函数功能:取中的首个单词
实例:

1
2
#返回值为foo
$(firstword foo bar fun)

dir函数

1
$(dir ...)

功能:取给定文件名序列中的目录(即/前面的部分)。如没有/,则返回./
实例:

1
2
#返回值为/home/ ./
$(dir /home/test testfile)

notdir函数

1
$(notdir ...)

功能:取给定文件名序列中的取出非目录部分(即/后面的部分)。
实例:

1
2
#返回值为test testfile
$(notdir /home/test testfile)

suffix函数

1
$(suffix ..)

功能:取后缀函数,若没有后缀返回为空
实例:

1
2
#返回值:.c .c
$(suffix a.c b.c)

basename函数

1
$(basename ...)

功能:取前缀函数,包括目录。
实例:

1
2
#返回值:/home/test a
$(basename /home/test.cpp a.cpp)

addsuffix函数

1
$(addsuffix <suffix> <name....>)

功能:给指定文件序列添加后缀名
实例:

1
2
#返回值:a.c b.c c.c
$(addsuffix .c a b c)

addprefix

1
$(addprefix <prefix> <name...>)

功能:给指定文件序列添加前缀
实例:

1
2
#返回值:src/a src/b src/c
$(addprefix src/ a b c)

join函数

1
$(join <list1> <list2>)

功能:将两个字符串中的list对应项连接
实例:

1
2
3
4
#返回值:an 2b 3
$(join 1 2 3, a b)
#返回值:1a 2b c
$(join 1 2, a b c)

makefile中链接静态库顺序问题

在链接静态库的时候,如果多个静态库之间存在依赖关系,则有依赖的静态库之间存在顺序问题,若顺序出现错误,则可能出现函数未定义或符号找不到等错误。

静态库链接的顺序的原则是:被依赖的库一定要放在后面,因为makefile在链接静态库时的顺序是从右往左(或从后向前).如libb.a依赖于liba.a,此时的链接顺序应该是:-Llibb.a -Lliba.a。

会出现上述问题的原因是:我们在生成静态库的时候并未把依赖库的定义编到生成的库中。如

1
2
3
4
gcc -c a.c
ar cr liba.a a.o
gcc -c b.c
ar cr libb.a b.o # 虽然libb.a使用到了liba.o中的一些函数,但并不会将它们的定义包含进来,所以在链接test时需要指定这两个库

Comment and share

System V IPC 机制和key_t本质类型

System V三种IPC机制

System V IPC包括三种不同的通信机制

  • 消息队列:消息队列类似于管道,但是又有很大的差别。第一,消息队列是有边界的,所以消息队列的通信机制是通过消息进行传递,而管道是通过字节流进行通信。第二,每条消息包括一个完整的整形字段,消息队列可以通过类型来选择消息。
  • 信号量:用于同步两个进程。
  • 共享内存:共享内存允许两个进程共享同一个内存段。即一块内存被映射到不同进程的虚拟内存中。

key_t本质

System V系统调用都有一个相关的get系统调用,它与文件io中的open函数类似,不同的是它的参数是一个整数,给定一个整数之后,系统会做以下操作:

  • 使用给定key创建一个新的IPC对象,并将IPC对象标识key_t返回给调用者.
  • 若该key对应的IPC对象存在,直接将标识返回给调用者,相当于使用key换取标识,而不做创建操作.

上述IPC对象的标识类型为key_t。key_t的本质是什么?我们可以通过grep命令追踪到

在文件/usr/include/sys/ipc.h中:

1
2
3
4
#ifndef __key_t_defined
typedef __key_t key_t;
# define __key_t_defined
#endif

在文件/usr/include/bits/types.h中:

1
__STD_TYPE __KEY_T_TYPE __key_t; /* Type of an IPC key. */

在文件/usr/include/bits/typesizes.h中:

1
#define __KEY_T_TYPE __S32_TYPE

在文件中:

1
#define __S32_TYPE int

故从上面的追踪,可以看出key_t的本质为int类型.

获取key_t方法

获取ket_t的方法三种

  • 随机选取一个整数值作为key值,多个进程共享一个key值。注意此时不能使用已经存在的key
  • 在创建IPC对象的get方法中用IPC_PRIVATE作为key值,这样会导致每个调用都会创建一个新的IPC对象
  • 使用ftok函数生成一个key,这个方法产生的key接近唯一。

IPC_PRIVATE

IPC_PRIVATE本质上为0的宏定义.使用方法是直接使用IPC_PRIVATE作为key生成一个IPC对象。这种做法保证每次产生的都是全新的IPC对象。
这种做法适用于在父进程fork前创建IPC对象,从而达到子进程继承IPC对象的目的。

ftok生成key

函数原型:

1
2
#include <sys/ipc.h>
key_t ftok(char* pathname, int proj);

函数功能:

此函数的功能是根据pathname和proj的值生成key,该算法只使用proj的最低的8个有效位,应用程序必须确保pathname引用一个可以应用stat()的既有文件。否则该函数会返回-1。

在linux中,key_t一般是32位的值。它通过取proj参数的最低8个有效位,包含该文件所属的文件系统的设备的设备号的最低8个有效位以及pathname所引用文件的inode号的最低16个有效位组合而成,这样保证了唯一性。所以从上面可以看出,对于同意个文件的不同路径(inode值相同),proj值相同,此时产生的key是相同的。 

Comment and share

魏传柳(2824759538@qq.com)

author.bio


Tencent


ShenZhen,China