tmux:终端复用利器
什么是终端复用(terminal multiplexer)?终端复用可以让你在同一个终端同时打开不同的程序并观察输出,同时允许你方便地退出和恢复这些程序的使用。
使用tmux可以达到终端复用的目的,下图为tmux在同一个终端窗口中同时打开不同程序的一个示例:
安装
对于 Mac OS,使用简单的 brew
命令即可完成tmux安装:
brew install tmux
Mac下如何安装的详细教程可以参考这个链接:Mac安装tmux教程。
什么是终端复用(terminal multiplexer)?终端复用可以让你在同一个终端同时打开不同的程序并观察输出,同时允许你方便地退出和恢复这些程序的使用。
使用tmux可以达到终端复用的目的,下图为tmux在同一个终端窗口中同时打开不同程序的一个示例:
对于 Mac OS,使用简单的 brew
命令即可完成tmux安装:
brew install tmux
Mac下如何安装的详细教程可以参考这个链接:Mac安装tmux教程。
本来对于多说不支持 HTTPS 就不太满意,早就想换一个评论系统了。昨天上去多说的官网一看,发现多说竟然要在今年6月1号关闭了。
可能是多说没有找到赚钱的路子吧,这样的话自己只能默默祝福一下了。
多说的关闭也坚定了自己将博客的评论跟换成网易跟贴的决心。
说干就干。
在编写c/c++测试程序时,我们习惯每次修改一处代码,然后就马上编译运行来查看运行的结果。这种编译方式对于小程序来说是没有多大问题的,可对于大型程序来说,由于包含了大量的源文件,如果每次改动一个地方都需要编译所有的源文件,这个简单的直接编译所有源文件方式对程序员来说简直是噩耗。
我们看一个例子:
1 | // main.c |
如果程序员只修改了头文件c.h
,则源文件main.c
和2.c
都无需编译,因为它们不依赖这个头文件。而对3.c
来说,由于它包含了c.h
,所以在头文件c.h
改动后,就必须得新编译。
而如果改动了b.h
可是忘记编译了2.c
,那么最终的程序就可能无法正常工作。
make 工具就是为了解决上述问题而出现的,它会在必要时重新编译所有受改动影响的源文件。
epoll也是实现I/O多路复用的一种方法,为了深入了解epoll的原理,我们先来看下epoll水平触发(level trigger,LT,LT为epoll的默认工作模式)与边缘触发(edge trigger,ET)两种工作模式。
使用脉冲信号来解释LT和ET可能更加贴切。Level是指信号只需要处于水平,就一直会触发;而edge则是指信号为上升沿或者下降沿时触发。说得还有点玄乎,我们以生活中的一个例子来类比LT和ET是如何确定读操作是否就绪的。
水平触发
儿子:妈妈,我收到了500元的压岁钱。
妈妈:嗯,省着点花。
儿子:妈妈,我今天花了200元买了个变形金刚。
妈妈:以后不要乱花钱。
儿子:妈妈,我今天买了好多好吃的,还剩下100元。
妈妈:用完了这些钱,我可不会再给你钱了。
儿子:妈妈,那100元我没花,我攒起来了
妈妈:这才是明智的做法!
儿子:妈妈,那100元我还没花,我还有钱的。
妈妈:嗯,继续保持。
儿子:妈妈,我还有100元钱。
妈妈:…
接下来的情形就是没完没了了:只要儿子一直有钱,他就一直会向他的妈妈汇报。LT模式下,只要内核缓冲区中还有未读数据,就会一直返回描述符的就绪状态,即不断地唤醒应用进程。在上面的例子中,儿子是缓冲区,钱是数据,妈妈则是应用进程了解儿子的压岁钱状况(读操作)。
poll
函数类似于select
函数,也可以实现I/O多路复用。poll
函数的声明如下:
1 | #include <poll.h> |
第一个参数是指向一个结构数组第一个元素的指针。每个数组元素都是一个pollfd
结构,用于指定测试某个给定描述符fd
的条件。
1 | struct pollfd { |
要测试的条件由events
成员指定,poll
函数在相应的revents
成员中返回该描述符的状态。events
和revents
都由某个特定条件的一位或多位构成。下面表格列出了用于指定events
标志以及测试revents
标志的一些常值。
I/O多路复用模型允许我们同时等待多个套接字描述符是否就绪。Linux系统为实现I/O多路复用提供的最常见的一个函数是select
函数,该函数允许进程指示内核等待多个事件中的任何一个发生,并只有在一个或多个事件发生或经历一段指定的时间后才唤醒它。
作为一个例子,我们可以调用select
,告知内核仅在下列情况发生时才返回:
也就是说,我们调用select
可以告知内核我们对哪些描述符感兴趣以及等待多久时间。select
是一个复杂的函数,有许多不同的应用场景,我们将只讨论第一种场景:等待一组描述符准备好读。
1 | #include <unistd.h> |
在多进程并发服务器的应用程序中,父进程accept
一个连接,fork
一个子进程,该子进程负责处理与该连接对端的客户之间的通信。
尽管多进程的编程模型中,各进程拥有独立的地址空间,减少了出错的概率,然而,fork
调用却存在一些问题:
fork
是昂贵的,fork
要把父进程的内存映像复制到子进程,并在子进程中复制所有描述符,这个操作是较重量级的。fork
返回之后父子进程之间信息的传递需要进程间通信(IPC)机制。线程则可以解决上述两个问题。线程有时也称为轻量级的进程,线程的创建可能比进程的创建快10-100倍。同一个进程内所有线程共享相同的全局内存,这使得线程之间易于共享信息,但伴随这种简易性而来的是线程安全问题。
我们介绍的第一个线程的函数是pthread_create
,它的作用是创建一个新线程。它的定义如下:
1 | #include <pthread.h> |
这个函数的定义看起来很复杂,其实用起来很简单。
第一个参数是指向pthread_t
类型的指针。线程被创建时,这个指针指向的变量将被写入一个标识符(线程ID)我们用该标识符来引用新线程。
第二个参数用于设置线程的属性,一般不需要特殊的属性,所以只需要设置该参数为NULL。
最后两个参数,分别告诉新线程将要启动执行的函数和传递给该函数的参数。pthread_create
函数在成功调用时返回0,如果失败则返回失败码。
我们来考虑有多个客户同时连接一个服务器的情况。在前面的TCP套接字编程的例子中,我们已经看到,服务器程序在接受来自客户端的一个新连接时,会创建出一个新的套接字(已连接套接字),而原先的监听套接字则继续监听后面的连接请求。如果服务器不能立刻接受后来的连接,它们将被放到队列中以等待处理。
原先的套接字仍然可用并且套接字的行为就像文件描述符,这一事实给我们提供了一种同时服务多个客户的方法。如果服务器调用fork
为自己创建第二份副本,打开的套接字就将被新的子进程所继承。新的子进程可以和连接的客户进行通信,而主服务器进程可以继续接受以后的客户连接。
为了了解这是如何工作的,假设我们有两个客户端和一个服务器,服务器正在监听一个监听套接字(比如描述符3)上的连接请求。
现在假设服务器接受了客户端1的连接请求,并返回一个已连接套接字(比如描述符4),如图1所示。
图1:第一步:服务器接受客户端的连接请求
监听套接字(listening socket)和已连接套接字(connected socket)之间的区别常会使很多人感到迷惑。本文简要描述一下这两者的区别。
为了说明监听套接字与已连接套接字的区别,我们先来看一下套接字在连接中的含义。
从内核的角度来看,一个套接字就是通信的一个端点。一个连接由它两端的套接了地址唯一确定,这对套接字地址叫做套接字对(socket pair),由下列4元组来表示:
(clientip:clientport, serverip:serverport)
其中,clientip 是客户端的IP地址,clientport 是客户端的端口,serverip 是服务器的IP地址,而 serverport 是服务器的端口。
图1:套接字对4元组示意图
上图1展示了一个套接字对4元组,即一个客户端与一个服务器之间的连接。在这个示例中,客户端套接字为