0%

出发前准备

签证:飞猪上有,我出发前一周多开始办理,出发前一天签证到手(非常极端)。
上网流量:可以去国内机场租一个wifi;联通也可以开通国际漫游,25一天流量不封顶(使用国际漫游一定要有稳定的梯子,不然没法上google)。最好别去日本机场买临时电话卡,非常贵,而且流量少。
现金:最少也要换一点钱能去机场买交通卡,另外要备一点,防止只能用现金的情况。日本很多便利店里有取款机,能用银联卡取钱,注意双侧银行的手续费。711的ATM取现不收当地手续费,推荐711。我个人感觉使用现金频繁程度为中等。
语言:使用简单英语沟通,另外下载一个VoiceTra(日本官方出的)/彩云小译,可以翻译日语。

交通

机场可以买西瓜卡(suica),可以在日本大部分主要城市使用。出境前可以在机场退卡,220日元。

如果有东京市内密集游览几天的计划,可以买一张地铁通票,在机场游客中心买,包含地铁9条线和都营4条线。

如果去东京周边市区,也有JR PASS通票可以在数天内坐JR的电车,新干线,巴士,轮船。

交通路线查询app推荐使用 google地图/苹果地图 为主, 换乘案内 为辅。进错站同出口出一半不收费,其他出口出就要收费了。google地图很好用。
列举东京铁路交通的一些坑:

  • 东京市内有两个地铁公司运营,站内换乘不同公司的地铁需要出闸机进另一个公司的闸机。另外等车的时候也要注意站台对不对。有的站比较大,站台很多。
  • 同一条线路的车子也分普通和特急,特急有的站不停。
  • 有的车开着开着就换名字了。当导航显示”无需换乘“时,只要你没坐错车,就不需要下车。

我啥周游券/通票都没买,在东京7天交通大概花了1000人民币的样子。

住宿

可以用booking订住宿。胶囊酒店真的便宜!200一晚上。正经酒店还挺贵的,我没怎么住。
浅草 MyCUBE by MYSTAYS Asakusa Kuramae:
A0A9B316-BE4A-4180-A558-CE1FCC78215E.jpg

买东西

银座的东西很贵,可以去新宿或者池袋购物。

药妆店推荐大国药妆。推荐一个app,日本购物扫一扫。几乎所有大商场或者药妆店都有优惠券,支付宝、大众点评、官方微信公众号上找找看。

有的店消费满5000可以凭护照免税。免税商品在日本境内不能拆,但是拆了其实日本海关也不知道(亲测,因为过海关的时候行李箱已经托运了)。

如果觉得语言不通很为难的话,直接去大众点评搜高分餐厅。这类高分餐厅要不有很详细的中文提示,要不店员会说中文。而且中国人顾客非常多。
东京的米其林餐厅特别多,想吃的话一定要提前预定
便利店其实完全可以解决你的三餐,吃个7天餐餐不重样没啥问题。

行程

东边

我在东京市内所有时间都住在浅草周围,挺方便的。

浅草寺

不要门票的哈,想去就去。避开人流高峰期,晚上也能去拍照。浅草寺的签有人说准有人说不准,看你自己喜好吧。御守也不贵。
周边住的地方很多,晚上还有烧烤摊一条街。就是一个人去吃有点惨。
浅草寺后面有个朱印所,有收集癖的人可以去买一本。
浅草寺:
40063648-67B3-4A7B-A997-0EBAFDDF2C4C.jpg
在浅草寺旁边碰到的卡丁车队:
B46452F9-3818-4144-A0DD-34C98C59E314.jpg

晴空塔

离浅草非常近,但我没去嘿嘿

秋叶原

我在秋叶原总共逛了两天。出了秋叶原地铁站就是Yodobashi Camera,10多层,我在这里就逛了一天。后来才发现,地铁站西北边这条街才是真正的天堂。在这里又逛了一天。
有几个要注意的:

  1. 别急着掏钱,特别是中古店(二手店),多逛逛你心里才有数一个东西值多少钱。下图这个手办我一共看到过4次,最贵的6000多日元,最便宜的3000多。
    50C41783-9752-4349-AEF9-3379D9936E9C.jpg
  2. 秋叶原的电器并不便宜。记得算一下。千万别忘了免税。
  3. 时间越晚,女仆咖啡厅在街上拉客的小姐姐越好看。

上野公园

春天去可能会很好看。我10月去的,公园本身没啥意思。有意思的是上野公园在开瓷器博览会,买了好多瓷器(给我打了40日元的折……)。
上野站:铁路爱好者一定要去看看
6D5317A1-012A-4A52-A38E-1C5ED6FBBE30.jpg
瓷器博览会:
A2F5F880-BFFF-4C06-B5AA-4F6079896D13.jpg

中央地带

皇居

血的教训,提前预约!不预约参观不了。特别是节假期,淘宝上都买不到。

银座

银座有八条街,一丁目和四丁目。没钱人可能很难体会到银座的繁华。我觉得挺一般的。但银座的米其林餐馆很多,吃得起的一定记得提前订座。
筑地市场就在银座周边,小吃很多。
对了,在这里你真的能看到日剧里才有的穿礼服的夜总会迎宾小姐。

东京塔与六本木

与其上东京塔,不如去六本木的森塔吧。在逛完森美术馆后还能俯拍东京塔。
森美术馆没有固定馆藏,看到就是赚到。
盐田千春展览:灵魂动摇:
38041B8B-0AF9-4775-97DC-511FF6B9CD69.jpg
东京塔:
DBF88582-E13F-48EA-B1B0-B4182A9F06EF.jpg

西边

明治神宫

没门票的。入口的鸟居是日本最大的木质鸟居,同样有朱印。
用净盆洗手漱口:先右手,再左手,再漱口
E7076F68-96E7-4E96-B6E8-BF696CCA1361.jpg

新宿,歌舞伎町一番街

歌舞伎町是全亚洲最著名的红灯区,随便看看就好。也有药妆店啥的。
新宿是东京最繁华的商业街之一,买买买。
找不到图了。

镰仓

镰仓高校前站为灌篮高手取景地。逛的时候主要是乘坐江之电。基本上一天就玩完了。但是由于有的景点关门比较早,所以时间还是比较紧的。
路线:江之岛 → 湘南海岸(有很舒服的沙滩,有很多人冲浪,还有流川枫上学的路。缺点是太长了得走很远,也没车能坐) → 镰仓高校前站(必去) → 极乐寺站 → 长谷寺 → 镰仓小町通(逛街吃饭的好地方)
湘南海岸:
8A3F90D3-D803-4B53-8A46-4C69BC21D0EE.jpg
镰仓高校前站:
59519342-32A2-4B9F-8D87-E594F7111246.jpg
镰仓小町通:
A169B7C7-99C0-4A5B-ABFA-4BBE51F2D29D.jpg

交通:
1.起的早的话,可以购买小田急电铁江之岛/镰仓周游券(售票处在小田急百货的一层),大概1500日元,包含从东京往返一次藤泽、藤泽-镰仓的江之电线路无限次乘坐。缺点是在镰仓玩完后好像还要回藤泽坐车回东京。所以一定要起得早。
2.按地图从东京坐车到藤泽,购买江之电通票(700日元左右),无限次乘坐江之电。一路玩到镰仓,晚上再从镰仓坐车回东京。
我是走的第二条路线,因为我11点才起床。极乐寺长谷寺都关门了,到镰仓小町通的时候也有很多店关门了。

富士山

富士山一年只有7-9月份不封山,封山时最多到五合目。所以主要就是在河口湖周边玩耍。玩一天两天三天都行。河口湖周边有很多逛得地方,离河口湖远一点有箱根。
嫌麻烦的话可以在马蜂窝直接报一个富士山旅游团。虽然玩的地方不多,但是会节省很多交通时间。

交通:

  • 建议乘坐富士急行巴士。新宿站发车,2000日元一趟,2小时车程。富士急行巴士willer都能预定。好像并不能现场买。
  • 小田急有富士-箱根周游券。
  • 河口湖周边有红线巴士(red line)、绿线巴士(green line),基本能到环河口湖所有景点。我印象里只收现金。也可以在河口湖站买周游券,包含巴士+缆车+游船
    绿线巴士:
    2F0BA0EB-3210-4CA1-9B76-E4F50EDCD4DC.jpg
    绿线通过的浅间神社,也是我住的地方附近:
    B6EF680C-29FF-4DEC-8246-2FB2B623A5A1.jpg
  • 河口湖站有轨道交通,可以到远一点的地方,比如浅间公园。

住:一般是住河口湖南岸或者直接住河口湖站附近。北岸也有湖畔温泉旅店。民宿的话记得头一天早点去,我住的民宿8点半老板就下班了,我10点半到富士山,差点没地方住。
温泉旅馆集中在东南湖岸,死贵。

路线:
河口湖周边:
河口湖游览船(在湖中间可以看到富士山全景)
BC9758E7-A449-4CAD-BEF8-DB578409E99E.jpg
天上山公园(建议坐缆车上去,不然累死,在买游览船船票的地方有船+缆车通票可以买,河口湖站也有周游券买)
67895539-AC2B-4170-8581-AF31AA615272.jpg
浅间公园:你很可能见过从浅间公园拍富士山的照片。别搞错了,是浅见公园,不是浅间神社。
F6C476A5-1B59-4116-A327-6FDE3ECB5232.jpg
河口湖美术馆、自然生活馆、音乐森林、根场(一个日式村落)等看自己喜好

富士山五合目:
封山时期最多到这个位置,也是大巴车能到的位置。大概是半山腰的位置。从富士急行河口湖站乘坐登山巴士50分钟即到。

箱根神社:泡温泉也可以去这里。太远了我没去过。每天只有几班大巴从河口湖去箱根。

其他景点

  • 迪士尼、海洋乐园。一个人去有点惨。
  • 台场。有一个特别大的高达模型。
  • 何须神社:你的名字取景地。
  • 横滨。养老城市。

参考文章:
进程的挂起、阻塞和睡眠
Linux IO模式及 select、poll、epoll详解
epoll(7) — Linux manual page

基础知识

阻塞,挂起,睡眠

  • 挂起:一般是主动的,由系统或程序发出,甚至于辅存中去。(不释放CPU,可能释放内存,放在外存),挂起倾向于换出阻塞态的进程,也可以是就绪态的进程。
  • 阻塞:一般是被动的,在抢占资源中得不到资源,被动的挂起在内存,等待某种资源或信号量(即有了资源)将他唤醒。(释放CPU,不释放内存)
  • 睡眠:可以认为是一种规定了阻塞时间的阻塞。或者说阻塞这种状态本身,就包含了线程在等待过程中的所有策略——其中就包括睡眠。
  • 阻塞(pend)就是任务释放CPU,其他任务可以运行,一般在等待某种资源或信号量的时候出现。挂起(suspend)不释放CPU,如果任务优先级高就永远轮不到其他任务运行。

挂起是一种主动行为,因此恢复也应该要主动完成,而阻塞则是一种被动行为,是在等待事件或资源时任务的表现,你不知道他什么时候被阻塞(pend),也就不能确切的知道他什么时候恢复阻塞。而且挂起队列在操作系统里可以看成一个,而阻塞队列则是不同的事件或资源(如信号量)就有自己的队列。
阻塞和挂起并不是独立的,如图。
v2-900c66934bd3bdf8a5c681e73b62de0d_1440w.webp

文件描述符file descriptor(fd)

文件描述符(File descriptor)是计算机科学中的一个术语,是一个用于表述指向文件的引用的抽象化概念。
文件描述符在形式上是一个非负整数。实际上,它是一个索引值,指向内核为每一个进程所维护的该进程打开文件的记录表。当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符。在程序设计中,一些涉及底层的程序编写往往会围绕着文件描述符展开。但是文件描述符这一概念往往只适用于UNIX、Linux这样的操作系统。
可以通过cat /proc/sys/fs/file-max查看操作系统自持的最大能打开的fd数。

缓存IO

缓存 I/O 又被称作标准 I/O,大多数文件系统的默认 I/O 操作都是缓存 I/O。在 Linux 的缓存 I/O 机制中,操作系统会将 I/O 的数据缓存在文件系统的页缓存( page cache )中,也就是说,数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。

network IO

对于一次IO访问(以read举例),数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。所以说,当一个read操作发生时,它会经历两个阶段:

  1. 等待数据准备 (Waiting for the data to be ready)
  2. 将数据从内核拷贝到进程中 (Copying the data from the kernel to the process)

正式因为这两个阶段,linux系统产生了下面五种网络模式的方案。

  • 阻塞 I/O(blocking IO)
  • 非阻塞 I/O(nonblocking IO)
  • I/O 多路复用( IO multiplexing)
  • 信号驱动 I/O( signal driven IO)
  • 异步 I/O(asynchronous IO)
    由于signal driven IO在实际中并不常用,所以只提及剩下的四种IO Model。

阻塞 I/O(blocking IO)

在linux中,默认情况下所有的socket都是blocking,一个典型的读操作流程大概是这样:
720333-20160916171008617-1558216223.png
当用户进程调用了recvfrom这个系统调用,kernel就开始了IO的第一个阶段:准备数据(对于网络IO来说,很多时候数据在一开始还没有到达。比如,还没有收到一个完整的UDP包。这个时候kernel就要等待足够的数据到来)。这个过程需要等待,也就是说数据被拷贝到操作系统内核的缓冲区中是需要一个过程的。而在用户进程这边,整个进程会被阻塞(当然,是进程自己选择的阻塞)。当kernel一直等到数据准备好了,它就会将数据从kernel中拷贝到用户内存,然后kernel返回结果,用户进程才解除block的状态,重新运行起来。
所以,blocking IO的特点就是在IO执行的两个阶段都被block了。

非阻塞 I/O(nonblocking IO)

linux下,可以通过设置socket使其变为non-blocking。当对一个non-blocking socket执行读操作时,流程是这个样子:
720333-20160916171226852-1916489268.png
当用户进程发出read操作时,如果kernel中的数据还没有准备好,那么它并不会block用户进程,而是立刻返回一个error。从用户进程角度讲 ,它发起一个read操作后,并不需要等待,而是马上就得到了一个结果。用户进程判断结果是一个error时,它就知道数据还没有准备好,于是它可以再次发送read操作。一旦kernel中的数据准备好了,并且又再次收到了用户进程的system call,那么它马上就将数据拷贝到了用户内存,然后返回。
所以,nonblocking IO的特点是用户进程需要不断的主动询问kernel数据好了没有。

I/O 多路复用( IO multiplexing)

IO multiplexing就是我们说的select,poll,epoll,有些地方也称这种IO方式为event driven IO。select/epoll的好处就在于单个process就可以同时处理多个网络连接的IO。它的基本原理就是select,poll,epoll这个function会不断的轮询所负责的所有socket,当某个socket有数据到达了,就通知用户进程。
720333-20160916171333523-650292614.png
当用户进程调用了select,那么整个进程会被block,而同时,kernel会“监视”所有select负责的socket,当任何一个socket中的数据准备好了,select就会返回。这个时候用户进程再调用read操作,将数据从kernel拷贝到用户进程。
所以,I/O 多路复用的特点是通过一种机制一个进程能同时等待多个文件描述符,而这些文件描述符(套接字描述符)其中的任意一个进入读就绪状态,select()函数就可以返回。
这个图和blocking IO的图其实并没有太大的不同,事实上,还更差一些。因为这里需要使用两个system call (select 和 recvfrom),而blocking IO只调用了一个system call (recvfrom)。但是,用select的优势在于它可以同时处理多个connection。
所以,如果处理的连接数不是很高的话,使用select/epoll的web server不一定比使用multi-threading + blocking IO的web server性能更好,可能延迟还更大。select/epoll的优势并不是对于单个连接能处理得更快,而是在于能处理更多的连接。)
在IO multiplexing Model中,实际中,对于每一个socket,一般都设置成为non-blocking,但是,如上图所示,整个用户的process其实是一直被block的。只不过process是被select这个函数block,而不是被socket IO给block。

异步 I/O(asynchronous IO)

linux下的asynchronous IO其实用得很少。先看一下它的流程:
720333-20160916171458461-2052304822.png
用户进程发起read操作之后,立刻就可以开始去做其它的事。而另一方面,从kernel的角度,当它受到一个asynchronous read之后,首先它会立刻返回,所以不会对用户进程产生任何block。然后,kernel会等待数据准备完成,然后将数据拷贝到用户内存,当这一切都完成之后,kernel会给用户进程发送一个signal,告诉它read操作完成了。

总结

blocking和non-blocking的区别

调用blocking IO会一直block住对应的进程直到操作完成,而non-blocking IO在kernel还准备数据的情况下会立刻返回。

synchronous IO和asynchronous IO的区别

在说明synchronous IO和asynchronous IO的区别之前,需要先给出两者的定义。POSIX的定义是这样子的:

  • A synchronous I/O operation causes the requesting process to be blocked until that I/O operation completes;
  • An asynchronous I/O operation does not cause the requesting process to be blocked;

两者的区别就在于synchronous IO做”IO operation”的时候会将process阻塞。按照这个定义,之前所述的blocking IO,non-blocking IO,IO multiplexing都属于synchronous IO。
有人会说,non-blocking IO并没有被block啊。这里有个非常“狡猾”的地方,定义中所指的”IO operation”是指真实的IO操作,就是例子中的recvfrom这个system call。non-blocking IO在执行recvfrom这个system call的时候,如果kernel的数据没有准备好,这时候不会block进程。但是,当kernel中数据准备好的时候,recvfrom会将数据从kernel拷贝到用户内存中,这个时候进程是被block了,在这段时间内,进程是被block的。
而asynchronous IO则不一样,当进程发起IO 操作之后,就直接返回再也不理睬了,直到kernel发送一个信号,告诉进程说IO完成。在这整个过程中,进程完全没有被block。

各个IO Model的比较如图所示:
720333-20160916171648430-240094129.png
通过上面的图片,可以发现non-blocking IO和asynchronous IO的区别还是很明显的。在non-blocking IO中,虽然进程大部分时间都不会被block,但是它仍然要求进程去主动的check,并且当数据准备完成以后,也需要进程主动的再次调用recvfrom来将数据拷贝到用户内存。而asynchronous IO则完全不同。它就像是用户进程将整个IO操作交给了他人(kernel)完成,然后他人做完后发信号通知。在此期间,用户进程不需要去检查IO操作的状态,也不需要主动的去拷贝数据。

select、poll、epoll

IO多路复用基本概念:
IO多路复用是指内核一旦发现进程指定的一个或者多个IO条件准备读取,它就通知该进程。IO多路复用适用如下场合:

  1. 当客户处理多个描述字时(一般是交互式输入和网络套接口),必须使用I/O复用。
  2. 当一个客户同时处理多个套接口时,而这种情况是可能的,但很少出现。
  3. 如果一个TCP服务器既要处理监听套接口,又要处理已连接套接口,一般也要用到I/O复用。
  4. 如果一个服务器即要处理TCP,又要处理UDP,一般要使用I/O复用。
  5. 如果一个服务器要处理多个服务或多个协议,一般要使用I/O复用。

 
与多进程和多线程技术相比,I/O多路复用技术的最大优势是系统开销小,系统不必创建进程/线程,也不必维护这些进程/线程,从而大大减小了系统的开销。
select,poll,epoll都是IO多路复用的机制。I/O多路复用就是通过一种机制,一个进程可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。但select,poll,epoll本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而异步I/O则无需自己负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间。

select

1
2
3
4
5
int select (int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
struct timeval{
long tv_sec;   //seconds
long tv_usec;  //microseconds
};

返回值:就绪描述符的数目,超时返回0,出错返回-1
第一个参数maxfdp1指定待测试的描述字个数,它的值是待测试的最大描述字加1(因此把该参数命名为maxfdp1),描述字0、1、2…maxfdp1-1均将被测试。因为文件描述符是从0开始的。
select 函数监视的文件描述符分3类,分别是writefds、readfds、和exceptfds。如果对某一个的条件不感兴趣,就可以把它设为空指针。struct fd_set可以理解为一个集合,这个集合中存放的是文件描述符,可通过以下四个宏进行设置:

1
2
3
4
void FD_ZERO(fd_set *fdset);           //清空集合
void FD_SET(int fd, fd_set *fdset);   //将一个给定的文件描述符加入集合之中
void FD_CLR(int fd, fd_set *fdset);   //将一个给定的文件描述符从集合中删除
int FD_ISSET(int fd, fd_set *fdset);   // 检查集合中指定的文件描述符是否可以读写 

调用后select函数会阻塞,直到有描述副就绪(有数据 可读、可写、或者有except),或者超时(timeout指定等待时间,如果立即返回设为null即可),函数返回。当select函数返回后,可以 通过遍历fdset,来找到就绪的描述符。

select在while循环执行,每次都需要将客户端的文件描述重新设置,调用FD_SET。因为每次事件发生后,文件描述符将被内核修改。
select目前几乎在所有的平台上支持,其良好跨平台支持也是它的一个优点。select的一 个缺点在于单个进程能够监视的文件描述符的数量存在最大限制,在Linux上一般为1024,可以通过修改宏定义甚至重新编译内核的方式提升这一限制,但 是这样也会造成效率的降低。

poll

poll的机制与select类似,与select在本质上没有多大差别,管理多个描述符也是进行轮询,根据描述符的状态进行处理,但是poll没有最大文件描述符数量的限制。poll和select同样存在一个缺点就是,包含大量文件描述符的数组被整体复制于用户态和内核的地址空间之间,而不论这些文件描述符是否就绪,它的开销随着文件描述符数量的增加而线性增大。

1
int poll (struct pollfd *fds, unsigned int nfds, int timeout);

不同与select使用三个位图来表示三个fdset的方式,poll使用一个 pollfd的指针实现。

1
2
3
4
5
struct pollfd {
int fd; /* file descriptor */
short events; /* requested events to watch 等待的事件*/
short revents; /* returned events witnessed  实际发生了的事件*/
};

pollfd结构包含了要监视的event和发生的event,不再使用select“参数-值”传递的方式。同时,pollfd并没有最大数量限制(但是数量过大后性能也是会下降)。 和select函数一样,poll返回后,需要轮询pollfd来获取就绪的描述符。
每一个pollfd结构体指定了一个被监视的文件描述符,可以传递多个结构体,指示poll()监视多个文件描述符。每个结构体的events域是监视该文件描述符的事件掩码,由用户来设置这个域。revents域是文件描述符的操作结果事件掩码,内核在调用返回时设置这个域。events域中请求的任何事件都可能在revents域中返回。合法的事件如下:

POLLIN 有数据可读。
POLLRDNORM 有普通数据可读。
POLLRDBAND 有优先数据可读。
POLLPRI 有紧迫数据可读。
POLLOUT 写数据不会导致阻塞。
POLLWRNORM 写普通数据不会导致阻塞。
POLLWRBAND 写优先数据不会导致阻塞。
POLLMSGSIGPOLL 消息可用。

此外,revents域中还可能返回下列事件(这些事件在events域中无意义,因为它们在合适的时候总是会从revents中返回):

POLLER 指定的文件描述符发生错误。
POLLHUP 指定的文件描述符挂起事件。
POLLNVAL  指定的文件描述符非法。

使用poll()和select()不一样,你不需要显式地请求异常情况报告。

POLLIN | POLLPRI等价于select()的读事件
POLLOUT |POLLWRBAND等价于select()的写事件
POLLIN等价于POLLRDNORM |POLLRDBAND
POLLOUT则等价于POLLWRNORM

例如,要同时监视一个文件描述符是否可读和可写,我们可以设置 events为POLLIN |POLLOUT。在poll返回时,我们可以检查revents中的标志,对应于文件描述符请求的events结构体。如果POLLIN事件被设置,则文件描述符可以被读取而不阻塞。如果POLLOUT被设置,则文件描述符可以写入而不导致阻塞。这些标志并不是互斥的:它们可能被同时设置,表示这个文件描述符的读取和写入操作都会正常返回而不阻塞。

timeout参数指定等待的毫秒数,无论I/O是否准备好,poll都会返回。timeout指定为负数值表示无限超时,使poll()一直挂起直到一个指定事件发生;timeout为0指示poll调用立即返回并列出准备好I/O的文件描述符,但并不等待其它的事件。这种情况下,poll()就像它的名字那样,一旦选举出来,立即返回。

返回值和错误代码
成功时,poll()返回结构体中revents域不为0的文件描述符个数;如果在超时前没有任何事件发生,poll()返回0;失败时,poll()返回-1,并设置errno为下列值之一:

EBADF 一个或多个结构体中指定的文件描述符无效。
EFAULTfds 指针指向的地址超出进程的地址空间。
EINTR 请求的事件之前产生一个信号,调用可以重新发起。
EINVALnfds 参数超出PLIMIT_NOFILE值。
ENOMEM 可用内存不足,无法完成请求。

从上面看,select和poll都需要在返回后,通过遍历文件描述符来获取已经就绪的socket。事实上,同时连接的大量客户端在一时刻可能只有很少的处于就绪状态,因此随着监视的描述符数量的增长,其效率也会线性下降。

epoll

epoll是在2.6内核中提出的,是之前的select和poll的增强版本。相对于select和poll来说,epoll更加灵活,没有描述符限制(我看man文档时,实际上有的:/proc/sys/fs/epoll/max_user_watches (since Linux 2.6.28)

        This specifies a limit on the total number of file descriptors
          that a user can register across all epoll instances on the
          system.  The limit is per real user ID.  Each registered file
          descriptor costs roughly 90 bytes on a 32-bit kernel, and
          roughly 160 bytes on a 64-bit kernel.  Currently, the default
          value for max_user_watches is 1/25 (4%) of the available low
          memory, divided by the registration cost in bytes.

)。epoll使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需一次。

epoll操作过程

epoll操作过程需要三个接口,分别如下:

1
2
3
int epoll_create(int size);//创建一个epoll的句柄,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);
  1. int epoll_create(int size);创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大,这个参数不同于select()中的第一个参数,给出最大监听的fd+1的值,参数size并不是限制了epoll所能监听的描述符最大个数,只是对内核初始分配内部数据结构的一个建议。当创建好epoll句柄后,它就会占用一个fd值,在linux下如果查看/proc/进程id/fd/,是能够看到这个fd的,所以在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽。
  2. int epoll_ctl(int epfd, int op, int fd, struct epoll_event \*event);函数是对指定描述符fd执行op操作。
    • epfd:是epoll_create()的返回值。
    • op:表示op操作,用三个宏来表示:添加EPOLL_CTL_ADD,删除EPOLL_CTL_DEL,修改EPOLL_CTL_MOD。分别添加、删除和修改对fd的监听事件。
    • fd:是需要监听的fd(文件描述符)
    • epoll_event:是告诉内核需要监听什么事,struct epoll_event结构如下:
      1
      2
      3
      4
      struct epoll_event {
      __uint32_t events; /* Epoll events */
      epoll_data_t data; /* User data variable */
      };
      //events可以是以下几个宏的集合:

      EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
      EPOLLOUT:表示对应的文件描述符可以写;
      EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
      EPOLLERR:表示对应的文件描述符发生错误;
      EPOLLHUP:表示对应的文件描述符被挂断;
      EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。
      EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里

  3. int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);等待epfd上的io事件,最多返回maxevents个事件。参数events用来从内核得到事件的集合,maxevents告之内核这个events有多大,这个maxevents的值不能大于创建epoll_create()时的size,参数timeout是超时时间(毫秒,0会立即返回,-1将不确定,也有说法说是永久阻塞)。该函数返回需要处理的事件数目,如返回0表示已超时。

工作模式

epoll对文件描述符的操作有两种模式:LT(level trigger)ET(edge trigger)。LT模式是默认模式,LT模式与ET模式的区别如下:

  • LT模式:当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序可以不立即处理该事件。下次调用epoll_wait时,会再次响应应用程序并通知此事件。
  • ET模式:当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序必须立即处理该事件。如果不处理,下次调用epoll_wait时,不会再次响应应用程序并通知此事件。

LT模式

LT(level triggered)是缺省的工作方式,并且同时支持block和no-block socket.在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你的。

ET模式

ET(edge-triggered)是高速工作方式,只支持no-block socket。在这种模式下,当描述符从未就绪变为就绪时,内核通过epoll告诉你。然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知,直到你做了某些操作导致那个文件描述符不再为就绪状态了(比如,你在发送,接收或者接收请求,或者发送接收的数据少于一定量时导致了一个EWOULDBLOCK 错误)。但是请注意,如果一直不对这个fd作IO操作(从而导致它再次变成未就绪),内核不会发送更多的通知(only once)
ET模式在很大程度上减少了epoll事件被重复触发的次数,因此效率要比LT模式高。epoll工作在ET模式的时候,必须使用非阻塞套接口,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。

总结

man在解释LT和ET时提供了这样一个例子:

  1. 我们已经把一个用来从管道中读取数据的文件句柄(RFD)添加到epoll描述符
  2. 这个时候从管道的另一端被写入了2KB的数据
  3. 调用epoll_wait(2),并且它会返回RFD,说明它已经准备好读取操作
  4. 然后我们读取了1KB的数据
  5. 调用epoll_wait(2)……

LT模式:如果是LT模式,那么在第5步调用epoll_wait(2)之后,仍然能受到通知。
ET模式:如果我们在第1步将RFD添加到epoll描述符的时候使用了EPOLLET标志,那么在第5步调用epoll_wait(2)之后将有可能会挂起,因为剩余的数据还存在于文件的输入缓冲区内,而且数据发出端还在等待一个针对已经发出数据的反馈信息。只有在监视的文件句柄上发生了某个事件的时候 ET 工作模式才会汇报事件。因此在第5步的时候,调用者可能会放弃等待仍在存在于文件输入缓冲区内的剩余数据。

当使用epoll的ET模型来工作时,当产生了一个EPOLLIN事件后,读数据的时候需要考虑的是当recv()返回的大小如果等于请求的大小,那么很有可能是缓冲区还有数据未读完,也意味着该次事件还没有处理完,所以还需要再次读取:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
while(rs){
buflen = recv(activeevents[i].data.fd, buf, sizeof(buf), 0);
if(buflen < 0){
// 由于是非阻塞的模式,所以当errno为EAGAIN时,表示当前缓冲区已无数据可读
// 在这里就当作是该次事件已处理处.
if(errno == EAGAIN){
break;
}
else{
return;
}
}
else if(buflen == 0){
// 这里表示对端的socket已正常关闭.
}

if(buflen == sizeof(buf){
rs = 1; // 需要再次读取
}
else{
rs = 0;
}
}

Linux中的EAGAIN含义

Linux环境下开发经常会碰到很多错误(设置errno),其中EAGAIN是其中比较常见的一个错误(比如用在非阻塞操作中)。从字面上来看,是提示再试一次。这个错误经常出现在当应用程序进行一些非阻塞(non-blocking)操作(对文件或socket)的时候。
例如,以 O_NONBLOCK的标志打开文件/socket/FIFO,如果你连续做read操作而没有数据可读。此时程序不会阻塞起来等待数据准备就绪返回,read函数会返回一个错误EAGAIN,提示你的应用程序现在没有数据可读请稍后再试。又例如,当一个系统调用(比如fork)因为没有足够的资源(比如虚拟内存)而执行失败,返回EAGAIN提示其再调用一次(也许下次就能成功)。

epoll总结

在 select/poll中,进程只有在调用一定的方法后,内核才对所有监视的文件描述符进行扫描,而epoll事先通过epoll_ctl()来注册一 个文件描述符,一旦基于某个文件描述符就绪时,内核会采用类似callback的回调机制,迅速激活这个文件描述符,当进程调用epoll_wait() 时便得到通知。(此处去掉了遍历文件描述符,而是通过监听回调的的机制。这正是epoll的魅力所在。)
epoll的优点主要是一下几个方面:

  1. 监视的描述符数量不受限制,它所支持的FD上限是最大可以打开文件的数目,这个数字一般远大于2048,举个例子,在1GB内存的机器上大约是10万左 右,具体数目可以cat /proc/sys/fs/file-max察看,一般来说这个数目和系统内存关系很大。select的最大缺点就是进程打开的fd是有数量限制的。这对于连接数量比较大的服务器来说根本不能满足。虽然也可以选择多进程的解决方案( Apache就是这样实现的),不过虽然linux上面创建进程的代价比较小,但仍旧是不可忽视的,加上进程间数据同步远比不上线程间同步的高效,所以也不是一种完美的方案。
  2. IO的效率不会随着监视fd的数量的增长而下降。epoll不同于select和poll轮询的方式,而是通过每个fd定义的回调函数来实现的。只有就绪的fd才会执行回调函数。
    如果没有大量的idle -connection或者dead-connection,epoll的效率并不会比select/poll高很多,但是当遇到大量的idle- connection,就会发现epoll的效率大大高于select/poll。
  3. 文件描述符只需要复制一次到内核,不需要每一次调用函数都进行文件描述符的内核复制

留坑

django、tomcat为什么不用epoll

tornado,nodejs关于epoll的使用

monkey patch

用来在运行时动态修改已有的代码,而不需要修改原始代码。

当我们import一个module时,python会做以下几件事情

  • 导入一个module
  • 将module对象加入到sys.modules,后续对该module的导入将直接从该dict中获得
  • 将module对象加入到globals dict中

当我们引用一个模块时,将会从globals中查找。这里如果要替换掉一个标准模块,我们得做以下两件事情

  1. 将我们自己的module加入到sys.modules中,替换掉原有的模块。如果被替换模块还没加载,那么我们得先对其进行加载,否则第一次加载时,还会加载标准模块。(这里有一个import hook可以用,不过这需要我们自己实现该hook,可能也可以使用该方法hook module import)
  2. 如果被替换模块引用了其他模块,那么我们也需要进行替换,但是这里我们可以修改globals dict,将我们的module加入到globals以hook这些被引用的模块。

重载运算符

Python语言提供了运算符重载功能,增强了语言的灵活性,这一点与C++有点类似又有些不同。鉴于它的特殊性,今天就来讨论一下Python运算符重载。

Python语言本身提供了很多魔法方法,它的运算符重载就是通过重写这些Python内置魔法方法实现的。这些魔法方法都是以双下划线开头和结尾的,类似于__X__的形式,python通过这种特殊的命名方式来拦截操作符,以实现重载。当Python的内置操作运用于类对象时,Python会去搜索并调用对象中指定的方法完成操作。

类可以重载加减运算、打印、函数调用、索引等内置运算,运算符重载使我们的对象的行为与内置对象的一样。Python在调用操作符时会自动调用这样的方法,例如,如果类实现了__add__方法,当类的对象出现在+运算符中时会调用这个方法。

python 重载运算符

环境管理器

高级语法

匿名函数:lambda

高阶函数:map、filter、reduce

迭代器

生成器

枚举

装饰器

面向对象

import实现

当我们执行一行import命令时,Python解释器会查找package这个包的module模块,并将该模块作为mymodule引入到当前的工作空间。所以import语句主要是做了二件事:

  1. 查找相应的module
  2. 加载module到local namespace

查找module的过程

在import的第一个阶段,主要是完成了查找要引入模块的功能,这个查找的过程如下:

1、在缓存 sys.modules 中查找要导入的模块,若找到则直接返回该模块对象

2、如果在 sys.modules 中没有找到相应模块的缓存,则顺序搜索 sys.meta_path,逐个借助其中的 finder 来查找模块,若找到则加载后返回相应模块对象。

3、如果以上步骤都没找到该模块,则执行默认导入。即如果模块在一个包中(如import a.b),则以 a.path 为搜索路径进行查找;如果模块不在一个包中(如import a),则以 sys.path 为搜索路径进行查找。

4、如果都未找到,则抛出 ImportError 异常。

加载

对于搜索到的模块,如果在缓存 sys.modules 中则直接返回模块对象,否则就需要加载模块以创建一个模块对象。加载是对模块的初始化处理,包括以下步骤:

设置属性:包括 namefilepackageloaderpath

编译源码:将模块文件(对于包,则是其对应的 init.py 文件)编译为字节码(*.pyc 或者 *.pyo),如果字节码文件已存在且仍然是最新的,则不重新编译

执行字节码:执行编译生成的字节码(即模块文件或 init.py 文件中的语句)

需要注意的是,加载不只是发生在导入时,还可以发生在 reload 时。

名字绑定

加载完模块后,作为最后一步,import 语句会为 导入的对象 绑定名字,并把这些名字加入到当前的名字空间中。其中,导入的对象 根据导入语句的不同有所差异:

如果导入语句为 import obj,则对象 obj 可以是包或者模块

如果导入语句为 from package import obj,则对象 obj 可以是 package 的子包、package 的属性或者 package 的子模块

如果导入语句为 from module import obj,则对象 obj 只能是 module 的属性

垃圾回收

引用计数机制为主,标记-清除分代收集两种机制为辅的策略

引用计数

Python在内存中存储了每个对象的引用计数(reference count)(每个对象维护一个ob_ref字段)。如果计数值变成0,那么相应的对象就会小时,分配给该对象的内存就会释放出来用作他用。

偶尔也会出现引用循环(reference cycle)。垃圾回收器会定时寻找这个循环,并将其回收。举个例子,假设有两个对象o1和o2,而且符合o1.x == o2和o2.x == o1这两个条件。如果o1和o2没有其他代码引用,那么它们就不应该继续存在。但它们的引用计数都是1。

标记/清除

『标记清除(Mark—Sweep)』算法是一种基于追踪回收(tracing GC)技术实现的垃圾回收算法。它分为两个阶段:第一阶段是标记阶段,GC会把所有的『活动对象』打上标记,第二阶段是把那些没有标记的对象『非活动对象』进行回收。那么GC又是如何判断哪些是活动对象哪些是非活动对象的呢?

对象之间通过引用(指针)连在一起,构成一个有向图,对象构成这个有向图的节点,而引用关系构成这个有向图的边。从根对象(root object)出发,沿着有向边遍历对象,可达的(reachable)对象标记为活动对象,不可达的对象就是要被清除的非活动对象。根对象就是全局变量、调用栈、寄存器。

.svg

在上图中,我们把小黑圈视为全局变量,也就是把它作为root object,从小黑圈出发,对象1可直达,那么它将被标记,对象2、3可间接到达也会被标记,而4和5不可达,那么1、2、3就是活动对象,4和5是非活动对象会被GC回收。

标记清除算法作为Python的辅助垃圾收集技术主要处理的是一些容器对象,比如list、dict、tuple,instance等,因为对于字符串、数值对象是不可能造成循环引用问题。Python使用一个双向链表将这些容器对象组织起来。不过,这种简单粗暴的标记清除算法也有明显的缺点:清除非活动的对象前它必须顺序扫描整个堆内存,哪怕只剩下小部分活动对象也要扫描所有对象。

分代回收

分代回收是一种以空间换时间的操作方式,Python将内存根据对象的存活时间划分为不同的集合,每个集合称为一个代,Python将内存分为了3“代”,分别为年轻代(第0代)、中年代(第1代)、老年代(第2代),他们对应的是3个链表,它们的垃圾收集频率与对象的存活时间的增大而减小。新创建的对象都会分配在年轻代,年轻代链表的总数达到上限时,Python垃圾收集机制就会被触发,把那些可以被回收的对象回收掉,而那些不会回收的对象就会被移到中年代去,依此类推,老年代中的对象是存活时间最久的对象,甚至是存活于整个系统的生命周期内。同时,分代回收是建立在标记清除技术基础之上。分代回收同样作为Python的辅助垃圾收集技术处理那些容器对象

基本数据类型

数组

Go语言中数组是值语义。一个数组变量即表示整个数组,它并不是隐式的指向第一个元素的指针(比如C语言的数组),而是一个完整的值。当一个数组变量被赋值或者被传递的时候,实际上会复制整个数组。如果数组较大的话,数组的赋值也会有较大的开销。为了避免复制数组带来的开销,可以传递一个指向数组的指针,但是数组指针并不是数组。

1
2
3
4
5
6
7
8
9
var a = [...]int{1, 2, 3} // a 是一个数组
var b = &a // b 是指向数组的指针

fmt.Println(a[0], a[1]) // 打印数组的前2个元素
fmt.Println(b[0], b[1]) // 通过数组指针访问数组元素的方式和数组类似

for i, v := range b { // 通过数组指针迭代数组的元素
fmt.Println(i, v)
}

但是数组指针类型依然不够灵活,因为数组的长度是数组类型的组成部分,指向不同长度数组的数组指针类型也是完全不同的。

字符串

Go语言字符串的底层结构在reflect.StringHeader中定义:

1
2
3
4
type StringHeader struct {
Data uintptr
Len int
}

分析可以发现,“Hello, world”字符串底层数据和以下数组是完全一致的:

1
2
3
var data = [...]byte{
'h', 'e', 'l', 'l', 'o', ',', ' ', 'w', 'o', 'r', 'l', 'd',
}

推荐系统

UserCF ItemCF
性能 适用于用户较少的场合,如果用户很多,计算用户相似度矩阵代价很大 适用于物品数明显小于用户数的场合,如果物品
领域 时效性较强,用户个性化兴趣不太明显的领域 长尾物品丰富,用户个性化需求强烈的领域
实时性 用户有新行为,不一定造成推荐结果的立即变化 用户有新行为,一定会导致推荐结果的实时变化
冷启动 在新用户对很少的物品产生行为后,不能立即对他 进行个性化推荐,因为用户相似度表是每隔一段时间离线计算的。新物品上线后一段时间,一旦有用户对物品产生行为,就可以将新物品推荐给和对它产生行为的用户兴趣相似的其他用户
推荐理由 很难提供令用户信服的推荐解释 利用用户的历史行为给用户做推荐解释,可以令用户比较信服