-
Notifications
You must be signed in to change notification settings - Fork 0
/
tipuesearch_content.json
1 lines (1 loc) · 318 KB
/
tipuesearch_content.json
1
{"pages":[{"url":"https://chukeer.github.io/boost内存池singleton_pool详解.html","text":"基本概念 内存池可以避免频繁分配释放内存时导致操作系统内存碎片的问题 boost内存池提供了如下对象 singleton_pool 对pool内存池的封装,在其基础上加了锁,避免多线程操作的安全问题,并暴露了常用的接口。 pool 真正内存池的实现,管理block链表,并交给simple_segregated_storage分割成固定长度的chunk,每次给用户分配1个或多个chunk,每个chunk都是固定长度的 simple_segregated_storage 将block分割成chunk并组成链表进行管理 通常我们使用的都是singleton_pool对象 接口定义 这里只列出常用接口 template <typename Tag, unsigned RequestedSize, typename UserAllocator, typename Mutex, unsigned NextSize, unsigned MaxSize > class singleton_pool { public: static void * malloc(); static void * ordered_malloc(); static void * ordered_malloc(const size_type n); static bool is_from(void * const ptr); static void free(void * const ptr); static void ordered_free(void * const ptr); static void free(void * const ptr, const size_type n); static void ordered_free(void * const ptr, const size_type n); static bool release_memory(); static bool purge_memory(); }; 先看几个模板参数的含义 Tag : 一般定义为一个struct类型,用来唯一标识内存池对象 RequestSize : 每次向内存池请求的内存大小 UserAllocator : 用户自定义的内存分配器,向操作系统申请释放内存时使用,默认为boost::default_user_allocator_new_delete Mutex : 互斥锁,默认为boost::details::pool::default_mutex NextSize : 内存池向操作系统第一次申请内存的chunk数量,默认为32 MaxSize : 内存池向操作系统申请一次内存的最大chunk数量,默认为0,代表无上限 使用方法如下 struct MyPoolTag { } ; typedef boost : :singleton_pool < MyPoolTag , 1024 > my_pool ; int main () { char * p = ( char * ) my_pool :: malloc (); // 分配内存大小为 1024 byte my_pool :: free ( p ); return 0 ; } malloc和free用来向内存池分配和归还内存,ordered_*代表顺序内存的操作,release_memory可以释放ordered内存池里的空闲block,purge_memory强制释放内存池所有block,具体行为后续分析 数据结构 内存池里有两个关键对象,分别为block和chunk block的管理 block为一大块连续的内存,chunk是由block分隔的等长内存块,其结构如下 block由pool对象进行管理,组成链表,next_ptr指向下个block,next_size为下个block的大小。对于非ordered内存池,每次申请的block放入链表头,如果是ordered内存池,则需要将block插入合适的位置,保持block起始地址的顺序 假设RequestSize为1K,NextSize为默认值32, MaxSize为默认值0,那么第一次向内存池请求内存时,由于没有可用的block,内存池会向操作系统申请第一个block,包含32个chunk,当第一个block用完之后,下一次申请的block会包含64个chunk(为前一次的2倍),依次内推,如果指定的MaxSize大于0,则一次申请的block最多包含MaxSize个chunk chunk的管理 每个block都交由simple_segregated_storage分割成chunk并进行管理,chunk也组成链表 block分割之后结构如下 first指向第一个可用的chunk,每个chunk指向相邻的chunk。多个block之间的chunk也会组成链表(真实场景中chunk可能并不是有序的) 在初始化的时候,block中的chunk指向其物理地址相邻的且起始地址比它大的chunk,最后一个chunk指向NULL或下一个block的第一个chunk 分配内存 分配内存接口如下 static void * malloc(); static void * ordered_malloc(); static void * ordered_malloc(const size_type n); malloc会返回第一个可用的chunk给用户,复杂度为O(1),ordered_malloc还会保持block和chunk的顺序,复杂度为O(N),只有在分配连续n个chunk时比较适用,其它时候不推荐使用ordered内存池 假设NextSize为2,当我们调用4次malloc,分别分配buf1, buf2, buf3, buf4, 其过程如下 分配buf1,没有可用block,内存池向操作系统申请block,包含2个chunk,并返回第一个chunk给用户 分配buf2,有可用的block,直接返回first指向的chunk给用户,同时first指向NULL 分配buf3,没有可用的block,内存池向操作系统申请新的block,包含4个chunk,并返回第一个chunk给用户,新的block插入block链表的头部 分配buf4,有可用的block,直接返回first指向的chunk给用户 对于ordered_malloc,其区别在于向操作系统申请新的block之后,会将block插入合适的位置,以保持block地址的顺序性 归还内存 归还内存接口如下 static void ordered_free(void * const ptr); static void free(void * const ptr, const size_type n); static void ordered_free(void * const ptr, const size_type n); free会将归还的chunk直接指向first,将first指向归还的chunk,复杂度为O(1),ordered_free在归还chunk时会保持其最初在链表中的顺序,复杂度为O(N),ordered_free可以返回连续chunk 在前面例子的基础上,我们按以下顺序分别释放buf,对比free和ordered_free操作的内存结构变化(为了简化,某些步骤会省略掉NULL指针以及next_ptr的指向关系) 操作步骤 free ordered_free 分配之后的结构 释放buf1 释放buf2 释放buf3 释放buf4 调用free之后,chunk的指向关系和chunk最初在block中被分割的顺序完全不一致,但每次释放只需在chunk链表的头部操作,因此效率非常高 而调用ordered_free后,所有chunk仍旧保持了最初分割时候的顺序,但是在释放每个chunk时,都需要从first开始查找,找到一个合适的位置进进行插入 清理内存 清理内存有如下连个接口 static bool release_memory(); static bool purge_memory(); purge_memory会将所有block无条件释放,即便有些chunk已经分配出去,因此要慎用。release_memory只对ordered内存池有效,会释放没有分配chunk的block,假设现在内存结构如下 block1的两个chunk都还没被分配出去,block2有一个chunk已分配出去,并且内存池为ordered结构,此时调用release_memory后,block1会被归还给操作系统,内存结构如下 总结 boost内存池提供了丰富的接口,malloc和free都非常高效,但是由于是非ordered,无法手动回收内存,如果服务内存突增之后又降下来,内存池会一直占用峰值达到的内存 ordered内存池虽然支持内存回收,但分配效率不高,不推荐使用","tags":"Language","title":"boost内存池singleton_pool详解"},{"url":"https://chukeer.github.io/Boost Asio async_write回调行为分析.html","text":"本文主要分析async_write在一些边界情况下的回调行为,包括如下几点 对方接受缓慢 对方正常关闭 对方异常关闭 主动shutdown_receive 主动shutdown_send 主动shutdown_both 先给出结论 正常情况下async_write将数据写入socket发送缓冲区就会触发回调 在以下情况下async_write回调也会被触发,并携带相应错误码:对方关闭(close, kill, kill -9),主动shutdown_send,主动shutdown_both 基本知识 每个tcp socket在内核都有发送缓冲区和接收缓冲区,发送函数send只是将数据写入发送缓冲区,接受函数recv是将数据从接收缓冲区拷贝到应用层,当接收缓冲区已满时,收端会通知发端接收窗口关闭(win=0)。真正的tcp发送是由内核协议来完成的,即便发送进程退出,只要发送缓冲区还有数据,对方仍旧能收到这部分数据 async_write底层调用的async_write_some,可能会分多次调用,最终的发送也是将数据写入发送缓冲区,只有当async_write的所有数据都写入缓冲区,async_write的回调才会被调用 发送缓冲区写满的回调分析 我们设计这样一种场景,当客户端连接上之后,服务端即调用async_write向其发送数据,并且在回调函数里继续调用async_write,观察回调情况,流程如下 每次async_write发送10K数据,并且客户端连接上来之后不调用recv,模拟接受缓慢的场景,观察on_write调用次数,关键代码如下 int g_write_count = 1000 ; int g_write_size = 1024 * 10 ; int g_index = 0 ; void on_write ( Context * context , const boost : :system : :error_code & err , std : :size_t bytes ) { if ( err ) { cout << \"on_write, err:\" << err . message () << endl ; context -> socket -> close (); delete context -> socket ; context -> socket = NULL ; delete context ; return ; } static int count = 0 ; cout << \"on_write bytes:\" << bytes << \" time:\" << time ( NULL ) << \" count:\" << ++ count << endl ; if ( g_index < g_write_count ) { context -> write_buf = ( char * ) malloc ( g_write_size ); memset ( context -> write_buf , 'a' , g_write_size ); async_write ( * context -> socket , buffer ( context -> write_buf , g_write_size ) , boost :: bind ( & on_write , context , placeholders :: error , placeholders :: bytes_transferred )); cout << \"async_write, index:\" << ++ g_index << endl ; delete context -> write_buf ; context -> write_buf = NULL ; } } 客户端是用Python解释器进行交互式操作,便于分析每一步的结果 测试步骤为 启动服务端 Python客户端连接服务端 退出服务端 Python客户端继续读取数据 在这个过程中同时进行抓包分析,我们发现 客户端连接上来之后(即步骤2完成之后)服务端一共调用了142次回调,第143次async_write的回调并没有调用 此时客户端的接收缓冲区已满(注意观察两图的时间戳) 服务端退出之后客户端仍旧能读到数据,并且读到的数据包(这里以10K作为一个数据包)数量正好介于142和143之间 结合前面的基础知识分析这个过程如下 客户端连接上来之后服务端开始调用async_write,当数据被写入发送缓冲区时回调函数即被调用 内核协议将发送缓冲区数据发送给客户端,写入接收缓冲区,直到对方接收缓冲区写满,这个过程中步骤1一直在执行(一共执行了142次) 当服务端调用第143次async_write时,由于发送缓冲区空间不够,因此回调没有发生,但仍旧写入了一部分数据 服务端退出后客户端读取接受缓冲区数据,同时内核将发送缓冲区数据发送给客户端,这个数据量为前142次async_write写入的全部数据加第143次async_write写入的部分数据 倘若服务端不退出,当客户端读取一部分数据后,第143次async_write的回调即会发生,这里就不贴出相应数据了 从以上分析可以看出,当数据写入发送缓冲区后async_write的回调即会发生,如果发送缓冲区已满,async_write的回调不会发生 socket关闭的回调分析 还是使用前面的服务端和客户端程序,测试场景如下 启动服务端 Python客户端连接上服务端,服务端调用一次async_write sleep 15秒钟,让客户端有充足的时间执行操作 客户端分别在不读取缓冲区和读完缓冲区的两种情况下执行close, kill, kill -9,并抓包进行分析 对服务端通过发送kill -15的信号执行shutdown_send, shutdown_receive, shutdown_both,分析回调行为 服务端通过捕捉信号执行shutdown操作如下 void SignalHandler ( int signum ) { cout << \"Interrupt signal (\" << signum << \") received\" << endl ; boost :: system :: error_code ec ; g_socket -> shutdown ( boost :: asio :: ip :: tcp :: socket :: shutdown_send , ec ); // g_socket -> shutdown ( boost :: asio :: ip :: tcp :: socket :: shutdown_receive , ec ); // g_socket -> shutdown ( boost :: asio :: ip :: tcp :: socket :: shutdown_both , ec ); } 对端关闭(close,kill,kill -9) 如果对端接收缓冲区还有数据,关闭(close,kill, kill -9)会导致对端发送RST包,如下最后一行,此时本端调用async_write回立马产生回调,err参数为Connection reset by peer 23 : 37 : 36.235495 IP localhost . 50366 > localhost . 9999 : Flags [ S ], seq 1891887111 , win 43690 , options [ mss 65495 , sackOK , TS val 656034598 ecr 0 , nop , wscale 7 ], length 0 23 : 37 : 36.235504 IP localhost . 9999 > localhost . 50366 : Flags [ S .], seq 53115564 , ack 1891887112 , win 43690 , options [ mss 65495 , sackOK , TS val 656034598 ecr 656034598 , nop , wscale 7 ], length 0 23 : 37 : 36.235514 IP localhost . 50366 > localhost . 9999 : Flags [.], ack 1 , win 342 , options [ nop , nop , TS val 656034598 ecr 656034598 ], length 0 23 : 37 : 36.235749 IP localhost . 9999 > localhost . 50366 : Flags [ P .], seq 1 : 10241 , ack 1 , win 342 , options [ nop , nop , TS val 656034598 ecr 656034598 ], length 10240 23 : 37 : 36.235757 IP localhost . 50366 > localhost . 9999 : Flags [.], ack 10241 , win 1365 , options [ nop , nop , TS val 656034598 ecr 656034598 ], length 0 **** close /kill/ kill - 9 at this moment **** 23 : 37 : 39.011643 IP localhost . 50366 > localhost . 9999 : Flags [ R .], seq 1 , ack 10241 , win 1365 , options [ nop , nop , TS val 0 ecr 656034598 ], length 0 如果对端接受缓冲区没有数据,通过close关闭或者被进程被kill,会发送FIN包,此时本端调用async_write会收到正常回调,但对端回的是RST包,当下次调用async_write时产生错误回调,err为Broken pipe 23 : 46 : 13.105899 IP localhost . 50369 > localhost . 9999 : Flags [ S ], seq 94591470 , win 43690 , options [ mss 65495 , sackOK , TS val 656163816 ecr 0 , nop , wscale 7 ], length 0 23 : 46 : 13.105909 IP localhost . 9999 > localhost . 50369 : Flags [ S .], seq 1590802765 , ack 94591471 , win 43690 , options [ mss 65495 , sackOK , TS val 656163816 ecr 656163816 , nop , wscale 7 ], length 0 23 : 46 : 13.105922 IP localhost . 50369 > localhost . 9999 : Flags [.], ack 1 , win 342 , options [ nop , nop , TS val 656163816 ecr 656163816 ], length 0 23 : 46 : 13.106132 IP localhost . 9999 > localhost . 50369 : Flags [ P .], seq 1 : 10241 , ack 1 , win 342 , options [ nop , nop , TS val 656163816 ecr 656163816 ], length 10240 23 : 46 : 13.106139 IP localhost . 50369 > localhost . 9999 : Flags [.], ack 10241 , win 1365 , options [ nop , nop , TS val 656163816 ecr 656163816 ], length 0 **** close / kill at this moment *** 23 : 46 : 19.929814 IP localhost . 50369 > localhost . 9999 : Flags [ F .], seq 1 , ack 10241 , win 1365 , options [ nop , nop , TS val 656165522 ecr 656163816 ], length 0 23 : 46 : 19.933364 IP localhost . 9999 > localhost . 50369 : Flags [.], ack 2 , win 342 , options [ nop , nop , TS val 656165523 ecr 656165522 ], length 0 23 : 46 : 23.106636 IP localhost . 9999 > localhost . 50369 : Flags [ P .], seq 10241 : 20481 , ack 2 , win 342 , options [ nop , nop , TS val 656166316 ecr 656165522 ], length 10240 23 : 46 : 23.106658 IP localhost . 50369 > localhost . 9999 : Flags [ R ], seq 94591472 , win 0 , length 0 如果对端接收缓冲区没有数据,并且进程是被kill -9杀掉,则会发送RST包,和缓冲区有数据时退出一样,此时本端调用async_write会产生错误回调,err为Connection reset by peer,过程如下 23 : 53 : 39.531436 IP localhost . 50372 > localhost . 9999 : Flags [ S ], seq 1739017762 , win 43690 , options [ mss 65495 , sackOK , TS val 656275422 ecr 0 , nop , wscale 7 ], length 0 23 : 53 : 39.531449 IP localhost . 9999 > localhost . 50372 : Flags [ S .], seq 2103076725 , ack 1739017763 , win 43690 , options [ mss 65495 , sackOK , TS val 656275422 ecr 656275422 , nop , wscale 7 ], length 0 23 : 53 : 39.531464 IP localhost . 50372 > localhost . 9999 : Flags [.], ack 1 , win 342 , options [ nop , nop , TS val 656275422 ecr 656275422 ], length 0 23 : 53 : 39.531672 IP localhost . 9999 > localhost . 50372 : Flags [ P .], seq 1 : 10241 , ack 1 , win 342 , options [ nop , nop , TS val 656275422 ecr 656275422 ], length 10240 23 : 53 : 39.531680 IP localhost . 50372 > localhost . 9999 : Flags [.], ack 10241 , win 1365 , options [ nop , nop , TS val 656275422 ecr 656275422 ], length 0 **** kill - 9 at this moment **** 23 : 53 : 42.420885 IP localhost . 50372 > localhost . 9999 : Flags [ R .], seq 1 , ack 10241 , win 1365 , options [ nop , nop , TS val 0 ecr 656275422 ], length 0 本端关闭 本端通过shutdown_send关闭,本端会发送FIN包,调用async_write会产生错误回调,err为Broken pipe 00 : 01 : 00.243682 IP localhost . 50383 > localhost . 9999 : Flags [ S ], seq 859261593 , win 43690 , options [ mss 65495 , sackOK , TS val 656385600 ecr 0 , nop , wscale 7 ], length 0 00 : 01 : 00.243693 IP localhost . 9999 > localhost . 50383 : Flags [ S .], seq 2783724350 , ack 859261594 , win 43690 , options [ mss 65495 , sackOK , TS val 656385600 ecr 656385600 , nop , wscale 7 ], length 0 00 : 01 : 00.243707 IP localhost . 50383 > localhost . 9999 : Flags [.], ack 1 , win 342 , options [ nop , nop , TS val 656385600 ecr 656385600 ], length 0 00 : 01 : 00.244063 IP localhost . 9999 > localhost . 50383 : Flags [ P .], seq 1 : 10241 , ack 1 , win 342 , options [ nop , nop , TS val 656385600 ecr 656385600 ], length 10240 00 : 01 : 00.244074 IP localhost . 50383 > localhost . 9999 : Flags [.], ack 10241 , win 1365 , options [ nop , nop , TS val 656385600 ecr 656385600 ], length 0 **** shutdown at this moment **** 00 : 01 : 02.275905 IP localhost . 9999 > localhost . 50383 : Flags [ F .], seq 10241 , ack 1 , win 342 , options [ nop , nop , TS val 656386108 ecr 656385600 ], length 0 00 : 01 : 02.313533 IP localhost . 50383 > localhost . 9999 : Flags [.], ack 10242 , win 1365 , options [ nop , nop , TS val 656386118 ecr 656386108 ], length 0 本端调用shutdown_both,结果同shutdown_send 本端调用shutdown_receive,对async_write回调没有影响 结论 综上可以看出,async_write的回调行为和连接是否正常以及连接关闭时收到的是RST包还是FIN包有关,总结如下 连接状态 对端接受缓冲区是否有数据 async_write回调行为 正常 -- 写入缓冲区即回调,若缓冲区已满,暂时不会回调,会等到缓冲区可以写入之后才回调 对端关闭(close, kill, kill -9) 是 本端收到RST包,下次async_write产生错误回调,err为Connection reset by peer 对端关闭(close, kill) 否 本端收到FIN包,下次async_write正常回调并收到RST包(没有收到ACK包),再次err为Broken pipe 对端关闭(kill -9) 否 本端收到RST包,下次async_write产生错误回调,err为Connection reset by peer 本端shutdown_send,shutdown_both -- 下次async_write产生错误回调,err为Broken pipe 本端shutdown_receive -- 对async_write没有影响 完整服务端测试代码 #include <signal.h> #include <unistd.h> #include <iostream> #include <string> #include \"boost/asio.hpp\" #include \"boost/bind.hpp\" #include \"boost/thread.hpp\" using namespace std ; using namespace boost :: asio ; int g_write_count = 1000 ; int g_write_size = 1024 * 10 ; int g_index = 0 ; ip :: tcp :: socket * g_socket = NULL ; struct Context { Context ( ip :: tcp :: socket * s , io_service * service , ip :: tcp :: acceptor * a ) : socket ( s ), io ( service ), acceptor ( a ) { } ip :: tcp :: socket * socket ; ip :: tcp :: acceptor * acceptor ; io_service * io ; char read_buf [ 1024 ]; char * write_buf ; }; void SignalHandler ( int signum ) { cout << \"Interrupt signal (\" << signum << \") received\" << endl ; boost :: system :: error_code ec ; g_socket -> shutdown ( boost :: asio :: ip :: tcp :: socket :: shutdown_send , ec ); //g_socket->shutdown(boost::asio::ip::tcp::socket::shutdown_receive, ec); //g_socket->shutdown(boost::asio::ip::tcp::socket::shutdown_both, ec); } void on_read ( Context * context , const boost :: system :: error_code & err , std :: size_t bytes ) { if ( err ) { cout << \"on_read, err:\" << err . message () << endl ; return ; } cout << \"on reawd, bytes:\" << bytes << endl ; async_read ( * context -> socket , buffer ( context -> read_buf , 1024 ), boost :: bind ( & on_read , context , placeholders :: error , placeholders :: bytes_transferred )); } void on_write ( Context * context , const boost :: system :: error_code & err , std :: size_t bytes ) { if ( err ) { cout << \"on_write, err:\" << err . message () << endl ; context -> socket -> close (); delete context -> socket ; delete context ; return ; } static int count = 0 ; cout << \"on_write bytes:\" << bytes << \" time:\" << time ( NULL ) << \" count:\" << ++ count << endl ; //sleep(15); if ( g_index < g_write_count ) { context -> write_buf = ( char * ) malloc ( g_write_size ); memset ( context -> write_buf , 'a' , g_write_size ); async_write ( * context -> socket , buffer ( context -> write_buf , g_write_size ), boost :: bind ( & on_write , context , placeholders :: error , placeholders :: bytes_transferred )); cout << \"async_write, index:\" << ++ g_index << endl ; delete context -> write_buf ; context -> write_buf = NULL ; } } void on_accept ( Context * context , const boost :: system :: error_code & err ) { if ( err ) { cout << \"on accept, err:\" << err << endl ; return ; } boost :: system :: error_code re_ec ; ip :: tcp :: socket * socket = context -> socket ; ip :: tcp :: endpoint endpoint = socket -> remote_endpoint ( re_ec ); cout << \"remote addr \" << endpoint . address (). to_v4 () << \":\" << endpoint . port () << endl ; context -> write_buf = ( char * ) malloc ( g_write_size ); memset ( context -> write_buf , 'a' , g_write_size ); async_write ( * socket , buffer ( context -> write_buf , g_write_size ), boost :: bind ( & on_write , context , placeholders :: error , placeholders :: bytes_transferred )); cout << \"async_write, index:\" << ++ g_index << endl ; async_read ( * socket , buffer ( context -> read_buf , 1024 ), boost :: bind ( & on_read , context , placeholders :: error , placeholders :: bytes_transferred )); delete context -> write_buf ; context -> write_buf = NULL ; g_socket = context -> socket ; } int main ( int argc , char * argv []) { if ( argc != 4 ) { cout << \"usage: \" << argv [ 0 ] << \" port write_size write_count\" << endl ; return 0 ; } signal ( SIGTERM , SignalHandler ); int port = atoi ( argv [ 1 ]); g_write_size = atoi ( argv [ 2 ]); g_write_count = atoi ( argv [ 3 ]); io_service service ; boost :: asio :: ip :: tcp :: acceptor acceptor ( service ); ip :: tcp :: endpoint endpoint ( boost :: asio :: ip :: tcp :: v4 (), port ); acceptor . open ( endpoint . protocol ()); acceptor . set_option ( ip :: tcp :: acceptor :: reuse_address ( true )); acceptor . bind ( endpoint ); acceptor . listen (); ip :: tcp :: socket * s = new ip :: tcp :: socket ( service ); Context * context = new Context ( s , & service , & acceptor ); acceptor . async_accept ( * s , boost :: bind ( & on_accept , context , _1 )); service . run (); return 0 ; }","tags":"Language","title":"Boost Asio async_write回调行为分析"},{"url":"https://chukeer.github.io/cppcheck使用方式详解.html","text":"顾名思义,cppcheck是对C++代码的静态检查工具,只需对源代码进行静态扫描,即可发现一些隐患,包括如下分类 Dead pointers Division by zero Integer overflows Invalid bit shift operands Invalid conversions Invalid usage of STL Memory management Null pointer dereferences Out of bounds checking Uninitialized variables Writing const data 并按严重程度又进行了如下区分 error: 直接判定为bug warning: 可能会引起bug style: 代码风格的问题,如包含未使用函数,重复代码等 performance: 性能相关的问题,如函数参数中复杂对象的值传递 portability: 代码兼容性相关的问题 information: 可以忽略不看 以上警告的准确率并非100%,我们还需人为的去对照代码具体分析 基本使用方式 检查文件和目录 检查单个文件 cppcheck file1.cpp 检查目录下所有文件 cppcheck path 检查目录下所有文件,排除某些目录 cppcheck -i path/sub1 -i path/sub2 path 参数选项介绍 只打印扫描结果 -q, --quiet 指定消息级别 --enable 指定展示消息级别,默认只展示error类型的消息,展示warning和performance如下 cppcheck --enable=warning --enable=performance path 并发扫描 -j 多进程扫描,和make的选项含义相同 输出不确定项 --inconclusive cppcheck只会输出它自己确定的问题,使用该参数相当于放宽对bug的标准 指定源代码平台 --platform 可指定为unix32, unix64, win32A, win32W, win64,默认为cppcheck编译所使用平台 以xml格式输出 --xml-version 可指定为1和2 cppcheck --xml-version=2 file1.cpp 格式化输出信息 --template 提供的模板包括vs和gcc 比如 cppcheck --template=vs gui/test.cpp 输出格式为 Checking gui/test.cpp... gui/test.cpp(31): error: Memory leak: b gui/test.cpp(16): error: Mismatching allocation and deallocation: k cppcheck --template=gcc gui/test.cpp 输出格式为 Checking gui/test.cpp... gui/test.cpp:31: error: Memory leak: b gui/test.cpp:16: error: Mismatching allocation and deallocation: k 也可以自定义格式, cppcheck --template=\"{file},{line},{severity},{id},{message}\" gui/test.cpp Checking gui/test.cpp... gui/test.cpp,31,error,memleak,Memory leak: b gui/test.cpp,16,error,mismatchAllocDealloc,Mismatching allocation and deallocation 可使用的标记包括 标记名 含义 callstack callstack - if available file filename id message id line line number message verbose message text severity a type/rank of message 忽略指定类型的消息 按如下方式定义一个消息类型 [error id]:[filename]:[line] [error id]:[filename2] [error id] --suppress 通过命令行指定 cppcheck --suppress=memleak:src/file1.cpp src/ --suppressions-list 通过文件指定,假设有文件suppressions.txt内容如下 // suppress memleak and exceptNew errors in the file src/file1.cpp memleak:src/file1.cpp exceptNew:src/file1.cpp // suppress all uninitvar errors in all files uninitvar 使用方式如下 cppcheck --suppressions-list=suppressions.txt src/ --inline-suppr 在源代码中指定,假设如下文件 void f() { char arr[5]; arr[10] = 0; } 扫描报错为 # cppcheck test.c Checking test.c... [test.c:3]: (error) Array 'arr[5]' index 10 out of bounds 将文件修改如下 void f() { char arr[5]; // cppcheck-suppress arrayIndexOutOfBounds arr[10] = 0; } 采用如下命令可屏蔽该错误 cppcheck --inline-suppr test.c 结果分析 将扫描结果存入xml文件,再通过cppcheck-htmlreport工具可展示为html格式,方便查看错误信息以及对应源代码,cppcheck-htmlreport是一个Python脚本,可从cppcheck的github仓库下载 https://github.com/danmar/cppcheck.git cppcheck --quiet --std=c++03 --platform=unix64 \\ --language=c++ -j 8 --enable=warning --enable=performance \\ --xml-version=2 src/ > cppcheck-result.xml 2>&1 cppcheck-htmlreport --file cppcheck-result.xml \\ --source-dir=. \\ --report-dir=/usr/share/nginx/html/cppcheck/ \\ --title=balabala cppcheck-htmlreport参数选项说明如下 --file: cppcheck输出结果文件 --source-dir: 源代码路径,如果cppcheck扫描的是src目录,则这里为src的父目录 --report-dir: 报告存储目录 --titie: 报告的标题名字 展示结果如下 集成到其它工具 cppcheck可以和git, svn, jenkins等平台和工具结合,在svn和git commit之前执行相应脚本,或者在jenkins项目构建之后执行相应检查,具体用法参考以下链接 CLion - Cppcheck plugin Code::Blocks - integrated CodeDX (software assurance tool) - integrated CodeLite - integrated CppDepend 5 - integrated Eclipse - Cppcheclipse KDevelop - integrated since v5.1 gedit - gedit plugin Hudson - Cppcheck Plugin Jenkins - Cppcheck Plugin Mercurial (Linux) - pre-commit hook - Check for new errors on commit (requires interactive terminal) Tortoise SVN - Adding a pre-commit hook script Git (Linux) - pre-commit hook - * * Check for errors in files going into commit (requires interactive terminal) Visual Studio - Visual Studio plugin tCreator - Qt Project Tool (qpt) 参考文档 cppcheck官网 http://cppcheck.net/ cppcheck1.8.1使用文档 http://cppcheck.net/manual.pdf","tags":"Tool","title":"cppcheck使用方式详解"},{"url":"https://chukeer.github.io/understand-vim-encoding.html","text":"vim的使用环境比较复杂,可以通过terminal在本地使用(比如Mac或Linux主机),也可以ssh连接到远程服务器使用,还可以使用gvim。这里主要讨论terminal下的使用,搞清楚了vim在terminal下的编码设置,gvim相对更简单,自然也就了解了 首先我们要理解字符和字节的区别,字符是用来显示的,而字节是存储和传输时使用,网络传输的是字节流,文件存储的也是字节流,而编辑器要显示文件内容,就需要转化为字符来显示,字符和字节之间的关系可以定义如下 encode(字符, 编码方案) -> 字节 decode(字节, 编码方案) -> 字符 可见encode和decode是一对逆向操作,它们都需要指定编码方案,如果编码方案不一致,则会操作失败 通过terminal操作远程vim时,其数据流向可以表示如下 terminal -> 本地shell -> ssh -> 远程shell -> vim 在这个流向里,只有terminal和vim需要显示字符,其它进程或服务只是做数据传输,如果只是单纯传递二进制数据,是不需要涉及编码解码的,只有当显示字符的时候才需要进行解码,因此只有terminal和vim需要配置编码,而terminal需要和本地shell打交道,远程vim也需要和shell打交道,shell的编码也至关重要 Terminal编码 terminal本身也是一个进程,最终的字符显示都需要由terminal来完成,我们在terminal上输入字符也会由它进行编码之后再传递,简单来说就是 输入时,terminal将字符编码为二进制,传递给其它进程或服务 显示时,terminal接收到其它进程或服务的二进制数据流,解码为字符进行显示 这里编解码方案就是terminal需要配置的 shell编码 locale命令也可查看shell编码设置,以LC_开头的代表系统不同类别的编码方案,分为如下几类 语言符号及其分类(LC_CTYPE) 数字(LC_NUMERIC) 比较和排序习惯(LC_COLLATE) 时间显示格式(LC_TIME) 货币单位(LC_MONETARY) 信息主要是提示信息,错误信息,状态信息,标题,标签,按钮和菜单等(LC_MESSAGES) 姓名书写方式(LC_NAME) 地址书写方式(LC_ADDRESS) 电话号码书写方式(LC_TELEPHONE) 度量衡表达方式 (LC_MEASUREMENT) 默认纸张尺寸大小(LC_PAPER) 对locale自身包含信息的概述(LC_IDENTIFICATION)。 至于最终选什么方案,其优先级如下 LC_ALL > LC_* > LANG 也就是说一切都以LC_ALL为主,如果没有设置,则查找LC_*对应的设置项,如果仍旧没有,则使用LANG的设置,影响字符显示的为LC_CTYPE项,为了便于描述,后续提到shell编码时一律指LC_ALL项,设置shell编码方式如下 export LC_ALL=zh_CN.GBK export LC_ALL=zh_CN.UTF-8 terminal编码和shell编码不一致会出现什么情况? 假设我们本地terminal编码设置为UTF-8,shell编码设置为GBK,当我们在terminal上输入中文字符时,会显示为乱码或不显示 我们分析一下在终端输入shell命令时的数据交互 输入法 -> terminal -> shell -> terminal 在terminal上输入字符,terminal根据自己的编码设置,将字符编码为utf8字节,传递给shell shell收到二进制数据,转码为gbk格式数据,再回传给terminal显示。这一步转码会出错 terminal收到二进制数据,按utf8方式解码为字符并显示。这一步解码会出错 将terminal和shell看做两个服务,它们之间需要进行数据交互,在发送数据时进行编码,在收到数据时会进行解码,如果编码方案和解码方案不一致,就会导致乱码或失败,表现形式就是在terminal上输入中文命令时会显示异常,执行结果也不符合预期 如果用ssh登陆远程shell,则远程shell的编码配置和本地shell一致,在通过 ssh -v 可以打印ssh在登陆过程中做了哪些事 因此我们第一个要点是 terminal和shell编码必须设置为一样 Vim编码 vim和编码相关的有4个设置项 fileencodings 一个编码列表,以逗号分隔,打开文件时会依次以列表里的编码方式去解码,如果执行成功,则该编码为文件编码,并设置fileencoding fileencoding 检测到的文件编码 encoding vim内部使用的编码,包括文件内容,寄存器等 termencoding terminal编码,在terminal中使用vim时会用到,gvim不需要设置 可见vim的编码设置相当复杂,我们还是以具体的实例来分析这些编码设置的作用 不管是打开本地vim,还是打开远程vim,我们首先保证本地shell的编码设置和terminal一致,这样涉及到编解码的数据流可以简化为 terminal -> vim 打开文件 vim打开文件,最终还是在terminal上显示,这个过程和编码设置相关的有 打开文件,根据fileencodings设置项检测文件编码,并设置fileencoding为对应编码 根据encoding设置项,将文件二进制进行编码转换,存储到内存中 根据termencoding设置项,将内存中的二进制转化为对应编码,传输给terminal terminal根据自身的编码设置,将收到的数据解码并显示 可见vim在打开文件并显示的过程中有大量的编码转化操作,将二进制从编码A转化为编码B的步骤为 字节流 -> decode(A) -> encode(B) -> 字节流 最终输出仍旧为字节流,如果A和B不同,则输出字节流和输入就不一样(ascii字节流除外,在所有编码方案里ascii字符对应的字节流都是一样的)。转换成功的前提是,decode所采用的编码方案必须和输入字节流编码方案一致,也就是说如果输入字节流是采用C编码方案生成的,采用A编码方案去解码就会失败 如果vim的某些编码项没有设置,会使用其依赖项的设置或默认设置,依赖关系如下 termencoding -> encoding -> shell vim的这些编码设置项里通常我们只设置fileencodings和encoding,如果只在中英文环境下使用,可设置如下 set fileencodings=utf8,gbk set encoding=utf8 encoding一定要设置utf8,因为utf8可以表示所有字符 terminal编码和vim的encoding编码不一致会出现什么情况? 假设terminal编码设置为gbk,vim的encoding为utf8,此时我们打开一个文件,不管这个文件是utf8还是gbk编码,它都无法正常显示 前面提到,vim的termencoding默认会继承encoding的设置,对应前面打开文件的步骤如下 打开文件,检测文件编码。这里不管是utf8还是gbk都没有影响 将文件内容转化为utf8格式,存储到内存中。这一步由于知道了文件的原始编码,因此转换不会出错 将内存数据转化为termencoding对应的编码,传输给terminal。由于termencoding继承自encoding设置,因此这一步实际上不需要做编码转换 terminal按gbk解码。问题出在这一步,由于terminal不知道vim传过来的数据是什么编码,它会直接按照自己的编码设置进行解码,编码不一致导致出错 如果要正常显示,只需要临时修改vim的termencoding编码和terminal编码一致即可,termencoding只涉及到显示,不涉及文件内容的改变,切勿修改encoding项,准确来说,在任何时候都不要试图修改encoding设置 因此我们的第二个要点是 vim的termencoding(继承自encoding)必须和terminal编码设置一致 修改文件 如果说打开文件的数据流是从vim到terminal,那修改文件则是从terminal到vim再到terminal这么一个来回 terminal -> vim -> terminal 和编码相关的步骤如下,打开文件显示的过程前面已经描述过,这里只说修改和保存的过程 在terminal输入字符,根据terminal编码方案进行编码,传输给vim vim收到二进制数据,将数据由termencoding编码转换为encoding编码并保存在内存中 保存文件时,将数据从encoding编码转为fileencoding编码,若fileencoding为空,则直接以encoding方案保存 fileencoding有两种情况 打开空文件,fileencoding默认为空 打开已经存在的文件,fileencoding是根据fileencodings中的编码列表匹配到的编码方案,若都没匹配上,则为空 由上可见,encoding方案编码的数据在vim中是一个中转站,接收数据时(从文件读取或从终端输入)都要转化为encoding编码方案,保存文件时再由encoding编码方案转化为fileencoding编码方案。因此encoding必须设定为一个能表示所有字符的编码方案,通常我们设置为utf8 vim的encoding编码设置和terminal编码设置不同,如何正常输入文字 假设terminal和shell的编码设置均为gbk,vim的encoding设置为utf8,如果想正常输入和显示字符,必须将termencoding设置和terminal编码一致,这是不管是显示字符还是输入字符保存文件,都可以正常工作 我们可以设置编码不一致只是为了演示编码的影响,在实际环境中,必须保证这些编码设置都一致,因此终极要点是 terminal编码,shell编码,以及vim的encoding均设置为utf8 vim编码配置终极方案 terminal编码设置为utf8 shell编码设置为utf8, export LC_ALL=zh_CN.UTF-8 vim设置encoding为utf8, set encoding=utf-8 vim设置fileencodings, set fileencodings=utf-8, gbk","tags":"Tool","title":"深入理解vim编码设置"},{"url":"https://chukeer.github.io/C++数据库操作之SOCI.html","text":"SOCI 是一个数据库操作的库,并不是ORM库,它仍旧需要用户编写sql语句来操作数据库,只是使用起来会更加方便,主要有以下几个特点 以stream方式输入sql语句 通过into和use语法传递和解析参数 支持连接池,线程安全 由此可见它只是一个轻量级的封装,因此也有更大的灵活性,后端支持oracle,mysql等,后续示例均基于mysql 安装 git项目地址 https://github.com/SOCI/soci 推荐使用cmake编译 git clone https://github.com/SOCI/soci.git cd soci mkdir build cd build cmake .. -G \"Unix Makefiles\" -DCMAKE_INSTALL_PREFIX = /opt/third_party/soci make sudo make install 基本查询 假设有如下表单 CREATE TABLE ` Person ` ( ` id ` int ( 11 ) unsigned NOT NULL AUTO_INCREMENT , ` first_name ` varchar ( 64 ) NOT NULL DEFAULT '' , ` second_name ` varchar ( 64 ) NOT NULL DEFAULT '' , PRIMARY KEY ( ` id ` ) ) ENGINE = InnoDB DEFAULT CHARSET = utf8 ; 初始化session using namespace soci ; session sql ( \"mysql\" , \"dbname=test user=your_name password=123456\" ); 第一个参数为使用的后端数据库类型,第二个参数为数据库连接参数,可以指定的参数包括 host port dbname user passowrd 等,以空格分隔 insert string first_name = \"Steve\" ; string last_name = \"Jobs\" ; sql << \"insert into Person(first_name, last_name)\" \" values(:first_name, :last_name)\" , use ( first_name ), use ( last_name ); long id ; sql . get_last_insert_id ( \"Person\" , id ) 通过流的方式传递sql语句,用use语法传递参数 其中 Person(first_name, last_name) 为数据库table名和column名, values(:first_name, :last_name) 里的为参数的占位符,这里可以随便书写, get_last_insert_id 函数可以获取自增长字段的返回值 需要注意的是 use 函数里的参数的生命周期,切记不能将函数返回值作为 use 函数的参数 select int id = 1 ; string first_name ; string last_name ; sql << \"select first_name, last_name from Person where id=:id \" , use ( id ), into ( first_name ), into ( last_name ); if ( ! sql . got_data ()) { cout << \"no record\" << endl ; } 这里根据id字段查询first_name和last_name两个字段,并通过 into 函数将数据复制给变量, got_data() 方法可判断是否有数据返回 当id为整数时,sql语句也可以写作 sql << \"select balabala from Person where id=\" << id ,但当id为字符串时这样写会报错,因此建议都采用 use 函数 如果查询结果是多行数据,则需要使用rowset类型并自己提取 rowset < row > rs = ( sql . prepare << \"select * from Person\" ); for ( rowset < row >:: iterator it = rs . begin (); it != rs . end (); ++ it ) { const row & row = * it ; cout << \"id:\" << row . get < long long > ( 0 ) << \" first_name:\" << row . get < string > ( 1 ) << \" last_name:\" << row . get < string > ( 2 ) << endl ; } 这里get模版的参数类型必需和数据库类型一一对应,varchar和text类型对应string,整数类型按如下关系对应 数据库类型 soci类型 SMALLINT int MEDIUMINT int INT long long BIGINT unsigned long long update int id = 1 ; string first_name = \"hello\" ; string last_name = \"world\" ; sql << \"update Person set first_name=:first_name, last_name=:last_name\" \" where id=:id\" , use ( first_name ), use ( last_name ), use ( id ); delete int id = 1 ; sql << \"delete from Person where id=:id\" , use ( id ); 有时候我们需要关注delete操作是否真的删除了数据,mysql本身也会返回操作影响的行数,可以采用如下方法获取 statement st = ( sql . prepare << \"delete from Person where id=:id\" , use ( id )); st . execute ( true ); int affected_rows = st . get_affected_rows (); 使用连接池 使用连接池可以解决多线程的问题,每个线程在操作数据库时先从连接池取出一个session,这个session会被设置为锁定,用完之后再换回去,设置为解锁,这样不同线程使用不同session,互不影响。session对象可以用连接池来构造,构造时自动锁定,析构时自动解锁 int g_pool_size = 3 ; connection_pool g_pool ( g_pool_size ); for ( int i = 0 ; i < g_pool_size ; ++ i ) { session & sql = g_pool . at ( i ); sql . open ( \"mysql\" , \"dbname=test user=zhangmenghan password=123456\" ); } session sql ( g_pool ); sql << \"select * from Person\" ; 此时 session sql(g_pool) 的调用是没有超时时间的,如果没有可用的session,会一直阻塞,如果要设置超时时间,可以采用connection_pool的底层接口 session & at ( std :: size_t pos ); bool try_lease ( std :: size_t & pos , int timeout ); void give_back ( std :: size_t pos ); 调用方式如下 size_t pos if ( ! try_lease ( pos , 3000 )) // 锁定session,设置超时为3秒 { return ; } session & sql = g_pool . at ( pos ) // 获取session,此时pos对应的session已被锁定 /* sql操作 ... */ g_pool . give_back ( pos ); // 解锁pos对应的session 需要注意的是,如果 try_lease 调用成功后没有调用 give_back ,会一直锁定对应的session,因此 try_lease 和 give_back 必需成对使用 事务 session对象提供了对事务的操作方法 void begin (); void commit (); void rollback (); 同时也提供了封装好的transaction对象,使用方式如下 { transaction tr ( sql ); sql << \"insert into ...\" ; sql << \"more sql queries ...\" ; // ... tr . commit (); } 如果commit没有被执行,则transaction对象在析构时会自动调用session对象的rollback方法 ORM soci可以通过自定义对象转换方式从而在use和into语法中直接使用用户对象 比如针对Person表单我们定义如下结构和转换函数 struct Person { uint32_t id ; string first_name ; string last_name ; } namespace soci { template <> struct type_conversion < Person > { typedef values base_type ; static void from_base ( const values & v , indicator ind , Person & person ) { person . id = v . get < long long > ( \"id\" ); person . first_name = v . get < string > ( \"first_name\" ); person . last_name = v . get < string > ( \"last_name\" ); } static void to_base ( const Person & person , values & v , indicator & ind ) { v . set ( \"id\" , ( long long ) person . id ); v . set ( \"first_name\" , person . first_name ); v . set ( \"last_name\" , person . last_name ); } }; } 需要注意的是这里get模板的参数类型必需和数据库字段对应,对应关系见之前select的示例,对于整数类型,在set时最好也加上强转并且和get一致,否则可能会抛异常 std::bad_cast 。get和set函数的第一个参数是占位符,占位符的名字不一定要和数据库column名一致,但后续操作中 values 语法里的占位符必需和这里指定的一致 定义了 type_conversion 之后,后续在用到use和into语法时可直接使用Person对象,这时soci会根据占位符操作指定字段 insert Person person ; person . first_name = \"Steve\" ; person . last_name = \"Jobs\" ; sql << \"insert into Person(first_name, last_name)\" \" values(:first_name, :last_name)\" , use ( person ); select int id = 1 ; Person person ; sql << \"select * from Person where id=:id\" , use ( id ), into ( person ); rowset < Person > rs = ( sql . prepare << \"select * from Person\" ); for ( rowset < Person >:: iterator it = rs . begin (); it != rs . end (); ++ it ) { const Person & person = * it ; // do something with person } update person . id = 1 ; person . first_name = \"hello\" ; person . last_name = \"world\" ; sql << \"update Person set first_name=:first_name, last_name=:last_name\" \" where id=:id\" , use ( person ); delete Person person ; person . id = 1 ; sql << \"delete from Person where id=:id\" , use ( person ); 完整示例 https://github.com/handy1989/soci_test","tags":"Language","title":"C++数据库操作之SOCI"},{"url":"https://chukeer.github.io/详解Linux shell命令帮助格式.html","text":"linux shell命令通常可以通过 -h 或 --help 来打印帮助说明,或者通过man命令来查看帮助,有时候我们也会给自己的程序写简单的帮助说明,其实帮助说明格式是有规律可循的 帮助示例 下面是 git reset 命令的帮助说明,通过 man git-reset 可以查看 git reset [-q] [<tree-ish>] [--] <paths>... git reset (--patch | -p) [<tree-ish>] [--] [<paths>...] git reset [--soft | --mixed | --hard | --merge | --keep] [-q] [<commit>] 对于命令和参数大致有如下几种类型 没有任何修饰符参数 : 原生参数 <> : 占位参数 [] : 可选组合 () : 必选组合 | : 互斥参数 ... : 可重复指定前一个参数 -- : 标记后续参数类型 下面来一一介绍 参数类型解读 原生参数 说明文档里的字符即为命令需要使用的字符,比如以上命令的 git reset 这种参数在使用时必需指定,且和说明文档里的一致 占位参数 表示方式: <> 和原生参数类似,都是必需指定的,只不过占位参数的实际字符是在使用时指定的,同时为了方便阅读会用一个描述词汇来表示,并以 <> 包围,比如 <paths> 表示路径,使用时可以指定为具体的路径,而paths只是起一个说明作用,有些帮助说明里也会用大写来表示占位参数,比如将以上参数说明写成 PATHS 可选组合 表示方式: [] 括号里的参数为可选参数,比如usage第二个里面的 [-q] ,则 -q 为可选参数 可选项和占位参数也可以同时使用,如 [<commit>] 表示该参数可以指定某次提交,也可以不指定 必选组合 表示方式: () 括号里的参数必需指定,通常里面会是一些互斥参数,比如 (--patch | -p) 表示 --patch 和 -p 这两个参数必需指定一个 互斥参数 表示方式: | 互斥参数一般都在 () 和 [] 里,表示该参数只能指定其中一个,比如 [--mixed | --soft | --hard | --merge | --keep] 重复参数 表示方式: ... 表示前一个参数可以被指定多个,比如 <paths>... <paths> 是一个占位参数,使用时必需指定为路径, ... 并表示可以指定多个路径。重复参数的一个典型使用场景就是移动文件,将多个文件移动到一个目录下,比如如下命令 git mv [<options>] <source>... <destination> 我们可以这样使用 git mv -f a.cpp b.py dir 此时options对应为 -f 参数,source对应为 a.cpp b.py ,destination对应为 dir 标记后续参数类型 表示方式: -- 表示后续参数的某种类型,比如这里如果使用如下命令 git reset -p -- xx 对比第一个命令,这里的xx对应的应该是 <paths> 参数,当我们指定 -- 之后,则git会认为xx就是一个路径,那怕它是特殊符号或者路径并不存在。这是shell命令的一个通用方式,比如我们有一个文件名为 -h ,如果想删除这个文件,执行 rm -h 肯定是无法删除的,因为这时-h会被认为是rm的一个参数选项,应该使用 rm -- -h 这时shell会将-h解释为一个文件名传递给rm命令 解读实战 最后来解释一个比较复杂的帮助说明 git cat-file (-t [--allow-unknown-type]|-s [--allow-unknown-type]|-e|-p|<type>|--textconv) <object> 该命令参数由四个部分,其中 git 和 cat-file 为原生参数, () 里的为可选组合, <object> 为占位参数 组合又由6部分组成,为互斥关系 -t [--allow-unknown-type] -s [--allow-unknown-type] -e -p <type> --textconv 因此该命令的帮助说明可以拆分如下 git cat-file -t <object> git cat-file -t --allow-unknown-type <object> git cat-file -s <object> git cat-file -s --allow-unknown-type <object> git cat-file -e <object> git cat-file -p <object> git cat-file <type> <object> git cat-file --textconv <object>","tags":"Skill","title":"详解Linux shell命令帮助格式"},{"url":"https://chukeer.github.io/gtest.html","text":"安装 项目地址: https://github.com/google/googletest git clone https://github.com/google/googletest cd googletest/googletest mkdir build cd build cmake .. -DCMAKE_INSTALL_PREFIX = /usr/local make sudo make install 其中 -DCMAKE_INSTALL_PREFIX 指定的是安装目录,这里安装到了/usr/local目录,后续编译测试代码均使用此目录下的gtest库 使用 检查结果 检查结果可用 EXPECT_ 和 ASSERT_ 两组宏,前者如果验证失败会继续执行,后者会退出当前测试用例,但仍旧会执行后续的测试用例,两组宏的使用方式完全一致,下面列出 EXPECT_ 宏的使用方式 EXPECT_TRUE(condition); EXPECT_FALSE(condition); EXPECT_EQ(val1,val2); EXPECT_NE(val1,val2); EXPECT_LT(val1,val2); EXPECT_LE(val1,val2); EXPECT_GT(val1,val2); EXPECT_GE(val1,val2); EXPECT_STREQ(str1,str_2); EXPECT_STRNE(str1,str2); EXPECT_STRCASEEQ(str1,str2); EXPECT_STRCASENE(str1,str2); 顾名思义,就不多解释了 简单用例 使用TEST宏,每个宏定义一个测试用例,宏的两个参数分别代表测试类名和测试名,可随意定义 // filename: test1.cpp #include \"gtest/gtest.h\" int Add ( int x , int y ) { return x + y ; } TEST ( TestClass , TestName1 ) { EXPECT_EQ ( 2 , Add ( 1 , 1 )) << \"Add error\" ; } TEST ( TestClass , TestName2 ) { ASSERT_NE ( 3 , Add ( 2 , 3 )); } int main ( int argc , char ** argv ) { testing :: InitGoogleTest ( & argc , argv ); return RUN_ALL_TESTS (); } EXPECT_EQ(2, Add(1, 1)) << \"Add error\"; 表示在EXPECT_EQ宏比较失败时会打印后面的错误信息 编译 g++ test1.cpp -I/usr/local/include -L/usr/local/lib -lgtest -lpthread -o test1 注意编译时需指定pthread库 运行 ./test1 -h 查看参数说明 ./test1 --gtest_list_tests 查看用例 ./test1 运行所有用例 ./test1 --gtest_filter=TestClass.Testname1 运行指定用例 ./test1 --gtest_filter='TestClass.*' 使用通配符 ./test1 --gtest_filter=-TestClass.Testname1 排除指定用例 共享成员变量 使用TEST_F宏,宏参数含义和TEST一样,区别是第一个参数必须是已定义的类,每个类对应一组测试用例,可以选择定义如下几组函数 SetUp和TearDown, 在每个测试用例调用前和调用后执行 SetUpTestCase和TearDownTestCase,在每组测试用例调用前和调用后执行,必须为static void类型 亦可定义成员变量在该组例间共享,以上定义都必须是public或protected类型 #include <stdio.h> #include <vector> #include \"gtest/gtest.h\" using std :: vector ; class TestFixture : public testing :: Test { protected : static void SetUpTestCase () { printf ( \"SetUpTestCase \\n \" ); } // static void TearDownTestCase() {} virtual void SetUp () { v1_ . push_back ( 1 ); printf ( \"SetUp \\n \" ); } //virtual void TearDown() {} vector < int > v1_ ; vector < int > v2_ ; }; TEST_F ( TestFixture , TestName1 ) { EXPECT_EQ ( 1 , v1_ . size ()) << \"v1_.size() error\" ; } TEST_F ( TestFixture , TestName2 ) { EXPECT_EQ ( 0 , v2_ . size ()); } int main ( int argc , char ** argv ) { testing :: InitGoogleTest ( & argc , argv ); return RUN_ALL_TESTS (); } 全局事件 全局事件可以在所有测试用例运行前后执行 #include <stdio.h> #include \"gtest/gtest.h\" using std :: vector ; class FooEnvironment : public testing :: Environment { public : virtual void SetUp () { printf ( \"Foo FooEnvironment SetUp \\n \" ); } virtual void TearDown () { printf ( \"Foo FooEnvironment TearDown \\n \" ); } }; class TestFixture : public testing :: Test { protected : static void SetUpTestCase () { printf ( \"SetUpTestCase \\n \" ); } // static void TearDownTestCase() {} virtual void SetUp () { v1_ . push_back ( 1 ); printf ( \"SetUp \\n \" ); } //virtual void TearDown() {} vector < int > v1_ ; vector < int > v2_ ; }; TEST_F ( TestFixture , TestName1 ) { EXPECT_EQ ( 1 , v1_ . size ()) << \"v1_.size() error\" ; } TEST_F ( TestFixture , TestName2 ) { EXPECT_EQ ( 0 , v2_ . size ()); } TEST ( TestClass , TestName1 ) { EXPECT_EQ ( 1 , 1 ); } int main ( int argc , char ** argv ) { testing :: AddGlobalTestEnvironment ( new FooEnvironment ); testing :: InitGoogleTest ( & argc , argv ); return RUN_ALL_TESTS (); } 定义一个类 FooEnvironment 继承自 testing::Environment ,并定义 SetUp 和 TeawDown 成员函数函数,在main函数里调用 testing::AddGlobalTestEnvironment(new FooEnvironment) 即可","tags":"Tool","title":"gtest"},{"url":"https://chukeer.github.io/Unix IPC概述.html","text":"IPC全称Inter-Process Communication,即进程间通信。我们知道一个进程可以有多个线程,他们可以共享进程的全部资源,比如打开的文件句柄,创建的全局变量等,因此线程间通信相对就容易一些,而不同进程拥有独立的虚拟地址空间,他们之间想要通信就需要特定的IPC方法。我们只讨论同一主机上的进程间通信,socket通信是广义上的进程间通信 这里主要介绍管道,FIFO,消息队列三种IPC通信机制 管道 管道是一种单向通信的数据通道,其表现为一对文件句柄,一个写入端和一个读取端,模型如下 即用户空间创建管道,得到两个文件句柄,往fd[1]写数据,从fd[0]读数据,虽然管道是由单个进程创建,但很少在单个进程内使用,最常用的是父子进程之间进行通信,如下 首先父进程创建管道后调用fork生成子进程,子进程继承了父进程的管道句柄,接着父进程关闭管道的读取端,子进程关闭管道的写入端,这样父子进程之间就生成了一个单向数据流,如下 这样父进程往fd[1]写数据,子进程就能从fd[0]读到 管道最常用的场景是unix shell中,比如以下命令 who | sort | head -1 创建了两个管道,还把每个管道的读取端复制到相应进程的标准输入,把写入端复制到相应进程的标准输出,其数据流通如下 说了这么多模型,最后来看一下创建管道的API int pipe(int pipefd[2]); 创建两个句柄,pipefd[0]是读取端,pipefd[1]是写入端,切勿弄反,句柄操作和普通文件句柄操作一样,通过close来关闭,用read和write来读写 FIFO 管道没有名字,它必须由一个进程创建,只能由进程自己和它fork出的进程使用,对于没有亲缘关系的进程则不能使用,FIFO又称命名管道,可以在任意进程间使用 先来看创建fifo的API int mkfifo(const char *pathname, mode_t mode); pathname为文件路径,mode为文件权限,和open的含义一样,返回为文件句柄。也就是说FIFO必须和文件名绑定,并且在Linux上FIFO本身就是一种文件,我们可以通过mkfifo的命令创建FIFO如下 $ mkfifo fifo && ls -li fifo 1187053 prw-rw-r-- 1 zhangmenghan zhangmenghan 0 12月 6 22:14 fifo 可以看到文件fifo的类型为p,代表的是管道,并且管道文件也是占用一个inode的。既然是文件,那就可以通过write/read/close/unlink这一系列文件API来操作了,不同点在于,对管道和FIFO的write总是往末尾添加数据,read则总是从开头返回数据,如果对管道或FIFO进行lseek操作,会返回ESPIPE错误 FIFO的读写模型和管道类似,只不过管道返回的是两个句柄,一个写一个读,而FIFO只有一个句柄,其读写属性是在open时指定的,并且它们都可以通过非阻塞方式进行IO操作,只需对句柄设置O_NONBLOCK即可,可通过fcntl函数设置 管道和FIFO的读写还具有以下特性 往一个空管道或FIFO读取数据,默认会阻塞直到有数据写入,若设置了O_NONBLOCK则返回EAGAIN错误 如果请求读取的数据多余管道或FIFO中的数据,那么只返回这些可用的数据 如果请求写入字节数小于等于PIPE_BUF,那么write操作保证是原子的,比如两个进程同时请求写同一个管道或FIFO,要么第一个进程先写完要么反之,不会导致数据交错,如果请求写入字节数大于PIPE_BUF则不能保证原子性,具体的PIPE_BUF值和操作系统相关,Posix.1要求PIPE_BUF至少为512字节,通过 long pathconf(char *path, int name); 可查看该值,其中name指定为 _PC_PIPE_BUF 如果设置了O_NONBLOCK且待写入字节数小于等于PIPE_BUF 如果管道或FIFO剩余空间足够,那么所有数据都写入 如果管道或FIFO剩余空间不够,那么立即返回EAGAIN错误。因为此时要保证原子性,所以不会只写入部分数据 如果设置了O_NONBLOCK且待写入字节数大于PIPE_BUF 如果管道或FIFO剩余空间足够,则所有数据都写入,否则值写入剩余字节数 如果管道或FIFO已满,则返回EAGAIN错误 当关闭管道或FIFO,里面的数据会被丢弃 消息队列 管道和FIFO都是面向字节流的通信,也就是说读取方并不知道数据的边界,如果写入方将一组数据写入管道或FIFO,读取方必须知道这组数据的实际长度和格式才能准确将数据读取并解析出来,消息队列提供了一种面相消息的通信方式 消息队列可以被认为是一个消息链表,有写权限的进程往链表放置消息,有读权限的进程从里面取消息。写入者可以随时往消息队列放置消息,而不用管此时是否有读取者,这和管道以及FIFO不同,消息队列有随内核的持续性,即便读写进程退出,消息队列仍然存在 unix上的消息队列实现有两种,Posix消息队列和System V消息队列,这两者都应用的比较广泛,System V消息队列诞生的更早,后来的Posix消息队列加入了一些新特性,因此也被一些新开发的程序所使用,两者提供的API有很多相似性,也有如下一些差别 对Posix消息队列的读总是返回最高优先级的最早消息,对System V消息队列的读则可以返回任意指定优先级的消息 当往一个空队列放置消息时,Posix消息队列允许产生一个信号或启动一个线程,System V消息队列则不提供类似极致 消息队列中的每个消息都具有如下属性 一个无符号整数优先级(Posix)或一个长整形类型(System V) 消息的数据部分长度(可以为0) 数据本身(如果长度大于0) 对消息队列的操作也基本类似,接下来分别介绍两种消息队列的API Posix 创建/打开消息队列 mqd_t mq_open(const char *name, int oflag); mqd_t mq_open(const char *name, int oflag, mode_t mode, struct mq_attr *attr); 其中name是消息队列名字,其格式必须符合文件系统路径名,但并不要求是真实存在的文件,oflag是O_RANDLY,O_WRONLY或O_RDWR之一,还可以按位或上O_CREATE,O_EXCL,O_NONBLOCK之一,这和文件API open的参数类似,如果是打开已存在消息队列,这两个参数就够了,如果是新建消息队列,则需要带上mode和atrr参数,mode也和open参数一样是指定读写权限,attr可以设置消息队列属性,如果为NULL则使用默认属性 其返回值为消息队列句柄,作用和文件句柄类似,在消息队列的其它API的第一个参数中都会用到 设置/获取属性 int mq_getattr(mqd_t mqdes, struct mq_attr *attr); int mq_setattr(mqd_t mqdes, struct mq_attr *newattr, struct mq_attr *oldattr); 其属性结构如下 struct mq_attr { long mq_flags; /* Flags: 0 or O_NONBLOCK */ long mq_maxmsg; /* Max. # of messages on queue */ long mq_msgsize; /* Max. message size (bytes) */ long mq_curmsgs; /* # of messages currently in queue */ }; 有两个值比较关键,分别是最大消息数(mq_maxmsg)和单个消息最大长度(mq_msgsize) 发送/接收消息 int mq_send(mqd_t mqdes, const char *msg_ptr, size_t msg_len, unsigned msg_prio); ssize_t mq_receive(mqd_t mqdes, char *msg_ptr, size_t msg_len, unsigned *msg_prio); 前三个参数和write/read类似,最后一个参数代表消息优先级 其中mq_send返回0代表成功,其它代表失败,这一点和write不同。mq_receive返回值为实际读取的字节数 消息通知 int mq_notify(mqd_t mqdes, const struct sigevent *sevp); 当往一个空消息队列放置消息时,Posix消息队列可以发送一个通知,这个通知就是会向接收进程发送一个信号。这是System V消息队列所不具备的 mq_notify就是接收者用来注册或反注册通知信号的,若sevp非空则代表注册,若sevp为空则代表反注册。这种通知机制还有以下特点 任一时刻只有一个进程可以被注册为接收某个指定队列的通知 如果接受者阻塞在mq_received中,通知不会发出 当通知信号发给注册进程,其注册即被撤销,该进程必须再次注册(如果想要的话) 关闭/删除消息队列 int mq_close(mqd_t mqdes); int mq_unlink(const char *name) 功能和文件API的close,unlink类似 System V 创建/打开消息队列 int msgget(key_t key, int msgflg) 其返回值是一个整形标识符,用来唯一表示消息队列,用在其它msg函数的第一个参数中。它是基于指定的key产生的,key既可以是ftok的返回值也可以是IPC_PRIVATE 以下情况会创建新的消息队列 key指定为IPC_PRIVATE key对应的消息队列不存在且msgflag指定了IPC_CREAT oflag和open函数的mode参数类似,同时还可以或上IPC_CREAT和IPC_EXCL,其含义和O_CREATE,O_EXCL类似 发送消息 int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg); 其中msqid为消息标识符,msgp为指向消息结构的指针,消息结构具有如下模板 struct msgbuf { long mtype; /* message type, must be > 0 */ char mtext[1]; /* message data */ }; mtype为消息类型,必须大于0,mtext为消息数据,它可以是任何类型的数据,不管是二进制还是文本,其大小由msgsz指定,也就是说参数中msgsz的大小是msgbuf结构中除mtype之外的大小,比如我们可以定义自己的消息结构 struct my_msgbuf { long mtype; short myshort; char mydata[1024]; }; 此时msgsz的大小为 sizeof(struct my_msgbuf) - sizeof(long) msgflag可以指定为0,也可以指定为IPC_NOWAIT,当指定了IPC_NOWAIT时,若消息无法发送出去,msgsnd函数会立马返回EAGAIN错误,否则会一直阻塞直到发送成功或消息队列被删除 接收消息 ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg); msgp,msgsz,msgflg的含义和msgsnd函数一样,msgtyp代表希望读取的消息类型 msgtyp等于0,返回消息队列中第一个消息 msgtyp大于0,返回消息队列中类型值为msgtyp的第一个消息 msgtyp小于0,返回消息队列中类型值小于等于msgtyp绝对值的消息中类型值最小的第一个消息 比如消息队列中有如下消息,第一个消息类型值为2,第二个为1,第三个为3,第4个为1,当msgtyp为0时,返回第一个消息,当msgtyp为3时返回第三个消息,当msgtyp为-3时返回第二个消息 修改消息队列 int msgctl(int msqid, int cmd, struct msqid_ds *buf) 该函数提供在一个消息队列上的各种控制操作,cmd可以指定如下三个命令 IPC_RIMD 从系统中删除msqid指定的消息,此时第三个参数被忽略 IPC_SET 设置消息队列msqid_ds结构中的以下4个成员: msg_qbytes, msg_perm.uid, msg_perm.gid, msg_perm.mode ,其它成员不会被修改 IPC_STAT 获取消息队列的msqid_ds结构 命令行操作IPC对象 以上介绍了常用的IPC通信方式,并且介绍了相应的API,和文件操作类似,操作系统还提供了通过shell命令行操作IPC对象的方式,主要有mkfifo,ipcmk,ipcs,ipcrm等,具体使用方式可以直接查看man手册,这里举几个例子说明一下 mkfifo 创建FIFO,创建完之后可以直接使用echo,cat,rm等命令,和操作文件类似 ipcmk -Q 创建消息队列 ipcs -q 查看消息队列 ipcrm -q msgid 删除msgid指定的消息队列","tags":"Skill","title":"Unix IPC概述"},{"url":"https://chukeer.github.io/linux shell发送邮件.html","text":"一封最简单的邮件 echo -e \"To: [email protected]\\nCC: [email protected]\\nFrom: handy<[email protected]>\\nSubject: test\\n\\nhello world\" | sendmail -t 看上去有点复杂,其实就是sendmail程序从标准输入读取邮件源码,-t参数表示从邮件源码提取收件人信息,然后发送到收件人的邮件服务器,我们稍做整理,将邮件源码保存在email.txt中如下 To : handy1989 @ qq . com CC : handy1989 @ qq . com From : handy < handy @ test . com > Subject : test hello world 将以上命令改为 cat email.txt | sendmail -t ,这样就一目了然了。收到的邮件信息如下 邮件的格式 从前面的邮件源码可以看到,邮件是和http类似的文本协议,由邮件头和邮件内容两部分组成,中间以空行分隔,邮件头每行对应一个字段,和http头类似,比如这里的To,CC,From,Subject,分别代表收件人,抄送人,发件人,标题,如果有多个收件人或抄送人,用逗号分隔,邮件内容才是我们在邮件客户端真正看到的东西 邮件客户端都可以查看邮件源码,比如下面就是我收到的一封邮件的源码 邮件标题使用中文 如果邮件标题直接使用中文字符会导致收到的邮件乱码,为了避免这种情况,应该对中文进行base64编码,而这也是邮件最常用的编码方式,当然,在进行base64编码之前先得对中文字符进行编码(UTF-8或GBK等等),这和html的编码是一样的概念,采用UTF-8和base64编码的格式如下 =?UTF-8?B?xxxxxx?= 其中 xxxxxx 为编码后的数据,用python可以快速对中文进行编码,比如对中文'测试'先进行utf-8编码再进行base64编码结果为 >>> import base64 >>> base64 . standard_b64encode ( u'测试' . encode ( 'utf-8' )) '5rWL6K+V' 在From和Subject中使用中文,邮件源码如下 To : handy1989 @ qq . com CC : handy1989 @ qq . com From : =? UTF - 8 ? B ? 5 rWL6K + V ?=< handy @ test . com > Subject : =? UTF - 8 ? B ? 5 rWL6K + V ?= hello world 这里将发件人的名字和邮件标题都改为了'测试',收到的邮件效果为 邮件内容使用html 如果邮件内容是html代码,则需要在邮件头添加Content-type字段来标记文本类型,同时还需要标记邮件内容的字符编码,以下邮件源码发送的正是html内容 To: [email protected] CC: [email protected] From: =?UTF-8?B?5rWL6K+V?= <handy @test.com > Subject: =?UTF-8?B?5rWL6K+V?= Content-type: text/html;charset=utf-8 <h1> hello world </h1> 收到的邮件效果为","tags":"Skill","title":"linux shell发送邮件"},{"url":"https://chukeer.github.io/iTerm2固定标签名字.html","text":"iTerm2是Mac上最好用的终端app,没有之一。使用终端时往往需要连接不同的服务器,通常我们会为每个服务器连接设置一个profile 这样点击一个profile就可以快速连接服务器,但这时可能会出现另一个问题,当我们打开多个标签时,希望能通过标签名字区分每个连接,最好是以profile name来标记,而iTerm2默认会根据你运行的程序自动切换标签的名字,比如我分别连接了\"内网开发机55\"和\"内网开发机57\"这两个配置,结果显示出来的是这样 如果要固定标签名字,有如下两个方式 临时修改 修改session title, Edit->Edit Session(cmd+I) ,输入session title即可。这种方式只是临时修改了标签名字 永久性修改 先确保 Preference->Appearance->Show profile name 已经勾选上 再打开 Preference->Profiles ,选中你要设置的profile,点击右边的Terminal标签,将 Terminal may set tab/window name 前的勾取消掉 这时再重新链接,就是这样的效果了","tags":"Skill","title":"iTerm2固定标签名字"},{"url":"https://chukeer.github.io/linux终端输出带颜色字符.html","text":"格式 linux终端下输出带颜色的文字只需在文字前面添加如下格式 \\033[显示方式;前景色;背景色m 其中 \\033 是ESC健的八进制, \\033[ 即告诉终端后面是设置颜色的参数,显示方式,前景色,背景色均是数字 参数含义 显示方式 意义 0 终端默认设置 1 高亮显示 4 使用下划线 5 闪烁 7 反白显示 8 不可见 前景色 背景色 颜色 30 40 黑色 31 41 红色 32 42 绿色 33 43 黃色 34 44 蓝色 35 45 紫红色 36 46 青蓝色 37 47 白色 示例 可以将所有控制参数都用上,也可以只使用前景色或背景色 但有一点要注意,如果输出带颜色的字符后并没有恢复终端默认设置,后续的命令输出仍旧会采用之前的颜色,如果是在脚本中设置了颜色而未恢复,则整个脚本的输出都会采用之前的颜色,因此如果不希望影响后面文字的输出,最好是在输出带颜色的文字之后恢复终端默认设置,如下 如果只是想简单设置文字颜色,推荐如下方式 echo \"\\033[31m红色文字\\033[0m\" echo \"\\033[32m绿色文字\\033[0m\" echo \"\\033[33m黄色文字\\033[0m\"","tags":"Skill","title":"linux终端输出带颜色字符"},{"url":"https://chukeer.github.io/深入理解字符编码.html","text":"大概每个人在使用软件时都遇到过乱码的问题,这是由于字符的编码和解码方式不一致导致,我们知道计算机只认识二进制数据,因此程序在处理、存储、传输文本时,需要将文本转化成二进制,通俗的来讲,编码就是将字符转化为二进制序列,解码就是将二进制序列转化为字符 编码模型 先抛开ascii,gbk,unicode,utf-8这些概念,我们先来想想如果自己要在计算机中表示所有字符该怎么办 字符集 首先我们要确认需要表示的字符集合,字符集是一个抽象的概念,它与编码无关,比如英文字符,数字标点,中文字符,这些都可以称之为字符集 编码集 为了表示这些字符集,我们需要将字符集中的每个字符进行惟一标识,最简单的方式就是将字符映射到一个非负整数,这个整数集合就是编码集合,每个字符集都对应自己的一个编码集 编码方案 由于计算机处理的是二进制数居,为了方便存储和传输,我们要将编码集中的整数表示成二进制序列,有些整数用一个字节就可以表示,有些可能需要多个字节,这个转化方式就是编码方案 有了以上的概念,我们再来理解ascii,gbk,utf-8 ASCII 计算机最早由老外发明,因此最早的ASCII编码只考虑到了英文字母的表示, 字符集:英文字母、数字、标点、控制字符(回车,制表符等) 编码集:由于这些字符数量有限,它的编码集也很小,只需要0-127的整数就可以表示, 编码方案:存储这些编码也很简单,只需要一个字节,即将字符的编码直接转化为一个字节的二进制数据即可 比如字符A的编码值为65,二进制存储为01000001 gbk 中文博大精深,中文字符也远远多于英文字符,这样长度只有一个字节的ascii编码就无法表示数量庞大的中文字符,于是就有了gb系列的编码,其中gbk是gb系列编码的扩展 字符集:ascii字符+中文 编码集:每个字符用两个字节表示,编码集为0-65535(没有完全覆盖),理论上最多可以表示65536个字符,这可以表示绝大多数汉字 编码方案:ascii字符保持不变,用一个字节表示,中文字符用两个字节表示,第一字节的范围是81–FE,第二字节的一部分领域在40–7E,其他领域在80–FE 比如\"中\"编码值为54992,十六进制为0xD6D0。gbk兼容ascii编码,事实上所有编码都兼容ascii编码。另外微软的CP936编码被视为等同于gbk unicode 中文的编码是解决了,但是其它语言的编码怎么办呢,总不能每个国家都搞一套编码方案吧,而且在互联网时代,很多信息都是共享的,于是需要一种能表示所有字符的编码方案,unicode就是这样的 字符集:所有语言的所有字符 编码集:unicode是一个很大的集合,可以表示100多万个符号,最长可用4个字节表示一个符号 编码方案:unicode只是规定了每个符号的二进制表示,并没有规定如何存储 比如\"中\"的unicode编码为十六进制0x4E2D,需要用两个字节来表示,有些字符可能需要3个甚至4个字节来表示,如果都采用定长编码,就会造成存储空间的极大浪费,因为我们知道英文字符只需要一个字节就能表示,于是便有了对unicode的不同实现方案,目前最广泛使用的就是utf-8 utf-8 utf-8是对unicode的一种实现方案,是一种可变长字符编码,也就是说它先基于unicode编码将字符表示成一个二进制,然后采用一种方式去存储这串二进制,它的规则也很简单 对于单字节的符号,字节的第一位设为0,后面7位为这个符号的unicode码。因此对于英语字母,UTF-8编码和ASCII码是相同的 对于n字节的符号(n>1),第一个字节的前n位都设为1,第n+1位设为0,后面字节的前两位一律设为10,剩下的没有提及的二进制位,全部为这个符号的unicode码 具体编码规则如下 Unicode符号范围(十六进制) UTF-8编码方式(二进制) 0000 0000-0000 007F 0xxxxxxx 0000 0080-0000 07FF 110xxxxx 10xxxxxx 0000 0800-0000 FFFF 1110xxxx 10xxxxxx 10xxxxxx 0001 0000-0010 FFFF 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx 举个例子,字符A的unicode值为01000001,其utf-8编码仍旧为01000001,汉字\"中\"的unicode编码为0x4E2D(01001110 00101101),在第三行的范围内,需要用三个字节表示,其utf-8编码值格式为1110xxxx 10xxxxxx 10xxxxxx,其中x为从最右边开始填入的unicode编码二进制,不够的补0,得到11100100 10111000 10101101 utf-8有两个主要优点,第一是变长编码,在能表示足够大的字符集的前提下减少了存储空间,第二是采用前缀编码,即任何一个字符的编码都不是另一个字符编码的前缀,这样的好处是在网络传输过程中如果丢失了一个字节,可以判断出这个字节所在字符的编码边界,只会影响这个字符的显示,而不会影响其后面的字符,这不同于gbk这种非前缀编码,gbk每个汉字用两个字节编码,如果丢失一个字节,剩下的一个字节和后面字符的第一个字节可以组成一个新的字符,这样后续的所有字符都错乱了 字符编码的应用 有了以上编码的概念,我们可以分析一些工作中遇到的和编码有关的问题来进一步加深理解 浏览器设置显示编码 最常用的应该就是浏览器里设置编码格式,当浏览器加载网页后,网页文件在内存中的格式其实是对网页文件进行编码后的字节流,这个编码格式是由web服务器决定的,为了显示字符,需要对内存中的字节流进行解码,即浏览器要做的事是 decode(二进制字节流) -> 字符 假设该段内存是以utf-8进行编码,那么以utf-8的方式来解码就可以正常显示,而如果以gbk的方式来解码,就会显示乱码 python的unicode字符 在python中有两种类型的字符串,一种是str,一种是unicode str是面向字节的,存储的是已经编码后的二进制,unicode是面向字符的,它是一个抽象层的概念。最典型的差别就是,当我们计算长度时,str是计算字节的长度,而unicode是计算字符的长度 >>> len('中国') 6 >>> len(u'中国') 2 这里'中国'的二进制字节长度为6,是因为我的终端编码为utf-8,每个中文编码为3个字节 str和unicode可以互相转化,str到unicode是解码,unicode到str是编码。字节解码为字符,字符编码为字节 >>> '中国'.decode('utf-8') u'\\u4e2d\\u56fd' >>> u'中国'.encode('utf-8') '\\xe4\\xb8\\xad\\xe5\\x9b\\xbd' 编码解码都需要指定编码方式,解码时指定的编码方式必须和二进制数据的实际编码方式一致,这里因为我终端默认采用的是utf-8编码,所以用utf-8方式来解码,如果用gbk来解码就会报错 >>> '中国'.decode('gbk') Traceback (most recent call last): File \"<stdin>\", line 1, in <module> UnicodeDecodeError: 'gbk' codec can't decode bytes in position 2-3: illegal multibyte sequence unicode字符往往在程序内部逻辑使用,而需要存储或网络传输时,则需要将unicode字符编码成二进制字节流,如果我们直接将unicode字符直接写文件是会报错的,而写str类型数据则不会 >>> file = open('xx', 'w') >>> file.write('中国') >>> file.write(u'中国') Traceback (most recent call last): File \"<stdin>\", line 1, in <module> UnicodeEncodeError: 'ascii' codec can't encode characters in position 0-1: ordinal not in range(128) 这是因为unicode只是用来描述字符,而文件存储和传输的对象是二进制数据,所以需要将unicode字符编码才行,将unicode写文件时默认采用ASCII编码,中文的unicode编码值超出了ASCII编码的范围,故而报错。解决上面的问题有两个方式,一个是在write时将unicode字符进行编码,比如用utf-8或gbk,一个是打开文件时指定编码格式,使用 file = codecs.open('xx', 'w', 'utf-8') 即可 但是用sys.stdout.write将unicode字符写到标准输出,或者print一个unicode字符时却不会报错,这是因为python能通过 sys.stdout.encoding 获取到标准输出的编码格式,实际上在向标准输出写的过程中内部已经进行了转码 在不同编码的文件之间复制文本 我们用编辑器打开两个编码不同当文本,假设文本A的编码方式是A,文本B的编码方式是B,你会发现在文本A中复制的字符粘贴到文本B中,并不会出现乱码 编辑器加载一个文本,其实内部存储的是这个文本编码后的二进制数据,复制粘贴的操作实际上是二进制数据的交互,然后以对应的编码方式解码,一段以A方式编码的内存按B方式解码,显示出来的字符应该是乱码,但其实操作系统的剪贴板在中间做了转换,我们把剪贴板也看作一段内存区域,只不过它是固定以unicode方式编码的(utf-8或utf-16,根据操作系统而不同),之所以用unicode,是因为可以表示所有语言的字符,当我们复制A编码的文本时,会向剪贴板的内存中写入这段字符的unicode编码数据,当我们往B中复制的时候,再将这段unicode编码数据转换为以B方式编码的数据,所以文本B中不会出现乱码 windows下的clipspy或Mac下的clipboard viewer软件可以监视剪贴板的二进制数据 vim的编码 vim的使用环境比较复杂,可以在操作系统里以gui的形式打开,也可以在终端打开,还可以通过ssh远程打开,vim和编码相关的设置主要有以下几个 fileencodings 一个编码列表,打开文件时会根据这个列表来识别文件编码 fileencoding 保存文件时的编码格式,如果未指定,则为fileencodings中匹配的编码 encoding vim内部使用的编码,如buffer,menu等 termencoding 在终端环境下使用Vim时,通过termencoding项来告诉vim终端所使用的编码,默认和encoding一致 这里我们主要讨论前三个 文件读写过程中的编码转化过程如下 读取文件二进制数据,根据fileencodings指定的编码顺序依次匹配,若没有匹配成功,则显示乱码 文件已经解码为字符,根据encoding指定的编码将字符编码为二进制数据,编码结果保留在内存中 保存文件时,将内存中的二进制数据按encoding的方式解码,然后按照fileencoding的方式编码,编码结果写入文件 读是从文件二进制到内存二进制的转换,写是从内存二进制到文件二进制的转换,这其中都经历了一次解码和编码,如果encoding和fileencoding相同,这个过程其实就不需要做编解码的事了。内存相当于一个中转站,不管读写都要经过内存,如果我们改变fileencoding或encoding会出现什么问题呢 修改fileencoding 假设encoding为utf-8,文件为gbk编码,打开文件后内存数据保存的是文件二进制数据经过gbk解码,再经过utf-8编码后的结果,如果我们此时设置fileencoding为utf-8,然后保存,会将内存数据以utf-8解码,再按utf-8方式编码后写入文件(可能判断解码和编码格式一样而直接写文件,但其过程仍就可以用一次解码和编码来描述),其实这个过程就是转换了文本的编码格式,重新打开后文件字符还是一样的,只是存储的字节不一样了 修改encoding 假设encoding为utf-8,文件编码也为utf-8(假设为N个中文字符),打开文件后内存数据是utf-8编码后的结果(字节数为3N),这时将encoding改为gbk,然后保存,此时由于fileencoding为utf-8,所以会将内存数据先按照gbk的方式解码(3N/2个字符,这一步可能解码失败),再按照utf-8的方式编码(字节数为3N/2*3)后写入文件,这时文件字节数为4.5N,既不是utf-8(3N字节)编码也不是gbk(2N字节)编码,这样就乱码了,所以encoding不要轻易修改,一般设置为和操作系统编码一致 修改fileencodings 打开一个utf-8编码的中文文件,修改fileencodings为gbk,通过:e命令刷新缓冲区,这时发现字符全部乱码了,其实这个结果是将utf-8编码的二进制文件内容按gbk方式解码得到的字符,通俗来讲就是切换显示编码,和浏览器里切换编码格式的效果一样,只影响字符的显示,不影响存储,但是这时如果插入新文本则会以gbk方式编码,会导致原来的文本是utf-8,新增的文本是gbk,这样无论以哪种方式显示都会产生乱码","tags":"Skill","title":"深入理解字符编码"},{"url":"https://chukeer.github.io/Mac Alfred快速复制剪贴板和指定文本.html","text":"这里主要考虑如下两种需求 快速唤出剪贴板历史,并复制某一项 快速复制某一段固定内容的文本 第一项在Mac上有很多小工具实现,第二项在输入密码时经常会碰到,比如我在终端sudo执行命令或者连接redis数据库时需要输入密码,这些密码我又不想人肉记住,希望每次要输入时一个快捷键就能搞定,这个有点类似windows上xshell的快速命令集,点一个按钮就可以自动在终端上输入指定的文本,非常方便 这两个需求Alfred都可以轻松搞定 打开Alfred剪贴板功能 Alfred的剪贴板功能默认是关闭的,在preferences->Features->Clipboard->History的Clipboard History后面打勾即可打开,还可以选择剪贴板历史数据保存的时间 新建snip snip可以满足需求2,其实就是一个文本片段,并指定一个name和keyword,这样即可通过name或keyword搜索到文本片段,并快速复制粘贴 使用 可以通过默认快捷键option+cmd+C唤出剪贴板和snip界面 输入关键字可以搜索剪贴板和snip,回车之后会将内容直接粘贴到之前的app上。对于snip还有一个打开方式,先唤出Alfred搜索框,输入默认关键词snip和搜索内容即可对指定的snip进行复制和粘贴 这两种方式选中之后会将内容粘贴到之前的app上,也可以设置选中之后只复制而不粘贴,在preferences->Features->Clipboard->Advanced下去掉Pasting后面的勾即可 小结 之前在Mac上使用iTerm2时很羡慕windows上xshell的快速命令按钮,点一下就可以输入指定文本,现在看来,有了Alfred,Mac才真正称之为Mac","tags":"Skill","title":"Mac Alfred快速复制剪贴板和指定文本"},{"url":"https://chukeer.github.io/Linux文件读写机制及优化方式.html","text":"本文只讨论Linux下文件的读写机制,不涉及不同读取方式如read,fread,cin等的对比,这些读取方式本质上都是调用系统api read,只是做了不同封装。以下所有测试均使用open, read, write这一套系统api 缓存 缓存是用来减少高速设备访问低速设备所需平均时间的组件,文件读写涉及到计算机内存和磁盘,内存操作速度远远大于磁盘,如果每次调用read,write都去直接操作磁盘,一方面速度会被限制,一方面也会降低磁盘使用寿命,因此不管是对磁盘的读操作还是写操作,操作系统都会将数据缓存起来 Page Cache 页缓存(Page Cache)是位于内存和文件之间的缓冲区,它实际上也是一块内存区域,所有的文件IO(包括网络文件)都是直接和页缓存交互,操作系统通过一系列的数据结构,比如inode, address_space, struct page,实现将一个文件映射到页的级别,这些具体数据结构及之间的关系我们暂且不讨论,只需知道页缓存的存在以及它在文件IO中扮演着重要角色,很大一部分程度上,文件读写的优化就是对页缓存使用的优化 Dirty Page 页缓存对应文件中的一块区域,如果页缓存和对应的文件区域内容不一致,则该页缓存叫做脏页(Dirty Page)。对页缓存进行修改或者新建页缓存,只要没有刷磁盘,都会产生脏页 查看页缓存大小 linux上有两种方式查看页缓存大小,一种是free命令 $ free total used free shared buffers cached Mem: 20470840 1973416 18497424 164 270208 1202864 -/+ buffers/cache: 500344 19970496 Swap: 0 0 0 cached那一列就是页缓存大小,单位Byte 另一种是直接查看/proc/meminfo,这里我们只关注两个字段 Cached : 1202872 kB Dirty : 52 kB Cached是页缓存大小,Dirty是脏页大小 脏页回写参数 Linux有一些参数可以改变操作系统对脏页的回写行为 $ sysctl -a 2>/dev/null | grep dirty vm.dirty_background_ratio = 10 vm.dirty_background_bytes = 0 vm.dirty_ratio = 20 vm.dirty_bytes = 0 vm.dirty_writeback_centisecs = 500 vm.dirty_expire_centisecs = 3000 vm.dirty_background_ratio 是内存可以填充脏页的百分比,当脏页总大小达到这个比例后,系统后台进程就会开始将脏页刷磁盘(vm.dirty_background_bytes类似,只不过是通过字节数来设置) vm.dirty_ratio 是绝对的脏数据限制,内存里的脏数据百分比不能超过这个值。如果脏数据超过这个数量,新的IO请求将会被阻挡,直到脏数据被写进磁盘 vm.dirty_writeback_centisecs 指定多长时间做一次脏数据写回操作,单位为百分之一秒 vm.dirty_expire_centisecs 指定脏数据能存活的时间,单位为百分之一秒,比如这里设置为30秒,在操作系统进行写回操作时,如果脏数据在内存中超过30秒时,就会被写回磁盘 这些参数可以通过 sudo sysctl -w vm.dirty_background_ratio=5 这样的命令来修改,需要root权限,也可以在root用户下执行 echo 5 > /proc/sys/vm/dirty_background_ratio 来修改 文件读写流程 在有了页缓存和脏页的概念后,我们再来看文件的读写流程 读文件 用户发起read操作 操作系统查找页缓存 若未命中,则产生缺页异常,然后创建页缓存,并从磁盘读取相应页填充页缓存 若命中,则直接从页缓存返回要读取的内容 用户read调用完成 写文件 用户发起write操作 操作系统查找页缓存 若未命中,则产生缺页异常,然后创建页缓存,将用户传入的内容写入页缓存 若命中,则直接将用户传入的内容写入页缓存 用户write调用完成 页被修改后成为脏页,操作系统有两种机制将脏页写回磁盘 用户手动调用fsync() 由pdflush进程定时将脏页写回磁盘 页缓存和磁盘文件是有对应关系的,这种关系由操作系统维护,对页缓存的读写操作是在内核态完成,对用户来说是透明的 文件读写的优化思路 不同的优化方案适应于不同的使用场景,比如文件大小,读写频次等,这里我们不考虑修改系统参数的方案,修改系统参数总是有得有失,需要选择一个平衡点,这和业务相关度太高,比如是否要求数据的强一致性,是否容忍数据丢失等等。优化的思路有以下两个考虑点 最大化利用页缓存 减少系统api调用次数 第一点很容易理解,尽量让每次IO操作都命中页缓存,这比操作磁盘会快很多,第二点提到的系统api主要是read和write,由于系统调用会从用户态进入内核态,并且有些还伴随这内存数据的拷贝,因此在有些场景下减少系统调用也会提高性能 readahead readahead是一种非阻塞的系统调用,它会触发操作系统将文件内容预读到页缓存中,并且立马返回,函数原型如下 ssize_t readahead(int fd, off64_t offset, size_t count); 在通常情况下,调用readahead后立马调用read并不会提高读取速度,我们通常在批量读取或在读取之前一段时间调用readahead,假设如下场景,我们需要连续读取1000个1M的文件,有如下两个方案,伪代码如下 直接调用read函数 char* buf = (char*)malloc(10*1024*1024); for (int i = 0; i < 1000; ++i) { int fd = open_file(); int size = stat_file_size(); read(fd, buf, size); // do something with buf close(fd); } 先批量调用readahead再调用read int* fds = (int*)malloc(sizeof(int)*1000); int* fd_size = (int*)malloc(sizeof(int)*1000); for (int i = 0; i < 1000; ++i) { int fd = open_file(); int size = stat_file_size(); readahead(fd, 0, size); fds[i] = fd; fd_size[i] = size; } char* buf = (char*)malloc(10*1024*1024); for (int i = 0; i < 1000; ++i) { read(fds[i], buf, fd_size[i]); // do something with buf close(fds[i]); } 感兴趣的可以写代码实际测试一下,需要注意的是在测试前必须先回写脏页和清空页缓存,执行如下命令 sync && sudo sysctl -w vm.drop_caches=3 可通过查看/proc/meminfo中的Cached及Dirty项确认是否生效 通过测试发现,第二种方法比第一种读取速度大约提高10%-20%,这种场景下是批量执行readahead后立马执行read,优化空间有限,如果有一种场景可以在read之前一段时间调用readahead,那将大大提高read本身的读取速度 这种方案实际上是利用了操作系统的页缓存,即提前触发操作系统将文件读取到页缓存,并且操作系统对缺页处理、缓存命中、缓存淘汰都由一套完善的机制,虽然用户也可以针对自己的数据做缓存管理,但和直接使用页缓存比并没有多大差别,而且会增加维护代价 mmap mmap是一种内存映射文件的方法,即将一个文件或者其它对象映射到进程的地址空间,实现文件磁盘地址和进程虚拟地址空间中一段虚拟地址的一一对映关系,函数原型如下 void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset); 实现这样的映射关系后,进程就可以采用指针的方式读写操作这一段内存,而系统会自动回写脏页面到对应的文件磁盘上,即完成了对文件的操作而不必再调用read,write等系统调用函数。如下图所示 mmap除了可以减少read,write等系统调用以外,还可以减少内存的拷贝次数,比如在read调用时,一个完整的流程是操作系统读磁盘文件到页缓存,再从页缓存将数据拷贝到read传递的buffer里,而如果使用mmap之后,操作系统只需要将磁盘读到页缓存,然后用户就可以直接通过指针的方式操作mmap映射的内存,减少了从内核态到用户态的数据拷贝 mmap适合于对同一块区域频繁读写的情况,比如一个64M的文件存储了一些索引信息,我们需要频繁修改并持久化到磁盘,这样可以将文件通过mmap映射到用户虚拟内存,然后通过指针的方式修改内存区域,由操作系统自动将修改的部分刷回磁盘,也可以自己调用msync手动刷磁盘","tags":"Skill","title":"Linux文件读写机制及优化方式"},{"url":"https://chukeer.github.io/http缓存服务器淘汰策略.html","text":"根据设计需求,一共有三级缓存,分别是内存,SSD,磁盘,所以缓存资源淘汰路径可以是 内存 -> SSD SSD -> 硬盘 硬盘 -> 删除 也会有资源的优先级提升,比如从磁盘提升到SSD或内存。这三种缓存资源可采用同一个优先级队列来管理,新增一个资源时先计算其优先级,得到其在优先级队列中的位置,通过位置可决定存储到哪种媒介,同样,当访问资源时更新其优先级即其在队列中的位置,如果该位置对应的媒介发生变化,则需要做资源的迁移,并且在迁移时可能对目的媒介做调整以满足迁移需求。 具体有哪些存储媒介涉及具体实现,淘汰算法本身不关心这些,淘汰算法要做的只是调整资源在优先级队列中的位置,至于调整之后的操作则由业务层去负责,因此下面只针对淘汰算法本身来讨论 LRU 最常见也是实现最简单的策略就是LRU(Least Recently Used,最近最少使用)算法,根据数据的历史访问记录来进行淘汰数据,其核心思想是\"如果数据最近被访问过,那么将来被访问的几率也更高\" LRU一般采用双向链表实现,基本结构如下 struct LruNode { LruNode* prev; LruNode* next; void* data; }; struct LruList { LruNode* head; LruNode* tail; }; LruNode中的data成员即指向实际缓存索引数据,假设缓存索引以hash结构表示,则淘汰链结构可设计如下 这样hash结构和LRU链表结构分离,分别持有对方指针。下面考虑资源的三种操作 删除节点 假设删除key2,先通过key2查找到ValueObject,得到指向LruNode的指针,删除该节点即可,时间复杂度O(1) 新增节点 新增节点直接加入LruList头部,时间复杂度O(1)如下 新增节点可能会导致缓存达到上限,比如限定内存缓存上限2G,新增一个内存缓存后会操过2G,则需要删除一些资源腾出空间,此时只需要从LruList尾部开始遍历,依次删除直到内存满足需求为止,时间复杂度O(M),M为需要删除的节点个数。假设加入节点key5时需要删除key1,则结构如下 访问节点 在LRU算法中,一个节点被访问后只需将该节点移动到链表头即可,时间复杂度O(1),假设访问key4,则结构如下 LRU优缺点 优点:实现简单 缺点:当存在热点数据时,LRU的效率很好,但偶发性的、周期性的批量操作会导致LRU命中率急剧下降,缓存污染情况比较严重 抽象 前面主要描述了LRU算法结合hash索引的各种操作,实际上任何一个淘汰策略模型都可以被抽象为一个有序队列,每个节点持有一个value,这个value由具体的函数计算得到,队列根据value排序,这样,淘汰策略模型具体操作可描述如下 添加节点: 计算节点value,插入队列,并对队列重新排序 删除指定节点: 将节点从队列中删除 访问节点: 重新计算该节点value,并对队列重新排序 淘汰节点: 从value最小的节点开始依次淘汰 模型的关键在于保持队列有序和计算节点vlaue值,假设我们已经有一个模型能满足基本的插入删除等操作,并保持队列有序,我们只需要实现不同的value计算函数即可实现不同的淘汰算法 以LRU为例,其value计算函数可描述为 $$ V_i = LatestRefTime $$ 即节点最近访问时间,每次访问节点均更新时间,这样新添加和最近被访问的节点优先级最高 基于这种抽象模型,下面介绍几种其它淘汰策略 squid淘汰策略 除了LRU以外,squid还实现了另外两种淘汰策略,这两种策略均可减少LRU缓存污染的缺点,并针对资源命中率和资源字节命中率做了优化 GDSF GDSF(GreddyDual-Size with Frequency)会同时考虑资源访问频次和资源大小,越小的文件被缓存的可能性越大,因此该算法可提高资源命中率,其value计算函数描述如下 $$ V_i = F_i * C_i/S_i + L$$ \\( V_i \\) 代表对象\\( i \\)计算的value值 \\( F_i \\) 代表对象的访问频次 \\( C_i \\) 代表将对象加入缓存的开销,根据squid论文,该值取1时效果最佳 \\( S_i \\) 代表对象大小 \\( L \\) 为动态age,随着对象的加入而递增 LFU-DA LFU-DA(Least Frequently Used with Dynamic Aging)是基于LFU(Least Frequently Used)增加了动态age,它更倾向于缓存被访问频次大的对象,而不论对象大小是多少,因此它可以获得更大的资源字节命中率,其value计算函数描述如下 $$ V_i = C_i * F_i + L$$ \\( V_i \\) 代表对象\\( i \\)计算的value值 \\( F_i \\) 代表对象的访问频次 \\( C_i \\) 代表将对象加入缓存的开销 \\( L \\) 为动态age,随着对象的加入而递增 当\\( C_i \\) 取值为1时,该算法等价于在LFU基础上添加动态age squid中均有以上两种策略的实现,均采用heap管理,只是提供不同计算value的函数 if (!document.getElementById('mathjaxscript_pelican_#%@#$@#')) { var align = \"center\", indent = \"0em\", linebreak = \"false\"; if (false) { align = (screen.width < 768) ? \"left\" : align; indent = (screen.width < 768) ? \"0em\" : indent; linebreak = (screen.width < 768) ? 'true' : linebreak; } var mathjaxscript = document.createElement('script'); var location_protocol = (false) ? 'https' : document.location.protocol; if (location_protocol !== 'http' && location_protocol !== 'https') location_protocol = 'https:'; mathjaxscript.id = 'mathjaxscript_pelican_#%@#$@#'; mathjaxscript.type = 'text/javascript'; mathjaxscript.src = location_protocol + '//cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS-MML_HTMLorMML'; mathjaxscript[(window.opera ? \"innerHTML\" : \"text\")] = \"MathJax.Hub.Config({\" + \" config: ['MMLorHTML.js'],\" + \" TeX: { extensions: ['AMSmath.js','AMSsymbols.js','noErrors.js','noUndefined.js'], equationNumbers: { autoNumber: 'AMS' } },\" + \" jax: ['input/TeX','input/MathML','output/HTML-CSS'],\" + \" extensions: ['tex2jax.js','mml2jax.js','MathMenu.js','MathZoom.js'],\" + \" displayAlign: '\"+ align +\"',\" + \" displayIndent: '\"+ indent +\"',\" + \" showMathMenu: true,\" + \" messageStyle: 'normal',\" + \" tex2jax: { \" + \" inlineMath: [ ['\\\\\\\\(','\\\\\\\\)'] ], \" + \" displayMath: [ ['$$','$$'] ],\" + \" processEscapes: true,\" + \" preview: 'TeX',\" + \" }, \" + \" 'HTML-CSS': { \" + \" styles: { '.MathJax_Display, .MathJax .mo, .MathJax .mi, .MathJax .mn': {color: 'inherit ! important'} },\" + \" linebreaks: { automatic: \"+ linebreak +\", width: '90% container' },\" + \" }, \" + \"}); \" + \"if ('default' !== 'default') {\" + \"MathJax.Hub.Register.StartupHook('HTML-CSS Jax Ready',function () {\" + \"var VARIANT = MathJax.OutputJax['HTML-CSS'].FONTDATA.VARIANT;\" + \"VARIANT['normal'].fonts.unshift('MathJax_default');\" + \"VARIANT['bold'].fonts.unshift('MathJax_default-bold');\" + \"VARIANT['italic'].fonts.unshift('MathJax_default-italic');\" + \"VARIANT['-tex-mathit'].fonts.unshift('MathJax_default-italic');\" + \"});\" + \"MathJax.Hub.Register.StartupHook('SVG Jax Ready',function () {\" + \"var VARIANT = MathJax.OutputJax.SVG.FONTDATA.VARIANT;\" + \"VARIANT['normal'].fonts.unshift('MathJax_default');\" + \"VARIANT['bold'].fonts.unshift('MathJax_default-bold');\" + \"VARIANT['italic'].fonts.unshift('MathJax_default-italic');\" + \"VARIANT['-tex-mathit'].fonts.unshift('MathJax_default-italic');\" + \"});\" + \"}\"; (document.body || document.getElementsByTagName('head')[0]).appendChild(mathjaxscript); }","tags":"Skill","title":"http缓存服务器淘汰策略"},{"url":"https://chukeer.github.io/libevent evhttp学习——http服务端.html","text":"http服务端相对客户端要简单很多,我们仍旧使用libevent-2.1.5版本,服务端接口和2.0版本没有区别 基本流程 http服务端使用到的借口函数及流程如下 创建event_base和evhttp cpp struct event_base *event_base_new(void); struct evhttp *evhttp_new(struct event_base *base); 绑定地址和端口 cpp int evhttp_bind_socket(struct evhttp *http, const char *address, ev_uint16_t port); 设置处理函数 cpp void evhttp_set_gencb(struct evhttp *http, void (*cb)(struct evhttp_request *, void *), void *arg); 派发事件循环 cpp int event_base_dispatch(struct event_base *); 完整代码 服务器接收到请求后打印URL,并返回一段文本信息 #include \"event2/http.h\" #include \"event2/event.h\" #include \"event2/buffer.h\" #include <stdlib.h> #include <stdio.h> void HttpGenericCallback ( struct evhttp_request * request , void * arg ) { const struct evhttp_uri * evhttp_uri = evhttp_request_get_evhttp_uri ( request ); char url [ 8192 ]; evhttp_uri_join ( const_cast < struct evhttp_uri *> ( evhttp_uri ), url , 8192 ); printf ( \"accept request url:%s \\n \" , url ); struct evbuffer * evbuf = evbuffer_new (); if ( ! evbuf ) { printf ( \"create evbuffer failed! \\n \" ); return ; } evbuffer_add_printf ( evbuf , \"Server response. Your request url is %s\" , url ); evhttp_send_reply ( request , HTTP_OK , \"OK\" , evbuf ); evbuffer_free ( evbuf ); } int main ( int argc , char ** argv ) { if ( argc != 2 ) { printf ( \"usage:%s port \\n \" , argv [ 0 ]); return 1 ; } int port = atoi ( argv [ 1 ]); if ( port == 0 ) { printf ( \"port error:%s \\n \" , argv [ 1 ]); return 1 ; } struct event_base * base = event_base_new (); if ( ! base ) { printf ( \"create event_base failed! \\n \" ); return 1 ; } struct evhttp * http = evhttp_new ( base ); if ( ! http ) { printf ( \"create evhttp failed! \\n \" ); return 1 ; } if ( evhttp_bind_socket ( http , \"0.0.0.0\" , port ) != 0 ) { printf ( \"bind socket failed! port:%d \\n \" , port ); return 1 ; } evhttp_set_gencb ( http , HttpGenericCallback , NULL ); event_base_dispatch ( base ); return 0 ; } 编译 g++ http-server.cpp -I/opt/third_party/libevent/include -L/opt/third_party/libevent/lib -levent -o http-server","tags":"Language","title":"libevent evhttp学习——http服务端"},{"url":"https://chukeer.github.io/libevent evhttp学习——http客户端.html","text":"基本环境 使用版本为libevent-2.1.5,目前为beta版,其中evhttp和旧版区别在于新增了如下接口 // 设置回调函数,在包头读取完成后回调 void evhttp_request_set_header_cb ( struct evhttp_request * , int ( * cb )( struct evhttp_request * , void * )) // 设置回调函数,在body有数据返回后回调 void evhttp_request_set_chunked_cb ( struct evhttp_request * , void ( * cb )( struct evhttp_request * , void * )) 这样的好处是可以在合适的时机回调我们注册的回调函数,比如下载1G的文件,在之前的版本只有下载完成后才会回调,现在每下载一部分数据就会回调一次,让上层应用更加灵活,尤其在http代理时,可以做到边下载边回复 2.1.5版本的完整接口文档详见 http://www.wangafu.net/~nickm/libevent-2.1/doxygen/html/http_8h.html 请求流程 http客户端使用到的接口函数及请求流程如下 初始化event_base和evdns_base struct event_base * event_base_new ( void ); struct evdns_base * evdns_base_new ( struct event_base * event_base , int initialize_nameservers ); 创建evhttp_request对象,并设置回调函数,这里的回调函数是和数据接收相关的 cpp struct evhttp_request *evhttp_request_new(void (*cb)(struct evhttp_request *, void *), void *arg); void evhttp_request_set_header_cb(struct evhttp_request *, int (*cb)(struct evhttp_request *, void *)); void evhttp_request_set_chunked_cb(struct evhttp_request *, void (*cb)(struct evhttp_request *, void *)); void evhttp_request_set_error_cb(struct evhttp_request *, void (*)(enum evhttp_request_error, void *)); 3. 创建evhttp_connection对象,并设置回调函数,这里的回调函数是和连接状态相关的 cpp struct evhttp_connection *evhttp_connection_base_new(struct event_base *base, struct evdns_base *dnsbase, const char *address, unsigned short port); void evhttp_connection_set_closecb(struct evhttp_connection *evcon, void (*)(struct evhttp_connection *, void *), void *); 4. 有选择的向evhttp_request添加包头字段 cpp int evhttp_add_header(struct evkeyvalq *headers, const char *key, const char *value); 5. 发送请求 cpp int evhttp_make_request(struct evhttp_connection *evcon, struct evhttp_request *req, enum evhttp_cmd_type type, const char *uri); 6. 派发事件 cpp int event_base_dispatch(struct event_base *); 完整代码 #include \"event2/http.h\" #include \"event2/http_struct.h\" #include \"event2/event.h\" #include \"event2/buffer.h\" #include \"event2/dns.h\" #include \"event2/thread.h\" #include <stdio.h> #include <string.h> #include <assert.h> #include <sys/queue.h> #include <event.h> void RemoteReadCallback ( struct evhttp_request * remote_rsp , void * arg ) { event_base_loopexit (( struct event_base * ) arg , NULL ); } int ReadHeaderDoneCallback ( struct evhttp_request * remote_rsp , void * arg ) { fprintf ( stderr , \"< HTTP/1.1 %d %s \\n \" , evhttp_request_get_response_code ( remote_rsp ), evhttp_request_get_response_code_line ( remote_rsp )); struct evkeyvalq * headers = evhttp_request_get_input_headers ( remote_rsp ); struct evkeyval * header ; TAILQ_FOREACH ( header , headers , next ) { fprintf ( stderr , \"< %s: %s \\n \" , header -> key , header -> value ); } fprintf ( stderr , \"< \\n \" ); return 0 ; } void ReadChunkCallback ( struct evhttp_request * remote_rsp , void * arg ) { char buf [ 4096 ]; struct evbuffer * evbuf = evhttp_request_get_input_buffer ( remote_rsp ); int n = 0 ; while (( n = evbuffer_remove ( evbuf , buf , 4096 )) > 0 ) { fwrite ( buf , n , 1 , stdout ); } } void RemoteRequestErrorCallback ( enum evhttp_request_error error , void * arg ) { fprintf ( stderr , \"request failed \\n \" ); event_base_loopexit (( struct event_base * ) arg , NULL ); } void RemoteConnectionCloseCallback ( struct evhttp_connection * connection , void * arg ) { fprintf ( stderr , \"remote connection closed \\n \" ); event_base_loopexit (( struct event_base * ) arg , NULL ); } int main ( int argc , char ** argv ) { if ( argc != 2 ) { printf ( \"usage:%s url\" , argv [ 1 ]); return 1 ; } char * url = argv [ 1 ]; struct evhttp_uri * uri = evhttp_uri_parse ( url ); if ( ! uri ) { fprintf ( stderr , \"parse url failed! \\n \" ); return 1 ; } struct event_base * base = event_base_new (); if ( ! base ) { fprintf ( stderr , \"create event base failed! \\n \" ); return 1 ; } struct evdns_base * dnsbase = evdns_base_new ( base , 1 ); if ( ! dnsbase ) { fprintf ( stderr , \"create dns base failed! \\n \" ); } assert ( dnsbase ); struct evhttp_request * request = evhttp_request_new ( RemoteReadCallback , base ); evhttp_request_set_header_cb ( request , ReadHeaderDoneCallback ); evhttp_request_set_chunked_cb ( request , ReadChunkCallback ); evhttp_request_set_error_cb ( request , RemoteRequestErrorCallback ); const char * host = evhttp_uri_get_host ( uri ); if ( ! host ) { fprintf ( stderr , \"parse host failed! \\n \" ); return 1 ; } int port = evhttp_uri_get_port ( uri ); if ( port < 0 ) port = 80 ; const char * request_url = url ; const char * path = evhttp_uri_get_path ( uri ); if ( path == NULL || strlen ( path ) == 0 ) { request_url = \"/\" ; } printf ( \"url:%s host:%s port:%d path:%s request_url:%s \\n \" , url , host , port , path , request_url ); struct evhttp_connection * connection = evhttp_connection_base_new ( base , dnsbase , host , port ); if ( ! connection ) { fprintf ( stderr , \"create evhttp connection failed! \\n \" ); return 1 ; } evhttp_connection_set_closecb ( connection , RemoteConnectionCloseCallback , base ); evhttp_add_header ( evhttp_request_get_output_headers ( request ), \"Host\" , host ); evhttp_make_request ( connection , request , EVHTTP_REQ_GET , request_url ); event_base_dispatch ( base ); return 0 ; } 编译 g++ http_client.cpp -I/opt/local/libevent-2.1.5/include -L/opt/local/libevent-2.1.5/lib -levent -g -o http_client 运行示例,这里只打印了包头字段 $ ./ http_client http :// www .qq.com >/ dev / null < HTTP / 1 .1 200 OK < Server : squid / 3 .4.3 < Content-Type : text / html ; charset = GB2312 < Cache-Control : max-age = 60 < Expires : Fri , 05 Aug 2016 08 :48:31 GMT < Date : Fri , 05 Aug 2016 08 :47:31 GMT < Transfer-Encoding : chunked < Connection : keep-alive < Connection : Transfer-Encoding <","tags":"Language","title":"libevent evhttp学习——http客户端"},{"url":"https://chukeer.github.io/C++ and Python 多线程笔记.html","text":"C++ Posix多线程 #include <stdio.h> #include <stdlib.h> #include <pthread.h> #include <vector> using std :: vector ; void * Proc ( void * arg ) { pthread_t pthread = * ( pthread_t * ) arg ; printf ( \"this is thread %ld \\n \" , pthread ); } int main ( int argc , char ** argv ) { if ( argc != 2 ) { printf ( \"usage:%s thread_num\" , argv [ 0 ]); return 1 ; } int thread_num = atoi ( argv [ 1 ]); vector < pthread_t > threads ( thread_num ); for ( int i = 0 ; i < thread_num ; ++ i ) { pthread_t pthread ; pthread_create ( & pthread , NULL , & Proc , & threads [ i ]); threads [ i ] = pthread ; } for ( vector < pthread_t >:: iterator it = threads . begin (); it != threads . end (); ++ it ) { pthread_join ( * it , NULL ); } return 0 ; } boost多线程 全局函数作为线程函数 #include <stdio.h> #include <stdlib.h> #include <pthread.h> #include <vector> #include <string> #include <boost/thread.hpp> using std :: vector ; using std :: string ; void Proc ( int num , const string & str ) { printf ( \"this is thread %d, say %s \\n \" , num , str . c_str ()); } int main ( int argc , char ** argv ) { if ( argc != 2 ) { printf ( \"usage:%s thread_num\" , argv [ 0 ]); return 1 ; } int thread_num = atoi ( argv [ 1 ]); vector < boost :: thread *> threads ; for ( int i = 0 ; i < thread_num ; ++ i ) { boost :: thread * thread = new boost :: thread ( & Proc , i , \"Hello World!\" ); threads . push_back ( thread ); } for ( vector < boost :: thread *>:: iterator it = threads . begin (); it != threads . end (); ++ it ) { ( * it ) -> join (); } return 0 ; } 类成员函数作为线程函数 #include <stdio.h> #include <stdlib.h> #include <pthread.h> #include <vector> #include <string> #include <boost/thread.hpp> #include <boost/bind.hpp> using std :: vector ; using std :: string ; class Test { public : void Proc ( int num , const string & str ) { printf ( \"this is thread %d, say %s \\n \" , num , str . c_str ()); } }; int main ( int argc , char ** argv ) { if ( argc != 2 ) { printf ( \"usage:%s thread_num\" , argv [ 0 ]); return 1 ; } int thread_num = atoi ( argv [ 1 ]); vector < boost :: thread *> threads ; for ( int i = 0 ; i < thread_num ; ++ i ) { Test * test = new Test (); boost :: thread * thread = new boost :: thread ( boost :: bind ( & Test :: Proc , test , i , \"Hello World!\" )); threads . push_back ( thread ); } for ( vector < boost :: thread *>:: iterator it = threads . begin (); it != threads . end (); ++ it ) { ( * it ) -> join (); } return 0 ; } Python 使用threading模块 import sys import threading def proc ( num , str ): print 'this is thread %d , say %s ' % ( num , str ) def start_threads ( thread_num ): threads = [] for i in range ( thread_num ): thread = threading . Thread ( target = proc , args = ( i , \"Hello World!\" )) threads . append ( thread ) thread . start () for thread in threads : thread . join () if __name__ == '__main__' : if len ( sys . argv ) != 2 : print \"usage: %s thread_num\" % sys . argv [ 0 ] sys . exit ( 1 ) start_threads ( int ( sys . argv [ 1 ])) 使用multiprocessing模块 import sys from multiprocessing.dummy import Pool as ThreadPool def proc ( arg ): num , str = arg print 'this is thread %d , say %s ' % ( num , str ) def start_threads ( thread_num ): pool = ThreadPool ( thread_num ) args = [( i , \"Hello World!\" ) for i in range ( thread_num )] pool . map ( proc , args ) pool . close () pool . join () if __name__ == '__main__' : if len ( sys . argv ) != 2 : print \"usage: %s thread_num\" % sys . argv [ 0 ] sys . exit ( 1 ) start_threads ( int ( sys . argv [ 1 ]))","tags":"Language","title":"C++ & Python 多线程笔记"},{"url":"https://chukeer.github.io/Python import机制备忘笔记.html","text":"python的模块有两种组织方式,一种是单纯的python文件,文件名就是模块名,一种是包,包是一个包含了若干python文件的目录,目录下必须有一个文件 __init__.py ,这样目录名字就是模块名,包里的python文件也可以通过 包名.文件名 的方式import import语法 import语法有两种 直接import模块 import Module import Module as xx 从模块import对象(下级模块,类,函数,变量等) from Module import Name from Module immport Name as yy as语法是用来设置对象(这里用对象泛指模块,类,函数等等)别名,import将对象名字引入了当前文件的名字空间 假设有如下目录结构 ├── A.py └── pkg ├── B.py └── __init__.py 在当前目录下,以下语句都是有效的 import A import pkg import pkg.B from pkg import B 为了简化讨论,下面将不会对as语法进行举例 import步骤 python所有加载的模块信息都存放在sys.modules结构中,当import一个模块时,会按如下步骤来进行 如果是 import A ,检查sys.modules中是否已经有A,如果有则不加载,如果没有则为A创建module对象,并加载A 如果是 from A import B ,先为A创建module对象,再解析A,从中寻找B并填充到A的 __dict__ 中 嵌套import 在import模块时我们可能会担心一个模块会不会被import多次,假设有A,B,C三个模块,A需要import B和C,B又要import C,这样A会执行到两次import C,一次是自己本身import,一次是在import B时执行的import,但根据上面讲到的import步骤,在第二次import时发现模块已经被加载,所以不会重复import 但如下情况却会报错 #filename: A.py from B import BB class AA : pass #filename: B.py from A import AA class BB : pass 这时不管是执行A.py还是B.py都会抛出ImportError的异常,假设我们执行的是A.py,究其原因如下 文件A.py执行 from B import BB ,会先扫描B.py,同时在A的名字空间中为B创建module对象,试图从B中查找BB 扫描B.py第一行执行 from A import AA ,此时又会去扫描A.py 扫描A.py第一行执行 from B import BB ,由于步骤1已经为B创建module对象,所以会直接从B的module对象的 __dict__ 中获取BB,此时显然BB是获取不到的,于是抛出异常 解决这种情况有两种办法, 将 from B import BB 改为 import B ,或将 from A import AA 改为 import A 将A.py或B.py中的两行代码交换位置 总之,import需要注意的是,尽量在需要用到时再import 包的import 当一个目录下有 __init__.py 文件时,该目录就是一个python的包 import包和import单个文件是一样的,我们可以这样类比: import单个文件时,文件里的类,函数,变量都可以作为import的对象 import包时,包里的子包,文件,以及__init__.py里的类,函数,变量都可以作为import的对象 假设有如下目录结构 pkg ├── __init__.py └── file.py 其中 __init__.py 内容如下 argument = 0 class A:pass 在和pkg同级目录下执行如下语句都是OK的 >>> import pkg >>> import pkg.file >>> from pkg import file >>> from pkg import A >>> from pkg import argument 但如下语句是错误的 >>> import pkg.A >>> import pkg.argument 报错 ImportError: No module named xxx ,因为当我们执行 import A.B ,A和B都必须是模块(文件或包) 相对导入和绝对导入 绝对导入的格式为 import A.B 或 from A import B ,相对导入格式为 from . import B 或 from ..A import B , . 代表当前模块, .. 代表上层模块, ... 代表上上层模块,依次类推。当我们有多个包时,就可能有需求从一个包import另一个包的内容,这就会产生绝对导入,而这也往往是最容易发生错误的时候,还是以具体例子来说明 目录结构如下 app ├── __inti__.py ├── mod1 │ ├── file1.py │ └── __init__.py ├── mod2 │ ├── file2.py │ └── __init__.py └── start.py 其中app/start.py内容为 import mod1.file1 app/mod1/file1.py内容为 from ..mod2 import file2 为了便于分析,我们在所有py文件(包括 __init__.py )第一行加入 print __file__, __name__ 现在app/mod1/file1.py里用到了相对导入,我们在app/mod1下执行 python file1.py 或者在app下执行 python mod1/file1.py 都会报错 ValueError: Attempted relative import in non-package 在app下执行 python -m mod1.file1 或 python start.py 都会报错 ValueError: Attempted relative import beyond toplevel package 具体原因后面再说,我们先来看一下导入模块时的一些规则 在没有明确指定包结构的情况下,python是根据 __name__ 来决定一个模块在包中的结构的,如果是 __main__ 则它本身是顶层模块,没有包结构,如果是 A.B.C 结构,那么顶层模块是A。基本上遵循这样的原则 如果是绝对导入, 一个模块只能导入自身的子模块或和它的顶层模块同级别的模块及其子模块 如果是相对导入, 一个模块必须有包结构且只能导入它的顶层模块内部的模块 有目录结构如下 A ├── B1 │ ├── C1 │ │ └── file.py │ └── C2 └── B2 其中A,B1,B2,C1,C2都为包,这里为了展示简单没有列出 __init__.py 文件,当file.py的包结构为 A.B1.C1.file (注意,是根据 __name__ 来的,而不是磁盘的目录结构,在不同目录下执行file.py时对应的包目录结构都是不一样的)时,在file.py中可采用如下的绝对的导入 import A.B1.C2 import A.B2 和如下的相对导入 from .. import C2 from ... import B2 什么情况下会让file.py的包结构为 A.B1.C1.file 呢,有如下两种 在A的上层目录执行 python -m A.B1.C1.file , 此时明确指定了包结构 在A的上层目录建立文件start.py,在start.py里有 import A.B1.C1.file ,然后执行 python start.py ,此时包结构是根据file.py的 __name__ 变量来的 再看前面出错的两种情况,第一种执行 python file1.py 和 python mod1/file1.py ,此时file.py的 __name__ 为 __main__ ,也就是说它本身就是顶层模块,并没有包结构,所以会报错 第二种情况,在执行 python -m mod1.file1 和 python start.py 时,前者明确告诉解释器mod1是顶层模块,后者需要导入file1,而file1.py的 __name__ 为mod1.file1,顶层模块为也mod1,所以在file1.py中执行 from ..mod2 import file2 时会报错 ,因为mod2并不在顶层模块mod1内部。通过错误堆栈可以看出,并不是在start.py中绝对导入时报错,而是在file1.py中相对导入报的错 那么如何才能偶正确执行呢,有两种方法,一种是在app上层目录执行python -m app.mod1.file1,另一种是改变目录结构,将所有包放在一个大包中,如下 app ├── pkg │ ├── __init__.py │ ├── mod1 │ │ ├── __init__.py │ │ └── file1.py │ └── mod2 │ ├── __init__.py │ └── file2.py └── start.py start.py内容改成 import pkg.mod1.file1 ,然后在app下执行 python start.py","tags":"Language","title":"Python import机制备忘笔记"},{"url":"https://chukeer.github.io/curl参数说明.html","text":"参数 说明 -i/--include 输出响应包头 -I 只获取响应包头 -x/—proxy <proxyhost[:port]> 设置代理 -X/—request <command> 设置http method -D/—dump-header <file> 输出包头到指定文件 -H/--header <header> 指定请求包头字段 如果有多个字段,可多次使用本参数 -d/—data <data> 发送post数据(ascii) curl -d \"param1=value1¶m2=value2\" --data-binary <data> —data-binary '@filename' 发送二进制post数据 如果以'@'开头则发送文件内容 -A/—user-agent <agent string> 设置user-agent","tags":"Skill","title":"curl参数说明"},{"url":"https://chukeer.github.io/gdb调试技巧备忘.html","text":"准备工作 为了能让程序更直观的被调试,在编译时应该添加一些选项 -g : 添加调试选项 -ggdb3 : 调试宏定义 启动方式 不带参数 gdb ./a.out 带参数 gdb ./a.out set args -a -b -c any_argument_you_need b main run 调试core文件 gdb bin_name core_name 调试正在运行的程序 大致按如下步骤 ps axu | grep bin_name , 获取进程id gdb attach pid ,启动gdb b somewhere ,设置断点 c ,继续运行程序 基本命令 括号里是命令缩写,详细命令介绍见 http://www.yolinux.com/TUTORIALS/GDB-Commands.html ,这里只列出常用的 命令 描述 查看信息 info break(b) 查看断点 info threads 查看线程 info watchpoints 查看观察点 thread thread-number 进入某个线程 删除信息 delete(d) 删除所有断点,观察点 delete(d) breakpoint-number delete(d) watchpoint 删除指定断点,观察点 调试 step(s) 进入函数 next(n) 执行一行 until line-number 执行到指定行 continue(c) 执行到下一个断点/观察点 finish 执行到函数完成 堆栈 backtrace(bt) 打印堆栈 frame(f) number 查看某一帧 up/down 查看上一帧/下一帧 thread apply all bt 打印所有线程堆栈信息 源码 list(l) list function 查看源码,函数 directory(dir) *directory-name * 添加源码搜索路径 查看变量 print(p) variable-name 打印变量 p *array-variable@length 打印数组的前length个变量 p/ format variable-name format 和printf格式近似d: 整数u: 无符号整数c: 字符f: 浮点数x: 十六进制o: 八进制t: 二进制r: raw格式 按指定格式打印变量,如p/x variable-name代表以十六进制打印变量 x/nfu address nfu为可选的三个参数n代表要打印的数据块数量f为打印的格式,和p/format中一致u为打印的数据块大小,有如下选择b/h/w/g: 单/双/四/八字节,默认为4字节 按指定格式查看内存数据,如x/7xh address表示从内存地址address开始打印7个双字节,每个双字节以十六进制显示 ptype variable 打印变量数据类型 运行和退出 run(r) 运行程序 quit(q) 退出调试 设置 set print pretty on/off 默认off。格式化结构体的打印 set print element 0 打印完整字符串 set logging file log-file 设置日志文件,默认是gdb.txt set logging on/off 打开/关闭日志 case说明 手动加载源代码 当我们服务器上调试程序时,由于没有加载源码路径而无法查看代码,此时可以将源码目录拷贝到服务器上,然后在gdb调试时通过 dir directory-name 命令加载源码,注意,这里的directory-name一般是程序的makefile所在的路径 打印调试信息到日志文件 有时候需要对打印的信息进行查找分析,这种操作在gdb界面不太方便,可以将内容打印到日志,然后通过shell脚本处理。先打开日志调试开关 set logging on ,然后打印你需要的信息,再关闭开关 set logging off ,这期间打印的信息就会被写入gdb.txt文件,如果不想写入这个文件,可以在打开日志开关前先设置日志文件名 set logging file log-file 可视化调试 gdb自带TUI(Text User Interface)模式,详细介绍见 https://sourceware.org/gdb/onlinedocs/gdb/TUI.html 基本使用方式如下 Ctrl-x a :启动/结束TUI ,启动TUI还可以使用win命令 Ctrl-x o :切换激活窗口 info win :查看窗口 focus next / prev / src / asm / regs / split :激活指定窗口 PgUp :在激活窗口上翻 PgDown :在激活窗口下翻 Up/Down/Left/Right :在激活窗口上移一行/下移一行/左移一列/右移一列 layout next / prev :上一个/下一个窗口布局 layout src :只展示源码窗口 layout asm :只展示汇编窗口 layout split :展示源码和汇编窗口 layout regs :展示寄存器窗口 winheight name +count/-count :调整窗口高度(慎用,可能会让屏幕凌乱) 需要注意的是,在cmd窗口上,原本Up/Down是在历史命令中选择上一条/下一条命令,若想使用该功能,必须先将焦点转移到cmd窗口,即执行focus cmd TUI的窗口一共有4种,src, cmd, asm, regs, 默认是打开src和cmd窗口,可以通过layout选择不同的窗口布局。最终的效果图是这样的 可以看到上面是代码区(src),可以查看当前执行的代码和断点信息,当前执行的代码被高亮显示,并且在代码最左边有一个符号 > ,设置了断点的行最左边的符号是 B ,下面是命令区(cmd),可以键入gdb调试命令 这样调试的时候执行到哪一行代码就一清二楚了,当然,用gdb调试最关键的还是掌握基本命令,TUI只是一中辅助手段 打印STL和boost数据结构 当我们要查看某种数据结构的变量,如果gdb不认识该数据结构,它会按照 p/r variable-name 的方式打印数据的原始内容,对于比较复杂的数据结构,比如map类型,我们更关心的是它存储的元素内容,而不是它的数据结构原始内容,还好gdb7.0提供Python接口可以通过实现Python脚本打印特殊的数据结构,已经有一些开源代码提供对boost以及STL数据结构的解析 打印STL数据结构 首先查看系统下是否有/usr/share/gdb/python/libstdcxx目录,如果有,说明gdb已经自带对STL数据类型的解析,如果没有可以自己安装,详细介绍见 https://sourceware.org/gdb/wiki/STLSupport ,这里简单说明一下 svn co svn://gcc.gnu.org/svn/gcc/trunk/libstdc++-v3/python 新建~/.gdbinit,键入如下内容 python import sys sys . path . insert ( 0 , '/home/maude/gdb_printers/python' ) from libstdcxx.v6.printers import register_libstdcxx_printers register_libstdcxx_printers ( None ) end 其中/home/maude/gdb_printers/python是你实际下载svn代码的路径 打印boost数据结构 souceforge 上有现成的 boost-gdb-printers ,但根据我的试验发现在打印unordered_map等数据结构时会报错,因此我做了一些修改并放在github上 https://github.com/handy1989/boost-gdb-printers ,经测试在boost的1.55和1.58版本下均可用 下载boost-gdb-printers,找到里面的boost-gdb-printers.py,修改 boost.vx_y 为实际的版本,并获取文件绝对路径,假设为your_dir/boost-gdb-printers.py,在~/.gdbinit里添加 source your_dir/boost-gdb-printers.py 这时即可打印boost数据结构,我们用以下代码做一个简单的测试 // filename: gdb_test.cpp #include <stdio.h> #include <string> #include <boost/shared_ptr.hpp> #include <boost/unordered_map.hpp> using std :: map ; using std :: string ; struct TestData { int x ; string y ; }; void break_here () { } int main () { boost :: shared_ptr < TestData > shared_x ( new TestData ()); shared_x -> x = 100 ; shared_x -> y = \"hello world\" ; TestData data1 ; data1 . x = 100 ; data1 . y = \"first\" ; TestData data2 ; data2 . x = 200 ; data2 . y = \"second\" ; boost :: unordered_map < int , TestData > unordered_map_x ; unordered_map_x [ 1 ] = data1 ; unordered_map_x [ 2 ] = data2 ; break_here (); return 0 ; } 编译如下 g++ -g gdb_test.cpp -I/your-boost-include-dir your-boost-include-dir替换为实际的boost头文件所在路径,编写gdb脚本gdb_test.gdb如下 b break_here r fin p/r shared_x p shared_x p/r unordered_map_x p unordered_map_x q y 执行gdb ./a.out -x gdb_test.gdb,查看变量的输出如下 \\ \\(1和\\\\) 3分别是shared_ptr和unordered_map数据类型的原始打印格式, \\(2和\\) 4是加载boost-gdb-printers之后的打印格式 if (!document.getElementById('mathjaxscript_pelican_#%@#$@#')) { var align = \"center\", indent = \"0em\", linebreak = \"false\"; if (false) { align = (screen.width < 768) ? \"left\" : align; indent = (screen.width < 768) ? \"0em\" : indent; linebreak = (screen.width < 768) ? 'true' : linebreak; } var mathjaxscript = document.createElement('script'); var location_protocol = (false) ? 'https' : document.location.protocol; if (location_protocol !== 'http' && location_protocol !== 'https') location_protocol = 'https:'; mathjaxscript.id = 'mathjaxscript_pelican_#%@#$@#'; mathjaxscript.type = 'text/javascript'; mathjaxscript.src = location_protocol + '//cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS-MML_HTMLorMML'; mathjaxscript[(window.opera ? \"innerHTML\" : \"text\")] = \"MathJax.Hub.Config({\" + \" config: ['MMLorHTML.js'],\" + \" TeX: { extensions: ['AMSmath.js','AMSsymbols.js','noErrors.js','noUndefined.js'], equationNumbers: { autoNumber: 'AMS' } },\" + \" jax: ['input/TeX','input/MathML','output/HTML-CSS'],\" + \" extensions: ['tex2jax.js','mml2jax.js','MathMenu.js','MathZoom.js'],\" + \" displayAlign: '\"+ align +\"',\" + \" displayIndent: '\"+ indent +\"',\" + \" showMathMenu: true,\" + \" messageStyle: 'normal',\" + \" tex2jax: { \" + \" inlineMath: [ ['\\\\\\\\(','\\\\\\\\)'] ], \" + \" displayMath: [ ['$$','$$'] ],\" + \" processEscapes: true,\" + \" preview: 'TeX',\" + \" }, \" + \" 'HTML-CSS': { \" + \" styles: { '.MathJax_Display, .MathJax .mo, .MathJax .mi, .MathJax .mn': {color: 'inherit ! important'} },\" + \" linebreaks: { automatic: \"+ linebreak +\", width: '90% container' },\" + \" }, \" + \"}); \" + \"if ('default' !== 'default') {\" + \"MathJax.Hub.Register.StartupHook('HTML-CSS Jax Ready',function () {\" + \"var VARIANT = MathJax.OutputJax['HTML-CSS'].FONTDATA.VARIANT;\" + \"VARIANT['normal'].fonts.unshift('MathJax_default');\" + \"VARIANT['bold'].fonts.unshift('MathJax_default-bold');\" + \"VARIANT['italic'].fonts.unshift('MathJax_default-italic');\" + \"VARIANT['-tex-mathit'].fonts.unshift('MathJax_default-italic');\" + \"});\" + \"MathJax.Hub.Register.StartupHook('SVG Jax Ready',function () {\" + \"var VARIANT = MathJax.OutputJax.SVG.FONTDATA.VARIANT;\" + \"VARIANT['normal'].fonts.unshift('MathJax_default');\" + \"VARIANT['bold'].fonts.unshift('MathJax_default-bold');\" + \"VARIANT['italic'].fonts.unshift('MathJax_default-italic');\" + \"VARIANT['-tex-mathit'].fonts.unshift('MathJax_default-italic');\" + \"});\" + \"}\"; (document.body || document.getElementsByTagName('head')[0]).appendChild(mathjaxscript); }","tags":"Skill","title":"gdb调试技巧备忘"},{"url":"https://chukeer.github.io/Python调用C++程序备忘笔记.html","text":"Python的优点是开发效率高,使用方便,C++则是运行效率高,这两者可以相辅相成,不管是在Python项目中嵌入C++代码,或是在C++项目中用Python实现外围功能,都可能遇到Python调用C++模块的需求,下面列举出集中c++代码导出成Python接口的几种基本方法 原生态导出 Python解释器就是用C实现,因此只要我们的C++的数据结构能让Python认识,理论上就是可以被直接调用的。我们实现test1.cpp如下 #include <Python.h> int Add ( int x , int y ) { return x + y ; } int Del ( int x , int y ) { return x - y ; } PyObject * WrappAdd ( PyObject * self , PyObject * args ) { int x , y ; if ( ! PyArg_ParseTuple ( args , \"ii\" , & x , & y )) { return NULL ; } return Py_BuildValue ( \"i\" , Add ( x , y )); } PyObject * WrappDel ( PyObject * self , PyObject * args ) { int x , y ; if ( ! PyArg_ParseTuple ( args , \"ii\" , & x , & y )) { return NULL ; } return Py_BuildValue ( \"i\" , Del ( x , y )); } static PyMethodDef test_methods [] = { { \"Add\" , WrappAdd , METH_VARARGS , \"something\" }, { \"Del\" , WrappDel , METH_VARARGS , \"something\" }, { NULL , NULL } }; extern \"C\" void inittest1 () { Py_InitModule ( \"test1\" , test_methods ); } 编译命令如下 g++ -fPIC -shared test1.cpp -I/usr/include/python2.6 -o test1.so 运行Python解释器,测试如下 >>> import test1 >>> test1 . Add ( 1 , 2 ) 3 这里要注意一下几点 如果生成的动态库名字为test1,则源文件里必须有inittest1这个函数,且Py_InitModule的第一个参数必须是\"test1\",否则Python导入模块会失败 如果是cpp源文件,inittest1函数必须用extern \"C\"修饰,如果是c源文件,则不需要。原因是Python解释器在导入库时会寻找initxxx这样的函数,而C和C++对函数符号的编码方式不同,C++在对函数符号进行编码时会考虑函数长度和参数类型,具体可以通过 nm test1.so 查看函数符号,c++filt工具可通过符号反解出函数原型 通过boost实现 我们使用和上面同样的例子,实现test2.cpp如下 #include <boost/python/module.hpp> #include <boost/python/def.hpp> using namespace boost :: python ; int Add ( const int x , const int y ) { return x + y ; } int Del ( const int x , const int y ) { return x - y ; } BOOST_PYTHON_MODULE ( test2 ) { def ( \"Add\" , Add ); def ( \"Del\" , Del ); } 其中BOOST_PYTHON_MODULE的参数为要导出的模块名字 编译命令如下 g++ test2.cpp -fPIC -shared -o test2.so -I/usr/include/python2.6 -I/usr/local/include -L/usr/local/lib -lboost_python 注意: 编译时需要指定boost头文件和库的路径,我这里分别是/usr/local/include和/usr/local/lib 或者通过setup.py导出模块 1 2 3 4 5 6 7 8 9 #!/usr/bin/env python from distutils.core import setup from distutils.extension import Extension setup ( name = \"PackageName\" , ext_modules = [ Extension ( \"test2\" , [ \"test2.cpp\" ], libraries = [ \"boost_python\" ]) ]) Extension的第一个参数为模块名,第二个参数为文件名 执行如下命令 python setup.py build 这时会生成build目录,找到里面的test2.so,并进入同一级目录,验证如下 >>> import test2 >>> test2 . Add ( 1 , 2 ) 3 >>> test2 . Del ( 1 , 2 ) - 1 导出类 test3.cpp实现如下 #include <boost/python.hpp> using namespace boost :: python ; class Test { public : int Add ( const int x , const int y ) { return x + y ; } int Del ( const int x , const int y ) { return x - y ; } }; BOOST_PYTHON_MODULE ( test3 ) { class_ < Test > ( \"Test\" ) . def ( \"Add\" , & Test :: Add ) . def ( \"Del\" , & Test :: Del ); } 注意:BOOST_PYTHON_MODULE里的.def使用方法有点类似Python的语法,等同于 class_ < Test >( \"Test\" ) .def ( \"Add\" , & Test : :Add ); class_ < Test >( \"Test\" ) .def ( \"Del\" , & Test : :Del ); 编译命令如下 g++ test3.cpp -fPIC -shared -o test3.so -I/usr/include/python2.6 -I/usr/local/include/boost -L/usr/local/lib -lboost_python 测试如下 >>> import test3 >>> test = test3 . Test () >>> test . Add ( 1 , 2 ) 3 >>> test . Del ( 1 , 2 ) - 1 导出变参函数 test4.cpp实现如下 #include <boost/python.hpp> using namespace boost :: python ; class Test { public : int Add ( const int x , const int y , const int z = 100 ) { return x + y + z ; } }; int Del ( const int x , const int y , const int z = 100 ) { return x - y - z ; } BOOST_PYTHON_MEMBER_FUNCTION_OVERLOADS ( Add_member_overloads , Add , 2 , 3 ) BOOST_PYTHON_FUNCTION_OVERLOADS ( Del_overloads , Del , 2 , 3 ) BOOST_PYTHON_MODULE ( test4 ) { class_ < Test > ( \"Test\" ) . def ( \"Add\" , & Test :: Add , Add_member_overloads ( args ( \"x\" , \"y\" , \"z\" ), \"something\" )); def ( \"Del\" , Del , Del_overloads ( args ( \"x\" , \"y\" , \"z\" ), \"something\" )); } 这里Add和Del函数均采用了默认参数,Del为普通函数,Add为类成员函数,这里分别调用了不同的宏,宏的最后两个参数分别代表函数的最少参数个数和最多参数个数 编译命令如下 g++ test4.cpp -fPIC -shared -o test4.so -I/usr/include/python2.6 -I/usr/local/ include/boost -L/usr/local/lib -lboost_python 测试如下 >>> import test4 >>> test = test4 . Test () >>> print test . Add ( 1 , 2 ) 103 >>> print test . Add ( 1 , 2 , z = 3 ) 6 >>> print test4 . Del ( 1 , 2 ) - 1 >>> print test4 . Del ( 1 , 2 , z = 3 ) - 1 导出带Python对象的接口 既然是导出为Python接口,调用者难免会使用Python特有的数据结构,比如tuple,list,dict,由于原生态方法太麻烦,这里只记录boost的使用方法,假设要实现如下的Python函数功能 def Square(list_a) { return [x * x for x in list_a] } 即对传入的list每个元素计算平方,返回list类型的结果 代码如下 #include <boost/python.hpp> boost :: python :: list Square ( boost :: python :: list & data ) { boost :: python :: list ret ; for ( int i = 0 ; i < len ( data ); ++ i ) { ret . append ( data [ i ] * data [ i ]); } return ret ; } BOOST_PYTHON_MODULE ( test5 ) { def ( \"Square\" , Square ); } 编译命令如下 g++ test5.cpp -fPIC -shared -o test5.so -I/usr/include/python2.6 -I/usr/local/include/boost -L/usr/local/lib -lboost_python 测试如下 >>> import test5 >>> test5 . Square ([ 1 , 2 , 3 ]) [ 1 , 4 , 9 ] boost实现了boost::python::tuple, boost::python::list, boost::python::dict这几个数据类型,使用方法基本和Python保持一致,具体方法可以查看boost头文件里的boost/python/tuple.hpp及其它对应文件 另外比较常用的一个函数是 boost::python::make_tuple() ,使用方法如下 boost : :python : :tuple t = boost : :python : :make_tuple ( a , b , c );","tags":"Language","title":"Python调用C++程序备忘笔记"},{"url":"https://chukeer.github.io/Linux程序编译链接动态库版本的问题.html","text":"不同版本的动态库可能会不兼容,如果程序在编译时指定动态库是某个低版本,运行是用的一个高版本,可能会导致无法运行。Linux上对动态库的命名采用libxxx.so.a.b.c的格式,其中a代表大版本号,b代表小版本号,c代表更小的版本号,我们以Linux自带的cp程序为例,通过ldd查看其依赖的动态库 $ ldd /bin/cp linux-vdso.so.1 => (0x00007ffff59df000) libselinux.so.1 => /lib64/libselinux.so.1 (0x00007fb3357e0000) librt.so.1 => /lib64/librt.so.1 (0x00007fb3355d7000) libacl.so.1 => /lib64/libacl.so.1 (0x00007fb3353cf000) libattr.so.1 => /lib64/libattr.so.1 (0x00007fb3351ca000) libc.so.6 => /lib64/libc.so.6 (0x00007fb334e35000) libdl.so.2 => /lib64/libdl.so.2 (0x00007fb334c31000) /lib64/ld-linux-x86-64.so.2 (0x00007fb335a0d000) libpthread.so.0 => /lib64/libpthread.so.0 (0x00007fb334a14000) 左边是依赖的动态库名字,右边是链接指向的文件,再查看libacl.so相关的动态库 $ ll /lib64/libacl.so* lrwxrwxrwx. 1 root root 15 1月 7 2015 /lib64/libacl.so.1 -> libacl.so.1.1.0 -rwxr-xr-x. 1 root root 31280 12月 8 2011 /lib64/libacl.so.1.1.0 我们发现libacl.so.1实际上是一个软链接,它指向的文件是libacl.so.1.1.0,命名方式符合我们上面的描述。也有不按这种方式命名的,比如 $ ll /lib64/libc.so* lrwxrwxrwx 1 root root 12 8月 12 14:18 /lib64/libc.so.6 -> libc-2.12.so 不管怎样命名,只要按照规定的方式来生成和使用动态库,就不会有问题。而且我们往往是在机器A上编译程序,在机器B上运行程序,编译和运行的环境其实是有略微不同的。下面就说说动态库在生成和使用过程中的一些问题 动态库的编译 我们以一个简单的程序作为例子 // filename:hello.c #include <stdio.h> void hello ( const char * name ) { printf ( \"hello %s! \\n \" , name ); } // filename:hello.h void hello ( const char * name ); 采用如下命令进行编译 gcc hello.c -fPIC -shared -Wl,-soname,libhello.so.0 -o libhello.so.0.0.1 需要注意的参数是 -Wl,soname (中间没有空格), -Wl 选项告诉编译器将后面的参数传递给链接器, -soname则指定了动态库的soname(简单共享名,Short for shared object name) 现在我们生成了libhello.so.0.0.1,当我们运行 ldconfig -n . 命令时,当前目录会多一个软连接 $ ll libhello.so.0 lrwxrwxrwx 1 handy handy 17 8月 17 14:18 libhello.so.0 -> libhello.so.0.0.1 这个软链接是如何生成的呢,并不是截取libhello.so.0.0.1名字的前面部分,而是根据libhello.so.0.0.1编译时指定的-soname生成的。也就是说我们在编译动态库时通过-soname指定的名字,已经记载到了动态库的二进制数据里面。不管程序是否按libxxx.so.a.b.c格式命名,但Linux上几乎所有动态库在编译时都指定了-soname,我们可以通过readelf工具查看soname,比如文章开头列举的两个动态库 $ readelf -d /lib64/libacl.so.1.1.0 Dynamic section at offset 0x6de8 contains 24 entries: Tag Type Name/Value 0x0000000000000001 ( NEEDED ) Shared library: [ libattr.so.1 ] 0x0000000000000001 ( NEEDED ) Shared library: [ libc.so.6 ] 0x000000000000000e ( SONAME ) Library soname: [ libacl.so.1 ] 这里省略了一部分,可以看到最后一行SONAME为libacl.so.1,所以/lib64才会有一个这样的软连接 再看libc-2.12.so文件,该文件并没有采用我们说的命名方式 $ readelf -d /lib64/libc-2.12.so Dynamic section at offset 0x18db40 contains 27 entries: Tag Type Name/Value 0x0000000000000001 ( NEEDED ) Shared library: [ ld-linux-x86-64.so.2 ] 0x000000000000000e ( SONAME ) Library soname: [ libc.so.6 ] 同样可以看到最后一行SONAME为libc.so.6,即便该动态库没有按版本号的方式命名,但仍旧有一个软链指向该动态库,而该软链的名字就是soname指定的名字 所以关键就是这个soname,它相当于一个中间者,当我们的动态库只是升级一个小版本时,我们可以让它的soname相同,而可执行程序只认soname指定的动态库,这样依赖这个动态库的可执行程序不需重新编译就能使用新版动态库的特性 可执行程序的编译 还是以hello动态库为例,我们写一个简单的程序 // filename:main.c #include \"hello.h\" int main () { hello ( \"handy\" ); return 0 ; } 现在目录下是如下结构 ├── hello.c ├── hello.h ├── libhello.so.0 -> libhello.so.0.0.1 ├── libhello.so.0.0.1 └── main.c libhello.so.0.0.1是我们编译生成的动态库,libhello.so.0是通过ldconfig生成的链接,采用如下命令编译main.c $ gcc main.c -L. -lhello -o main /usr/bin/ld: cannot find -lhello 报错找不到hello动态库,在Linux下,编译时指定-lhello,链接器会去寻找libhello.so这样的文件,当前目录下没有这个文件,所以报错。建立这样一个软链,目录结构如下 ├── hello.c ├── hello.h ├── libhello.so -> libhello.so.0.0.1 ├── libhello.so.0 -> libhello.so.0.0.1 ├── libhello.so.0.0.1 └── main.c 让libhello.so链接指向实际的动态库文件libhello.so.0.0.1,再编译main程序 gcc main.c -L. -lhello -o main 这样可执行文件就生成了。通过以上测试我们发现, 在编译可执行程序时,链接器会去找它依赖的libxxx.so这样的文件,因此必须保证libxxx.so的存在 用ldd查看其依赖的动态库 $ ldd main linux-vdso.so.1 => (0x00007fffe23f2000) libhello.so.0 => not found libc.so.6 => /lib64/libc.so.6 (0x00007fb6cd084000) /lib64/ld-linux-x86-64.so.2 (0x00007fb6cd427000) 我们发现main程序依赖的动态库名字是libhello.so.0,既不是libhello.so也不是libhello.so.0.0.1。其实在生成main程序的过程有如下几步 链接器通过编译命令 -L. -lhello 在当前目录查找libhello.so文件 读取libhello.so链接指向的实际文件,这里是libhello.so.0.0.1 读取libhello.so.0.0.1中的SONAME,这里是libhello.so.0 将libhello.so.0记录到main程序的二进制数据里 也就是说libhello.so.0是已经存储到main程序的二进制数据里的,不管这个程序在哪里,通过ldd查看它依赖的动态库都是libhello.so.0 而为什么这里ldd查看main显示libhello.so.0为not found呢,因为ldd是从环境变量\\$LD_LIBRARY_PATH指定的路径里来查找文件的,我们指定环境变量再运行如下 $ export LD_LIBRARY_PATH=. && ldd main linux-vdso.so.1 => (0x00007fff7bb63000) libhello.so.0 => ./libhello.so.0 (0x00007f2a3fd39000) libc.so.6 => /lib64/libc.so.6 (0x00007f2a3f997000) /lib64/ld-linux-x86-64.so.2 (0x00007f2a3ff3b000) 可执行程序的运行 现在测试目录结果如下 ├── hello.c ├── hello.h ├── libhello.so -> libhello.so.0.0.1 ├── libhello.so.0 -> libhello.so.0.0.1 ├── libhello.so.0.0.1 ├── main └── main.c 这里我们把编译环境和运行环境混在一起了,不过没关系,只要我们知道其中原理,就可以将其理清楚 前面我们已经通过ldd查看了main程序依赖的动态库,并且指定了LD_LIBRARY_PATH变量,现在就可以直接运行了 $ ./main hello Handy! 看起来很顺利。那么如果我们要部署运行环境,该怎么部署呢。显然,源代码是不需要的,我们只需要动态库和可执行程序。这里新建一个运行目录,并拷贝相关文件,目录结构如下 ├── libhello.so.0.0.1 └── main 这时运行会main会发现 $ ./main ./main: error while loading shared libraries: libhello.so.0: cannot open shared object file: No such file or directory 报错说libhello.so.0文件找不到,也就是说 程序运行时需要寻找的动态库文件名其实是动态库编译时指定的SONAME ,这也和我们用ldd查看的一致。通过 ldconfig -n . 建立链接,如下 ├── libhello.so.0 -> libhello.so.0.0.1 ├── libhello.so.0.0.1 └── main 再运行程序,结果就会符合预期了 从上面的测试看出,程序在运行时并不需要知道libxxx.so,而是需要程序本身记载的该动态库的SONAME,所以main程序的运行环境只需要以上三个文件即可 动态库版本更新 假设动态库需要做一个小小的改动,如下 // filename:hello.c #include <stdio.h> void hello ( const char * name ) { printf ( \"hello %s, welcom to our world! \\n \" , name ); } 由于改动较小,我们编译动态库时仍然指定相同的soname gcc hello.c -fPIC -shared -Wl,-soname,libhello.so.0 -o libhello.so.0.0.2 将新的动态库拷贝到运行目录,此时运行目录结构如下 ├── libhello.so.0 -> libhello.so.0.0.1 ├── libhello.so.0.0.1 ├── libhello.so.0.0.2 └── main 此时目录下有两个版本的动态库,但libhello.so.0指向的是老本版,运行 ldconfig -n . 后我们发现,链接指向了新版本,如下 ├── libhello.so.0 -> libhello.so.0.0.2 ├── libhello.so.0.0.1 ├── libhello.so.0.0.2 └── main 再运行程序 $ ./main hello Handy, welcom to our world! 没有重新编译就使用上了新的动态库, wonderful! 同样,假如我们的动态库有大的改动,编译动态库时指定了新的soname,如下 gcc hello.c -fPIC -shared -Wl,-soname,libhello.so.1 -o libhello.so.1.0.0 将动态库文件拷贝到运行目录,并执行 ldconfig -n . ,目录结构如下 ├── libhello.so.0 -> libhello.so.0.0.2 ├── libhello.so.0.0.1 ├── libhello.so.0.0.2 ├── libhello.so.1 -> libhello.so.1.0.0 ├── libhello.so.1.0.0 └── main 这时候发现,生成了新的链接libhello.so.1,而main程序还是使用的libhello.so.0,所以无法使用新版动态库的功能,需要重新编译才行 最后 在实际生产环境中,程序的编译和运行往往是分开的,但只要搞清楚这一系列过程中的原理,就不怕被动态库的版本搞晕。简单来说,按如下方式来做 编译动态库时指定 -Wl,-soname,libxxx.so.a ,设置soname为libxxx.so.a,生成实际的动态库文件libxxx.so.a.b.c, 编译可执行程序时保证libxx.so存在,如果是软链,必须指向实际的动态库文件libxxx.so.a.b.c 运行可执行文件时保证libxxx.so.a.b.c文件存在,通过ldconfig生成libxxx.so.a链接指向libxxx.so.a.b.c 设置环境变量LD_LIBRARY_PATH,运行可执行程序","tags":"Skill","title":"Linux程序编译链接动态库版本的问题"},{"url":"https://chukeer.github.io/一行Python代码——电话簿上的字符.html","text":"Question Given a digit string, return all possible letter combinations that the number could represent. A mapping of digit to letters (just like on the telephone buttons) is given below. Input : Digit string \"23\" Output : [ \"ad\" , \"ae\" , \"af\" , \"bd\" , \"be\" , \"bf\" , \"cd\" , \"ce\" , \"cf\" ]. Answer num2letter = { '1':'', '2':'abc', '3':'def', '4':'ghi', '5':'jkl', '6':'mno', '7':'pqrs', '8':'tuv', '9':'wxyz'} def letterCombinations(digits): return [] if digits == \"\" else reduce(lambda x, y:reduce(lambda a,b:a+b, [[i+j for i in x] for j in y], []), filter(lambda x:x!='', [num2letter[digit] for digit in digits]), ['']) 答案分析 我们先求得每个数字对应的字符串,然后求这些字符串能组合的所有情况,比如根据数字得到的字符为'abc', 'def','ghi',那么组合数是3 3 3=27种,具体思路就是先求'abc'和'def'的组合['ab','ae','af','bd','be','bf','cd','ce','cf'],再算和'ghi'的组合,以此类推,这本身是每什么难度的,在Python里正好有个内建函数是干这个事的,那就是reduce reduce的函数原型如下 def reduce(function, iterable, initializer=None): it = iter(iterable) if initializer is None: try: initializer = next(it) except StopIteration: raise TypeError('reduce() of empty sequence with no initial value') accum_value = initializer for x in it: accum_value = function(accum_value, x) return accum_value 我们举个例子来说明 reduce(lambda a,b:a+b, [1,2,3], 100) 这里返回的是100+1+2+3,lambda a,b:a+b定义了一个匿名函数,等价于 def add(a, b): return a + b 这个reduce函数的具体计算步骤如下 1. redult = 100 2. result = add(result, 1) # 101 3. result = add(result, 2) # 103 4. result = add(result, 3) # 106* 5. return result 现在再来分析答案中的letterCombinations函数 为了看得清晰,我们先使用一些中间变量,改写函数如下 def letterCombinations(digits): data = filter(lambda x:x!='', [num2letter[digit] for digit in digits]) return [] if digits == \"\" else reduce(lambda x, y:reduce(lambda a,b:a+b, [[i+j for i in x] for j in y], []), data, ['']) [num2letter[digit] for digit in digits] 根据digits获取对应字符串 filter(lambda x:x!='', [num2letter[digit] for digit in digits]) 过滤掉空字符串 return [] if digits == \"\" else something python的三元组表达式,类似C的 condition ? value1 : value2 ,Python的形式为 value1 if condition else value2 ,这里意思是如果digits为空则返回[],否则返回后面的something,这里的something就是我们要重点分析的如下表达式 reduce(lambda x, y:reduce(lambda a,b:a+b, [[i+j for i in x] for j in y], []), data, ['']) 下面我们来一层层抽丝剥茧。先看外层的reduce,我们将其表示成如下 reduce(func(x,y), data, ['']) 这是这道题的核心解题思路,我们还是以例子来说明,假设data=['abc','def','ghi'],那么计算步骤应该是 1. result = [''] 2. result = func(result, 'abc') 3. result = func(result, 'def') 4. result = func(result, 'ghi') 5. return result 我们只需要实现func函数,问题就迎刃而解 其实func函数要做的就是将y里的每个字符添加到x里的每个字符串的末尾,比如 x=['ab', 'cd'] y='ef' 那么func(x,y)应该返回 ['abe','cde', 'abf', 'cdf'] ,然后就是求func函数的实现 很容易我们想到列表解析,这里需要两层嵌套如下 def func(x,y): return [[i + j for i in x] for j in y] 但是这样的结果是[['abe', 'cde'], ['abf', 'cdf']],此时我们还需要将列表的每个元素(注意,元素类型也为列表)连接起来,这不就是元素求和吗,只是这里的元素是列表而已,我们以文章开头reduce为参考 reduce(lambda a,b:a+b, [1,2,3], 100) 只需稍作替换即可 reduce(lambda a,b:a+b, [[i + j for i in x] for j in y], []) 这样func(x,y)函数的实现就成了下面这样 def func(x,y): return reduce(lambda a,b:a+b, [[i + j for i in x] for j in y, []) 我们再将最外层reduce中的func函数改写成lambda表达式 reduce(lambda x,y:reduce(lambda a,b:a+b, [[i + j for i in x] for j in y], []), data, ['']) 大功告成!","tags":"Language","title":"一行Python代码——电话簿上的字符"},{"url":"https://chukeer.github.io/一行Python代码——单词逆转.html","text":"Question Given an input string, reverse the string word by word. For example, Given s = \" the sky is blue \", return \" blue is sky the \". Answer def reverseWords ( s ) : return ' ' . join ( filter ( lambda x : x ! = '' , s . split ( ' ' ))[ ::- 1 ]) 详解: s.split(' ') 将字符串s按空格为分隔符进行分割,生成列表,注意,如果有连续空格,列表里会有空元素 lambda x:x!='' 定义一个匿名函数,等价于如下函数 def func(x): return x != '' filter(lambda x:x!='', s.split(' ')) 由第1条可知,s.split(' ')是一个列表,lambda x:x!=''是一个函数,filter函数将列表的每个元素依次作为参数传递给lambda函数,如果函数返回true,则该元素被保留,否则被过滤。这里可以过掉列表里为空的元素 filter(lambda x:x != '', s.split(' '))[::-1] 列表元素顺序逆转 ' '.filter(lambda x:x != '', s.split(' '))[::-1] 用' '连接列表的每个元素","tags":"Language","title":"一行Python代码——单词逆转"},{"url":"https://chukeer.github.io/vim比较目录diff.html","text":"虽然现在有很多图形界面的diff工具,但对于有命令行情节的人来说,当飞快的在terminal下敲击键盘时,总不希望再拿鼠标去点击其它地方,况且有时候图形界面占用资源多,我的MBA就经常启动diffmerge时卡住,但vimdiff又只能在一个标签里比较一组文件的diff,如果想比较两个目录下文件的diff,它就显得无能为力了 假设我们要实现一个工具叫diffdir,先让我们脑洞打开设想一下它应该是怎样的 我希望能列出两个目录下文件名相同但内容不同的所有文件,并进行编号 我希望通过选择编号,打开需要比较diff的文件 如果想比较多组文件的diff,我希望每个vim标签打开一组文件比较 最好能过滤掉非文本文件,因为我不希望用vim打开一对二进制乱码 最好还能有交互,我可以选择只查看我感兴趣的文件,而不是一次打开所有文件的diff,当退出vim时我还可以继续选择 假设有两个目录分别是A和B,目录结构如下 A ├── file1 ├── file2 └── file3 B和A目录结构以及对应文件名都相同,其中file1和file2的内容不同,file3内容相同,那么当我们运行 diffdir A B 时,它应该是这样的界面 当我们选择编号1时,vim会打开一个标签对比两个目录下file1的差异 当我们选择 1,2 或 1-2 时,vim会打开两个标签分别比较file1和file2的差异 由于这个例子有diff的文件数量较少,我们还可以选择a一次打开所有文件的diff 如果diff文件个数较多,我们可以分批打开,并且当我们退出vim后还可以继续选择 接下来是实现 vim比较文件diff 我们都知道vimdiff的用法,其实 vimdiff A/file1 B/file1 等价于 vim -d A/file1 B/file2 ,又或者更原始一点,我们可以分两步来比较两个文件的diff 执行 vim A/file1 在normal模式下输入 :vertical diffsplit B/file1 虽然人们不会用这么麻烦的命令去比价文件的diff,但往往最基本的命令反而能组合出更多的功能,就像搭积木一样,我们只需要几个基本的形状,就可以通过自己的想象搭建多彩的世界,而vim的这些基本命令就像积木一样,我们要做的是利于好这些积木 vim在新标签比较文件diff 假设我们已经用上面的命令打开了vim并比较file1的diff,如果我们希望新建一个标签来比较file2的diff呢,还是要用到基本的ex命令 在normal模式下执行 :tabnew A/file2 在normal模式下执行 :vertical diffsplit B/file1 vim批量执行命令 以上两个示例就是我们需要的积木,有了积木,我们就可以组合出强大的命令,现在要做的是同时打开两组文件的diff,并且每个标签一组diff 通过查看vim帮助我们发现vim有如下两个参数 -c <command> 加载第一个文件后执行 <command> -S <session> 加载第一个文件后执行文件 <session> 这两个参数都可以让vim启动时执行一些命令,其中-c是从参数读取命令,-S是从文件读取命令,于是我们就可以将需要执行的命令存入文件,启动vim时通过-S参数加载该文件,就能达到我们批量执行命令的目的。假设我们需要打开两个标签,分别比较A,B目录下file1和file2的diff,事先创建vim.script如下(文件名随意,最好采用绝对路径,以免受到vim配置里autochdir的影响) edit A/file1 vertical diffsplit B/file1 tabnew A/file2 vertical diffsplit B/file2 然后执行 vim -S vim.script ,看看是否如你所愿,打开了两个标签,分别比较file1和file2的diff。注意,为了 最终实现 既然有了这些积木,那我们就可以灵活的根据需要编写脚本实现我们的需求,下面是我最终的实现,也可以在github上查看源码 https://github.com/handy1989/vim/blob/master/diffdir 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 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 #!/bin/bash if [ $# -ne 2 ] ; then echo \"Usage: $0 dir1 dir2\" exit 1 fi if [ ! -d $1 -o ! -d $2 ] ; then echo \" $1 or $2 is not derectory!\" exit 1 fi ## 注意,Mac的readlink程序和GNU readlink功能不同,Mac需要下载greadlink arg1 = ` greadlink -f $1 ` arg2 = ` greadlink -f $2 ` tmp_dir = /tmp/tmp. $$ rm -rf $tmp_dir mkdir -p $tmp_dir || exit 0 #echo $tmp_dir trap \"rm -rf $tmp_dir ; exit 0\" SIGINT SIGTERM ## 注意,Mac和Linux的MD5程序不同,请根据需求使用,这里是Mac版的用法 function get_file_md5 { if [ $# -ne 1 ] ; then echo \"get_file_md5 arg num error!\" return 1 fi local file = $1 md5 $file | awk -F \"=\" '{print $2}' } function myexit { rm -rf $tmp_dir exit 0 } function show_diff { if [ $# -ne 1 ] ; then return 1 fi local diff_file = $1 echo \"diff file:\" printf \" %-55s %-52s\\n\" $arg1 $arg2 if [ -f $tmp_dir /A_ony_file ] ; then awk '{printf(\" [%2d] %-50s\\n\", NR, $1)}' $tmp_dir /A_ony_file python -c 'print \"-\"*100' fi awk '{printf(\" [%2d] %-50s %-50s\\n\", NR, $1, $1)}' $diff_file echo \"(s):show diff files (a):open all diff files (q):exit\" echo } function check_value { local diff_file = $1 local value = $2 tmp_file = $tmp_dir /tmp_file > $tmp_file for numbers in ` echo \" $value \" | tr ',' ' ' ` do nf = ` echo \" $numbers \" | awk -F \"-\" '{print NF}' ` if [ $nf -ne 1 -a $nf -ne 2 ] ; then return 1 fi begin = ` echo \" $numbers \" | awk -F \"-\" '{print $1}' ` end = ` echo \" $numbers \" | awk -F \"-\" '{print $2}' ` if [ -z \" $end \" ] ; then sed -n $begin 'p' $diff_file >> $tmp_file else if [ \" $end \" -lt $begin ] ; then return 1 fi sed -n $begin ',' $end 'p' $diff_file >> $tmp_file fi if [ $? -ne 0 ] ; then return 1 fi done awk -v dir1 = $arg1 -v dir2 = $arg2 '{ if (NR==1) { printf(\"edit %s/%s\\nvertical diffsplit %s/%s\\n\", dir1, $0, dir2, $0) } else { printf(\"tabnew %s/%s\\nvertical diffsplit %s/%s\\n\", dir1, $0, dir2, $0) } }' $tmp_file } ############################################################# # 获取diff info ############################################################# for file in ` find $arg1 | grep -v \"/\\.\" | grep -v \"^\\.\" ` do file_relative_name = ${ file # $arg1 / } file $file | grep -Eq \"text\" if [ $? -ne 0 ] ; then continue fi if [ -f $arg2 / $file_relative_name ] ; then file $arg2 / $file_relative_name | grep -Eq \"text\" if [ $? -ne 0 ] ; then continue fi md5_1 = ` get_file_md5 $file ` md5_2 = ` get_file_md5 $arg2 / $file_relative_name ` if [[ \" $md5_1 \" = \" $md5_2 \" ]] ; then continue fi ## file not same echo \" $file_relative_name \" >> $tmp_dir /diff_file else echo \" $file_relative_name \" >> $tmp_dir /A_ony_file fi done ############################################################# # 根据输入标签打开用vim打开文件比较diff ############################################################# if [ ! -f $tmp_dir /diff_file ] ; then exit fi show_diff $tmp_dir /diff_file while true do echo -n \"Please choose file number list (like this:1,3-4,5):\" read value if [[ \" $value \" = \"s\" ]] || [[ \" $value \" = \"S\" ]] ; then show_diff $tmp_dir /diff_file continue elif [[ \" $value \" = \"q\" ]] || [[ \" $value \" = \"Q\" ]] ; then myexit elif [[ \" $value \" = \"a\" ]] || [ \" $value \" = \"A\" ] ; then value = \"1- $ \" fi vim_script = ` check_value $tmp_dir /diff_file \" $value \" 2>/dev/null ` if [ $? -ne 0 ] ; then echo \"invalid parameter[ $value ]!\" else vim -c \" $vim_script \" fi done","tags":"Skill","title":"vim比较目录diff"},{"url":"https://chukeer.github.io/vim自动更新tag.html","text":"用vim浏览C/C++代码时可以用ctags生成tag文件,这样可以很方便跳转到函数定义的地方,这个功能几乎所有的图形界面编辑器都有,比如VS,source insight等等,但是vim的tags文件是静态的,也就是说如果我们在源代码里新增了一些函数,原来的tags是不会自动更新的,我们也无法跳转到新增的函数定义处,这个问题怎么怎么办呢 我在网上搜索了很多地方,普遍给的方案就是将ctags命令映射到一个快捷键,这样只需要按一下快捷键就会生成新的tags文件,但这样有几个不方便的地方 每次tags文件都是全量生成,如果工程很大,生成tags文件可能需要十多秒,而运行命令的过程中是不能编辑文件的,也不可能每次修改文件都去更新tags vim下运行ctags命令其实是在命令模式下输入 !ctags -R . ,它会在vim的工作目录下生成tags文件,而如果你当前工作目录并不是你想要生成tags的目录,还得切换目录 总之是,想要自动更新tags,没有这么简单的事儿! 但是VS和source insight就可以做到,我们秉着凡是其它编辑器能实现的功能vim都能实现的原则来分析下问题的实质,其实不管是VS还是source insight它们都需要建立工程,然后将源代码导入工程,我们可以猜想到这些编辑器会对工程里的源文件建立索引,这样就可以实现各种跳转功能,当有代码更新或是新增源文件时,编辑器自然也可以检测到,这时它暗地里对源文件重新建立索引,我们就可以对新增的函数进行跳转。既然编辑器可以暗地里做很多事,我们为何不也这样呢,计算机的处理器大多数时候都是空闲的,不用白不用 试想我们在Linux下有一堆源代码,我们需要经常编辑和阅读这些代码,它的根目录结构应该是这样的 ├── auto_tags ├── build ├── common ├── include ├── libs ├── message ├── metadata ├── network ├── nodes ├── privacy ├── tags └── tests 其中tags文件是对当前目录下的代码生产的tag,其它目录存放的则是你的源代码,这样当我们打开目录下的源代码时,vim就可以根据tags文件来定位变量的位置 这里的auto_tags目录存放的是自己实现的脚本,它的功能是自动检测当前目录下的源代码是否有更新,如果有,则生产新的tags文件并替换老的,auto_tags目录结构如下 ├── auto_tags.conf ├── auto_tags.sh └── run_tags.sh auto_tags.conf为配置文件,配置项如下 CTAGS=/usr/bin/ctags # 生成tags文件存放的目录 tags_dir=../ # 源代码所在目录 source_root=../ # 需要创建tags的目录名,注意只有目录名字,不是路径 source_dirs=\"common message nodes\" # 日志相关 max_log_size=10 # Unit: Mb log_file=\"auto_tags.log\" 将source_dirs变量替换为你需要建立tags的目录名称即可,注意需要用双引号包围,且只写目录名字,不需要添加 ../ 使用方式如下 sh ./run_tags.sh start # 启动脚本 sh ./run_tags.sh stop # 停止脚本 核心脚本是auto_tags.sh,至于脚本是如何实现的就不贴出来了,毕竟这只是一个程序员自娱自乐实现的一个小小的功能,它并不完善但简单易用。github地址如下 https://github.com/handy1989/vim/tree/master/auto_tags","tags":"Skill","title":"vim自动更新tag"},{"url":"https://chukeer.github.io/Mac OS X下快速复制文件路径.html","text":"文件路径表示的是文件在文件系统里的位置,不管是用命令行操作的Linux程序员还是使用windows的广大用户,都难免会有这样的需求 在windows上复制当前目录的路径有一个特别方便的方式,只需要用鼠标点击路径栏,它就会自动变成像\"D:\\Downloads\\tmp\"这样的路径,如果要复制文件路径,只需要将目录路径和文件名拼接起来即可,Linux上也很方便, readlink -f yourfile 就会打印出文件的绝对路径,虽然Mac也可以在命令行下使用greadlink(Mac上自带的readlink和Linux上功能不一样,greadlink意思是GNU's readlink,和Linux上的readlink功能一致),但这种方式显然不是最佳解决方案,毕竟Mac是图形和命令行结合的最优组合,首选在图形界面下解决问题 搜了下百度,大致给了如下两种3方案 鼠标右键点击文件,选择 显示简介 可以查看文件所在的目录,这和windows上点击路径兰结果一致,但使用起来不太方便,因为这只显示了文件所在目录的路径,文件的绝对路径还需要加上文件名 将文件拖入浏览器,文件路径会显示在地址栏 打开文本terminal程序,将文件拖进去,路径会自动打印出来 以上方式各有优缺点,我所期望的最佳方案应该是鼠标右键点击文件,菜单直接出现 Copy path 的选项,点击之后文件路径就被复制到了剪贴板,是不是有一气呵成的感觉,其实Mac系统本身就为我们提供了这样的方式,只是需要一点小小的技巧,这里我们需要用到Automator 在应用程序文件夹里打开Automator,选择文件菜单,新建一个服务,如下 在左侧操作栏找到 拷贝至剪贴板 操作,拖到右侧,如下 有两个地方需要修改,\"服务\"收到选定为文件或文件夹,位于Finder,cmd+s保存,命名为Copy path,完事儿! 怎么使用呢?在Finder上随便找一个文件,鼠标点击右键,看看服务选项里是不是出现了Copy path选项,如下 点击之后文件路径就被复制到了剪贴板,找个地方粘贴一下看看结果是不是你想要的,如果想删除该服务,进入~/Library/Services/目录,删除对应的workflow文件即可 最后我要吐槽一下,这个问题困扰我好久,其实我一直想用Automator来解决这个问题,但Automator那么多操作我没有一个个细看,不管是用百度还是Google搜索Mac下复制文件路径这样的关键词,搜索到的方法永远只有我前面给的几个,直到有一天我把这个问题告诉了一个在国外的同学,他在Google上搜索copy file path in mac,一切迎刃而解 所以有时候不要怪搜索引擎,只怪我们自己没有给出最好的答案","tags":"Skill","title":"Mac OS X下快速复制文件路径"},{"url":"https://chukeer.github.io/Mac如何自动发邮件给kindle推送文档.html","text":"买过kindle的人一定对于它推送的服务印象深刻,只要你的kindle联网在,即便它被放在家里,你也可以在办公室给它发送书籍,等你回家就会发现,书籍已经自动下载好了,在不同平台下(Mac,windows等)都有相应的Send to kindle应用程序,有些程序是不支持中国亚马逊账户的,但我们可以采用通用的方式,通过邮件推送,可能有些人觉得发邮件很麻烦,但如果能实现自动发送邮件,你是否还这样觉得呢 首先需要准备如下几点 kindle绑定一个Amazon账号 在Amazon账户的个人文档设置里添加自己的邮箱,并获取kindle推送邮箱(为了防止你的kindle收到垃圾推送,只要自己添加的邮箱才能向你的kindle推送) 在Mac上创建邮件账号 假设Amazon账号为[email protected],个人邮件账号为[email protected](也可以用其它邮箱),kindle推送邮箱为[email protected],现在你在kindle上登录了[email protected],通过邮件推送的流程可以理解为如下几步 用[email protected]给[email protected]发送一封邮件,附件是需要推送的文档 亚马逊邮件服务器收到你发送的邮件,判断[email protected]可以向你的kindle推送文档,将文档经过处理(转格式等,比如doc转PDF)后发送给你的kindle设备 试想一下,我下载了电子书在电脑上,点击右键,选择发送给我的kindle,整个过程貌似和Amazon的Send to kindle程序做的事情一样,只不过我们是通过邮箱来实现的,这里要用到的就是Mac上特有的Automator,下面我们一步一步来讲解 kindle绑定Amazon账户 这一步很简单,很多人在买kindle之前就有Amazon账户,即便没有也没关系,打开kindle,进入主页,按如下顺序设置 点击右上角菜单键 -> 点击设置选项 -> 点击注册选项 -> 如果已有账号则直接注册,否则注册新账号 这样你的Amazon账号就和kindle绑定了 设置推送邮箱 登陆 z.cn ,在我的账户标签下选择管理我的设备和内容选项如下 在我的设备选项下找到自己的kindle邮箱,如下 然后在设备选项下找到如下内容,并添加你用来给kindle推送的邮箱 这样便获取到了kindle邮箱和用来给kindle推送的邮箱,比如我这里的kindle邮箱是[email protected],用来给kindle推送的邮箱是[email protected] 在Mac上登录邮箱 Mac有自带的邮件APP,只需先在系统偏好设置 -> 互联网账户里添加对应的邮箱就行,如下图 然后打开邮件APP,添加对应的账户,如下图 注意,如果添加的是QQ邮箱,对应密码是QQ邮箱独立密码,必须先去QQ邮箱进行设置,并打开imap和pop3的开关,具体方式是进入网页版QQ邮箱,点击设置,在账户标签下找到如下部分,并开启对应选项,如果事先没有开启,在开启时需要设置独立密码,设置独立密码之后记得检查各项开关是否开启 这样,个人推送账户也设置好了,我这里是[email protected],下面就是见证奇迹的时刻! 用Automator自动发送邮件 Automator是啥,下面是官方的解释 这是一款 OS X 附带的 Apple 应用程序,可让您创建工作流程来自动执行重复任务。Automator 可以配合许多其他应用程序使用,包括 Finder、Safari、\"日历\"、\"通讯录\"、Microsoft Office 和 Adobe Photoshop 简单来说就是它提供了一些基本的操作,你可以将其组合去自动实现一些简单的任务,比如自动发送邮件,给文件批量重命名,批量压缩照片等等,今天用到的是自动发送邮件 打开Automator,在顶部菜单栏点击文件,新建一个服务 然后设置成下图的样子 新建邮件信息和发送待发邮件两个操作是从左侧的操作栏直接拖过来的,注意顺序。画箭头的地方是必选的 设置服务作用对象为文件或文件夹 设置收件人为你的kindle邮箱 设置发件人地址 这个服务的意思很简单,当你选中一个文件时,点击对应的服务,它会新建一个邮件,将文件作为附件,其中收件人和发件人使用的是你指定的邮箱,然后再将邮件发出去,当然你也可以删除\"发送待发邮件\"这个标签,自己手动点击发送也可以 cmd+s存储服务,命名为Send to kindle,搞定! 验证 随便找一个电子书文档,点击鼠标右键,看看服务菜单里是不是出现了你刚才创建的服务,如下图 点击Send to kindle,该文件就推送到了你的kindle上,你可以在邮件APP的已发出邮件里找到你发送的内容 如果想删掉该服务,只需删掉对应的workflow文件即可,文件存储路径是~/Library/Services,在Finder上点击 前往 -> 前往文件夹 ,输入地址可以进入","tags":"Skill","title":"Mac如何自动发邮件给kindle推送文档"},{"url":"https://chukeer.github.io/shell命令技巧——文本去重并保持原有顺序.html","text":"简单来说,这个技巧对应的是如下一种场景 假设有文本如下 cccc aaaa bbbb dddd bbbb cccc aaaa 现在需要对它进行去重处理,这个很简单,sort -u就可以搞定,但是如果我希望保持文本原有的顺序,比如这里有两个aaaa,我只是希望去掉第二个aaaa,而第一个aaaa在bbbb的前面,去重后仍旧要在它前面,所以我期望的输出结果是 cccc aaaa bbbb dddd 当然,这个问题本身并不难,用C++或python写起来都很容易,但所谓杀机焉用牛刀,能用shell命令解决时,它永远都是我们的首选。答案在最后给出,下面说说我是如何想到这样 我们有时候想把自己的目录加入环境变量PATH时会在~/.bashrc文件中这样写,比如待加入的目录为\\$HOME/bin export PATH= $ HOME /bin: $ PATH 这样我们等于是在PATH追加了路径\\ \\(HOME/bin并让它在最前面被搜索到,但当我们执行 source ~/.bashrc 后,\\\\) HOME/bin目录就会被加入PATH,如果我们下次再添加一个目录,比如 export PATH= $ HOME /local/bin: $ HOME /bin: $ PATH 再执行 source ~/.bashrc 时,\\ \\(HOME/bin目录在PATH中其实会有两份记录,虽然这不影响使用,但对于一个强迫症来说,这是无法忍受的,于是问题就变成了,我们需要去掉\\) PATH里重复的路径,并且保持原有路径顺序不变,也就是原本谁在前面,去重后仍旧在前面,因为在执行shell命令时是从第一个路径开始查找的,所以顺序很重要 好了,说了这么多我们来揭示最终的结果,以文章开始的数据为例,假设输入文件是in.txt,命令如下 cat -n in.txt | sort -k2,2 -k1,1n | uniq -f1 | sort -k1,1n | cut -f2- 这些都是很简单的shell命令,下面稍作解释 cat -n in.txt : 输出文本,并在前面加上行号,以\\t分隔 sort -k2,2 -k1,1n : 对输入内容排序,primary key是第二个字段,second key是第一个字段并且按数字大小排序 uniq -f1 : 忽略第一列,对文本进行去重,但输出时会包含第一列 sort -k1,1n : 对输入内容排序,key是第一个字段并按数字大小排序 cut -f2- : 输出第2列及之后的内容,默认分隔符为\\t 大家可以从第一条命令开始,并依次组合,看看实际输出效果,那样便更容易理解了。对于$PATH中的重复路径又该如何处理呢,还是以前面的例子来说,只需在前后用tr做一下转换即可 export PATH= $ HOME /local/bin: $ HOME /bin: $ PATH export PATH=`echo $ PATH | tr ':' '\\n' | cat -n | sort -k2,2 -k1,1n | uniq -f1 | sort -k1,1n | cut -f2- | tr '\\n' ':'` 其实这样使用PATH会有个问题,比如我们执行了以上命令后,如果想去掉$HOME/bin这个路径,仅仅修改为如下内容是不够的 export PATH= $ HOME /local/bin: $ PATH export PATH=`echo $ PATH | tr ':' '\\n' | cat -n | sort -k2,2 -k1,1n | uniq -f1 | sort -k1,1n | cut -f2- | tr '\\n' ':'` 因为我们已经将 \\(HOME/bin加入了\\) PATH中,这样做并没有起到删除的作用,也许最好的方式还是自己清楚的知道所有路径,然后显示指定,而不是采取追加的方式 if (!document.getElementById('mathjaxscript_pelican_#%@#$@#')) { var align = \"center\", indent = \"0em\", linebreak = \"false\"; if (false) { align = (screen.width < 768) ? \"left\" : align; indent = (screen.width < 768) ? \"0em\" : indent; linebreak = (screen.width < 768) ? 'true' : linebreak; } var mathjaxscript = document.createElement('script'); var location_protocol = (false) ? 'https' : document.location.protocol; if (location_protocol !== 'http' && location_protocol !== 'https') location_protocol = 'https:'; mathjaxscript.id = 'mathjaxscript_pelican_#%@#$@#'; mathjaxscript.type = 'text/javascript'; mathjaxscript.src = location_protocol + '//cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS-MML_HTMLorMML'; mathjaxscript[(window.opera ? \"innerHTML\" : \"text\")] = \"MathJax.Hub.Config({\" + \" config: ['MMLorHTML.js'],\" + \" TeX: { extensions: ['AMSmath.js','AMSsymbols.js','noErrors.js','noUndefined.js'], equationNumbers: { autoNumber: 'AMS' } },\" + \" jax: ['input/TeX','input/MathML','output/HTML-CSS'],\" + \" extensions: ['tex2jax.js','mml2jax.js','MathMenu.js','MathZoom.js'],\" + \" displayAlign: '\"+ align +\"',\" + \" displayIndent: '\"+ indent +\"',\" + \" showMathMenu: true,\" + \" messageStyle: 'normal',\" + \" tex2jax: { \" + \" inlineMath: [ ['\\\\\\\\(','\\\\\\\\)'] ], \" + \" displayMath: [ ['$$','$$'] ],\" + \" processEscapes: true,\" + \" preview: 'TeX',\" + \" }, \" + \" 'HTML-CSS': { \" + \" styles: { '.MathJax_Display, .MathJax .mo, .MathJax .mi, .MathJax .mn': {color: 'inherit ! important'} },\" + \" linebreaks: { automatic: \"+ linebreak +\", width: '90% container' },\" + \" }, \" + \"}); \" + \"if ('default' !== 'default') {\" + \"MathJax.Hub.Register.StartupHook('HTML-CSS Jax Ready',function () {\" + \"var VARIANT = MathJax.OutputJax['HTML-CSS'].FONTDATA.VARIANT;\" + \"VARIANT['normal'].fonts.unshift('MathJax_default');\" + \"VARIANT['bold'].fonts.unshift('MathJax_default-bold');\" + \"VARIANT['italic'].fonts.unshift('MathJax_default-italic');\" + \"VARIANT['-tex-mathit'].fonts.unshift('MathJax_default-italic');\" + \"});\" + \"MathJax.Hub.Register.StartupHook('SVG Jax Ready',function () {\" + \"var VARIANT = MathJax.OutputJax.SVG.FONTDATA.VARIANT;\" + \"VARIANT['normal'].fonts.unshift('MathJax_default');\" + \"VARIANT['bold'].fonts.unshift('MathJax_default-bold');\" + \"VARIANT['italic'].fonts.unshift('MathJax_default-italic');\" + \"VARIANT['-tex-mathit'].fonts.unshift('MathJax_default-italic');\" + \"});\" + \"}\"; (document.body || document.getElementsByTagName('head')[0]).appendChild(mathjaxscript); }","tags":"Skill","title":"shell命令技巧——文本去重并保持原有顺序"},{"url":"https://chukeer.github.io/Linux技巧——用dd生成指定大小的文件.html","text":"我们在测试或调试的时候,有时候会需要生成某个size的文件,比如在测试存储系统时,需要将磁盘剩余空间减少5G,最简单的办法就是拷贝一个5G的文件过来,但是从哪儿去弄这样大小的文件呢,或许你想到随便找一个文件,不停的拷贝,最后合并,这也不失为一种办法,但是有了dd,你会更容易且更灵活的实现 我们来case by case的介绍dd的用法。先看第一个 生成一个大小为5G的文件,内容不做要求 命令如下 $ dd if = /dev/zero of = tmp.5G bs = 1G count = 5 解释一下这里用到的参数 if=FILE : 指定输入文件,若不指定则从标注输入读取。这里指定为/dev/zero是Linux的一个伪文件,它可以产生连续不断的null流(二进制的0) of=FILE : 指定输出文件,若不指定则输出到标准输出 bs=BYTES : 每次读写的字节数,可以使用单位K、M、G等等。另外输入输出可以分别用ibs、obs指定,若使用bs,则表示是ibs和obs都是用该参数 count=BLOCKS : 读取的block数,block的大小由ibs指定(只针对输入参数) 这样上面生成5G文件的命令就很好理解了,即从/dev/zero每次读取1G数据,读5次,写入tmp.5G这个文件 再看下面一个问题 将file.in的前1M追加到file.out的末尾 命令如下 $ file_out_size = ` du -b file.out | awk '{print $1}' ` $ dd if = ./file.in ibs = 1M count = 1 of = ./file.out seek = 1 obs = $file_out_size 这里ibs和obs设置为了不同的值,和前面的命令相比,只多了一个seek参数 seek=BLOCKS : 在拷贝数据之前,从输出文件开头跳过BLOCKS个block,block的大小由obs指定 命令的意思就是从file.in读取1个1M的数据块写入file.out,不过写入位置并不在file.out的开头,而是在1*$file_out_size字节偏移处(也就是文件末尾) 在此基础上再增加一个要求 将file.in的第3M追加到file.out的末尾 $ file_out_size = ` du -b file.out | awk '{print $1}' ` $ dd if = ./file.in skip = 2 ibs = 1M count = 1 of = ./file.out seek = 1 obs = $file_out_size 这里多了一个参数skip skip=BLOCKS : 拷贝数据前,从输入文件跳过BLOCKS个block,block的大小由ibs指定。这个参数和seek是对应的 上面命令的意思就是,从文件file.in开始跳过2 1M,拷贝1 1M数据,写入文件file.out的1*$file_out_size偏移处 这样基本的参数都介绍全了,无非就是设置输入输出文件以及各自的偏移,设置读写数据块大小和读取数据块个数,下面总结一下 输入参数: if skip ibs count 输出参数: of seek obs 最后来一道终极题。前面创建的都是null流,这次换一个 指定某个字符,创建一个全是这个字符的指定大小的文件。比如创建一个文件,大小为123456字节,每个字节都是字符A 这问题看似没什么意义,但有时候确实需要用到。比如我通过/dev/zero创建了一个1G的文件,但是出于测试需求我想修改中间100M数据,这时我需要创建一个100M的文件,将该文件写入到那个1G文件的指定位置,而这个100M的文件是不能从/dev/zero创建的,否则达不到修改的目的,这时候就需要这样的功能了 话不多说,直接上脚本,有了前面的基础,相信都能看得懂 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 31 32 #!/bin/bash if [ $# -ne 3 ] ; then echo \"usage : $0 character out_file file_size(Byte)\" exit 1 fi echo \" $1 \" | grep -q \"^[a-zA-Z] $ \" if [ $? -ne 0 ] ; then echo \"arg1 must be character\" exit 1 fi character = $1 out_file = $2 target_size = $3 # echo输出默认是带'\\n'字符的,所以需要通过dd指定输入字节数 echo \" $character \" | dd of = $out_file ibs = 1 count = 1 while true do cur_size = ` du -b $out_file | awk '{print $1}' ` if [ $cur_size -ge $target_size ] ; then break fi remain_size = $(( target_size- $cur_size )) if [ $remain_size -ge $cur_size ] ; then input_size = $cur_size else input_size = $remain_size fi dd if = $out_file ibs = $input_size count = 1 of = $out_file seek = 1 obs = $cur_size || exit 1 done 有了这些技巧,在对文件内容无要求的前提下,你就可以任意创建指定大小的文件,任意修改文件指定字节数,这会让某些测试场合变得非常方便","tags":"skill","title":"Linux技巧——用dd生成指定大小的文件"},{"url":"https://chukeer.github.io/如何让Magic Mouse真正拥有Magic.html","text":"Magic Mouse是苹果电脑的专属配件,它的庐山真面目就如题图一样,乍一看觉得除了外形像个鼠标,其它地方貌似都和普通的鼠标格格不入,起码连个最基本的滚轮都没有,但是既然敢叫Magic Mouse,必然有它的magical之处 它是为苹果电脑专门设计,如果你拿着它在windows上打游戏,或者在装了windows系统的Mac上打游戏,然后吐槽它不好用,那我只能送上一句呵呵,这种事貌似只有土豪才会能干得出来 Mac的触摸板是出了名的功能强大,多点触控多手势操作让windows pc的触摸板相形见绌,和苹果系统的深入结合足以让它胜任各个领域所有操作,但如果想要精准的移动和定位,或者只是想要一种点击的反馈感,这时候MM就显得很重要了,或许有人会说,普通鼠标也可以做到这一点啊,但如果我说MM能完成触摸板的所有手势操作,你还会这么想么 鼠标归根到底只是一种交互设备,我们先看一下普通的鼠标,它拥有左右键和中间的滚轮,和电脑的交互只能通过点击和滚动来实现,除了单击、双击、滚动以及相应的组合操作,你还能想到其它交互方式吗,除非你增加更多的按键,这样才能组合出更多的交互方式,很多游戏鼠标就是这么干的 再来看看MM,虽然它也可实现点击操作,但它没有滚轮,只有一个大大的面板,看起来好像组合方式更少,但正是由于有了这个大大的面板,它就好比一个触摸板一样,能感知不同的手势和动作,所以理论上凡是触摸板能完成的交互,MM都能完成,只是有个别操作没有使用触摸板那么自然,要达到这样的效果,就必须用到BTT——BetterTouchTool,如果你没有听过这个软件,那我不怪你会说MM不好用 这里要特别说明,BTT是免费软件。我这里只举几个简单的例子作为抛砖引玉,BTT的更多设置需要结合个人使用习惯。先来看BTT的设置界面 设置步骤如下 1. 选择操作的对象,可以对Magic Mouse,触摸板等进行操作 2. 选择动作执行的对象,可以是全局动作,也可以是针对某个应用的动作 3. 添加手势 4. 选择手势 5. 选择映射的快捷键或操作,二选一 我这里只举一个简单的例子。以前在windows上使用chrome时,我最喜欢的一个插件是鼠标手势插件,最常用的操作之一是快速滚到页面顶部和底部,通过按住鼠标右键,同时滚动滚轮来实现,按住右键往上滚是到顶部,往下滚是到底部,这样的交互非常自然,到了Mac平台,结合BTT,MM也能完成这样这样的快捷操作,甚至更强大。 参照上面的设置图,按如下方式设置 1. 选择操作对象为Magic Mouse 2. 选择动作执行对象为Global,如果你只想针对某个App实现该快捷操作,选择相应的App即可,点击下面的+号可以添加App 3. 点击\"Add New Gesture\" 4. 在Magic Mouse Gesture选项里选择Two Finger Gesture下的Two Finger Swipe Up 5. 在Predefined Action选项里选择Keyboard Keys下的End(End of the Page) 设置完之后不管你是在Safari还是在chrome下,在MM上双指往上滑动即可到达页面底部,使用非常自然,同理也可设置双指下滑到达页面顶部,由于这个手势是全局的,针对所有App都有效,而chrome插件只能针对chrome有效,所以App级别的API还是无法和系统级别的API相比较,就像你在Mac下查英文单词用有道取词,无论如何也没有系统自带的三指触摸取词方便 而这只是其中的一个设置,在Magic Mouse Gesture选项下,你可以看到所有手势操作,这些操作有些顾名思义,有些会有相应的说明,也很好理解,每个手势可以绑定一个键盘快捷键(Keyboard Shortcut)或指定操作,当然,并不是所有手势都需要用到,毕竟这么多谁也记不住,可以根据自己需求,在使用中如果发现某个操作非常频繁,就可以绑定到对应的手势。这里分享一下我的基本设置 全局设置 这里解释一下TipTap Right/Left操作,这需要两个手指配合操作,我通常是用食指和中指,TipTap Right是将食指放在鼠标表面,用中指轻敲(tap)鼠标表面,注意只是轻轻的拍打,不是将鼠标按键按下去,我绑定的是提高音量操作,这时候随着我中指的拍打,音量会一格一格变大,操作非常自然,TipTap Left则相反,将中指放在鼠标表面,食指轻敲鼠标,这个动作需要练习几次才会熟练,当我刚开始学会这个动作时,我带着耳机播放音乐反复做了好多次这个动作以感受那种自然而然又恰到好处的反馈 Safari设置 我只做了一个简单的设置如下,就不贴图了,描述如下 Two Finger Tap : CMD(⌘)+Click 这是一个键盘快捷操作,双指轻轻拍打,模拟cmd+click操作,在浏览网页时遇到链接想在后台打开,就可以用这个操作 MacVim设置 Single Finger Tap Right : ^] Single Finger Tap Left : ^o 熟悉vim的都知道,这两个快捷键分别是跳转到符号的定义处和回退到上次光标所在点,在浏览代码时我经常用这两个操作,对应的手势分别是轻拍鼠标的左边和右边,经过实践发现,轻怕左上角和右上角成功率比较高,这个操作比较容易误触到,但只在MacVim下设置,并且不涉及到修改操作,所以也没有多大影响 BTT的设置远不止这些,可以在使用过程中慢慢体会,有需要时再添加手势操作,切不可一上来就添加一大堆,最后连自己都忘了哪个对应哪个,另外,谨慎使用删除和关闭标签这类有修改作用的快捷键,否则误触到了会比较麻烦。我第一次用这个软件时没有体会到它的强大之处,折腾了一会儿就卸载了,等到我真正有需求再上网搜索时,看到所有人都推荐BTT,最后发现原来就是曾经被我抛弃的那个家伙,还好我又遇到了它,有了它,Magic Mouse才真正拥有Magic","tags":"Skill","title":"如何让Magic Mouse真正拥有Magic"},{"url":"https://chukeer.github.io/linux脚本实现自动输入密码.html","text":"使用Linux的程序员对输入密码这个举动一定不陌生,在Linux下对用户有严格的权限限制,干很多事情越过了权限就得输入密码,比如使用超级用户执行命令,又比如ftp、ssh连接远程主机等等,如下图 那么问题来了,在脚本自动化执行的时候需要输入密码怎么办?比如你的脚本里有一条scp语句,总不能在脚本执行到这一句时手动输入密码吧 针对于ssh或scp命令,可能有人会回答是建立信任关系,关于建立ssh信任关系的方法请自行百度Google,只需要两行简单的命令即可搞定,但这并不是常规的解决方案,如果是ftp连接就没辙了,况且,你不可能为了执行某些命令去给每个你要连接的主机都手动建立ssh信任,这已经偏离了今天主题的本意,今天要说的是在脚本里自动输入密码,我们可以想象下,更优雅的方式应该是在脚本里自己配置密码,当屏幕交互需要输入时自动输入进去,要达到这样的效果就需要用到expect 安装 CentOS下安装命令很简单,如下 sudo yum install expect 至于Mac用户,可以通过homebrew安装(需要先安装homebrew,请自行Google) brew install expect 测试脚本 我们写一个简单的脚本实现scp拷贝文件,在脚本里配置密码,保存为scp.exp如下 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 31 32 33 34 35 36 37 # !/usr/bin/expect set timeout 20 if { [llength $ argv ] < 2} { puts \"Usage:\" puts \" $ argv0 local_file remote_path\" exit 1 } set local_file [lindex $ argv 0] set remote_path [lindex $ argv 1] set passwd your_passwd set passwderror 0 spawn scp $ local_file $ remote_path expect { \"*assword:*\" { if { $ passwderror == 1 } { puts \"passwd is error\" exit 2 } set timeout 1000 set passwderror 1 send \" $ passwd \\r\" exp_continue } \"*es/no)?*\" { send \"yes\\r\" exp_continue } timeout { puts \"connect is timeout\" exit 3 } } 注意,第一行很重要,通常我们的脚本里第一行是 #!/bin/bash ,而这里是你机器上expect程序的路径,说明这段脚本是由expect来解释执行的,而不是由bash解释执行,所以代码的语法和shell脚本也是不一样的,其中 set passwd your_passwd 设置成你自己的密码,然后执行如下命令 ./scp.exp ./local_file user@host:/xx/yy/ 执行前确保scp.exp有执行权限,第一个参数为你本地文件,第二个为远程主机的目录,运行脚本如果报错\"connect is timeout\",可以把超时设长一点,第二行 set timeout 20 可以设置超时时间,单位是秒。脚本执行效果如下 还能做什么 细心的同学一定发现了,其实expect提供的是和终端的一种交互机制,输入密码只是其中一种应用形式,只要是在终端阻塞需要输入时,都可以通过expect脚本完成自动输入,比如前面脚本里配置了两种交互场景,一种是终端提示\"password:\"时输入密码,还有一种是提示\"yes/no)?\"时输入\"yes\",如果和远程主机是第一次建立连接,执行scp.exp脚本效果是这样的 所以我们可以根据终端的提示来配置输入命令,这样就能达到了自动化的效果。至于处理其它交互场景,只需要照着上面的脚本依葫芦画瓢就行了","tags":"Skill","title":"linux脚本实现自动输入密码"},{"url":"https://chukeer.github.io/网站二三事.html","text":"周五利用晚上的时间将博客换了个主机,今天就来谈谈与网站相关的一些事。先提前声明,今天的文章不涉及任何代码,如果是想知道网站怎么搭建,可以查看 http://macshuo.com/?p=547 ,我的博客就是按照这篇教程搭建的,讲的很详细。今天主要科普一下网站相关的知识 我们每个人每天都会访问各种网站,比如百度、新浪微博、QQ空间等,不管是这些大型网站,还是一些小的个人站点,比如我的 http://littlewhite.us ,他们的基本原理都是差不多的。搭建一个网站,首先需要这三样东西 域名 主机(也可以叫服务器) 相关服务(操作系统等) 域名是什么呢,就是网站的地址,比如www.baidu.com、www.sina.com等等,主机是这些网站内容存放的地方,当你访问一个网页时,其实是在访问它们主机上的内容,这些主机和我们的个人电脑不同,它们的主要功能是对外提供服务,不需要图形窗口,它们拥有更强劲的性能,当然价格也比我们所使用的个人电脑贵很多,不信你上京东搜搜服务器,价格动辄几千上万,而大型网站的主机往往是由若干机房组成,每个机房可能有几千台服务器,所以别看你访问百度只有那个简单的页面,其实它背后有成千上万的服务器在运转,机房最大的开销还不是买服务器花的钱,而是电费,因为这些大型服务器性能强劲,所以发热也很厉害,当几千台这样的机器放在一个一起时,如果不及时散热,机器分分钟就烧坏了,所以机房的空调是二十四小时不停运转的,每小时几千块的电费是家常便饭,我在百度时就遇到过一次某个数据机房出问题,导致数PB(1PB=1024TB=1024*1024GB,自己算算,总之PB是很大的单位)的数据丢失,原因是机房的空调坏了。。好了,扯远了,回到正题 那这些主机是如何被访问到的呢,首先它们得有一个公网IP,IP是互联网世界的门牌号,有了IP就知道这些主机在什么地方,比如百度的IP是119.75.217.56,你可以试试在浏览器地址栏输入这个IP,看看打开的是不是百度,但是我们在访问网站时,如果都是要通过IP访问,那实在是太为难用户了,本着用户至上的原则,互联网的那些先驱们就想出了一个办法,那就是通过域名去访问,但是通过域名怎么知道IP呢,别急,那些老家伙早就想到了,它们搞出了一个叫做DNS服务器的东西,专门负责告诉你域名和IP的对应关系,所以我们访问网站的流程是这样的 在浏览器输入www.baidu.com 浏览器请求DNS服务器,得到IP地址为119.75.217.56 浏览器像119.75.217.56发起请求 119.75.217.56收到请求,返回数据 整个流程简化一些大致就是这样的。我们可以将互联网世界和现实世界做一个类比,那些具有公网IP的服务器就是一个商店,它们的IP地址就是商店的具体地址,详细到城市街道门牌号,网站的域名就是商店的名字,要想找到这个商店你得知道它的门牌号,但是我们往往我只记住商店的名字,然后再通过某些手段(比如通过地图搜索)得到商店的具体门牌号,这样我们就能找到商店了。小型的网站就好比小卖部,大的网站就好比商场,到达那里的方式都是一样的 最后是主机上的服务,就好比商店的装修和商品,现在互联网上的主机大多数都使用的Linux操作系统,当然也有windows操作系统的,但是windows服务器的在互联网的分量就好比windows phone在移动设备的分量一样,属于小众型的。Linux是什么,普通用户不必了解,只需要知道它很牛逼,几乎整个互联网就是搭建在它之上的,更重要的是,这么牛逼的东西,它竟然还是免费的,要知道你每买一台windows笔记本,这其中有几百块钱是花在了操作系统上,而Linux和互联网免费共享的精神是共通的,这也足见它的伟大之处。除了操作系统,还需要一些提供服务的程序,比如nginx,MySQL,PHP等,这里就补详说了 最后说一下搭建一个简单的网站需要做哪些事。还是以刚刚的类比来描述,首先你得买一个商店(主机),买了之后你就知道了它的具体地址(公网IP),然后你要去给商店买一个名字(域名),并且去办理注册手续(设置域名解析),如果你是在国外注册的,那恭喜你,装修一下商店挂个牌子就可以用了,如果是在国内注册的,你得去工商局备案,以免你干了什么坏事天朝好捉拿你归案 好了,大致就是这些,希望能让用户对网站有一些感性的认识","tags":"view","title":"网站二三事"},{"url":"https://chukeer.github.io/没有main函数的helloworld.html","text":"几乎所有程序员的第一堂课都是学习helloworld程序,下面我们先来重温一下经典的C语言helloworld /* hello.c */ #include <stdio.h> int main () { printf ( \"hello world! \\n \" ); return 0 ; } 这是一个简单得不能再单的程序,但它包含有一个程序最重要的部分,那就是我们在几乎所有代码中都能看到的main函数,我们编译成可执行文件并查看符号表,过滤出里面的函数如下(为了方便查看我手动调整了grep的输出的格式,所以和你的输出格式是不一样的) $ gcc hello.c -o hello $ readelf -s hello | grep FUNC Num: Value Size Type Bind Vis Ndx Name 27: 000000000040040c 0 FUNC LOCAL DEFAULT 13 call_gmon_start 32: 0000000000400430 0 FUNC LOCAL DEFAULT 13 __do_global_dtors_aux 35: 00000000004004a0 0 FUNC LOCAL DEFAULT 13 frame_dummy 40: 0000000000400580 0 FUNC LOCAL DEFAULT 13 __do_global_ctors_aux 47: 00000000004004e0 2 FUNC GLOBAL DEFAULT 13 __libc_csu_fini 48: 00000000004003e0 0 FUNC GLOBAL DEFAULT 13 _start 51: 0000000000000000 0 FUNC GLOBAL DEFAULT UND puts@@GLIBC_2.2.5 52: 00000000004005b8 0 FUNC GLOBAL DEFAULT 14 _fini 53: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __libc_start_main@@GLIBC_ 58: 00000000004004f0 137 FUNC GLOBAL DEFAULT 13 __libc_csu_init 62: 00000000004004c4 21 FUNC GLOBAL DEFAULT 13 main 63: 0000000000400390 0 FUNC GLOBAL DEFAULT 11 _init 大家都知道用户的代码是从main函数开始执行的,虽然我们只写了一个main函数,但从上面的函数表可以看到还有其它很多函数,比如_start函数。实际上程序真正的入口并不是main函数,我们以下面命令对hello.c代码进行编译 $ gcc hello.c -nostdlib /usr/bin/ld: warning: cannot find entry symbol _start ; defaulting to 0000000000400144 -nostdlib命令是指不链接标准库,报错说找不到entry symbol _start,这里是说找不到入口符号_start,也就是说程序的真正入口是_start函数 实际上main函数只是用户代码的入口,它会由系统库去调用,在main函数之前,系统库会做一些初始化工作,比如分配全局变量的内存,初始化堆、线程等,当main函数执行完后,会通过exit()函数做一些清理工作,用户可以自己实现_start函数 /* hello_start.c */ #include <stdio.h> #include <stdlib.h> _start ( void ) { printf ( \"hello world! \\n \" ); exit ( 0 ); } 执行如下编译命令并运行 $ gcc hello_start.c -nostartfiles -o hello_start $ ./hello_start hello world! 这里的-nostartfiles的功能是Do not use the standard system startup files when linking,也就是不使用标准的startup files,但是还是会链接系统库,所以程序还是可以执行的。同样我们查看符号表 $ readelf -s hello_start | grep FUNC Num: Value Size Type Bind Vis Ndx Name 20: 0000000000400350 24 FUNC GLOBAL DEFAULT 10 _start 21: 0000000000000000 0 FUNC GLOBAL DEFAULT UND puts@@GLIBC_2.2.5 22: 0000000000000000 0 FUNC GLOBAL DEFAULT UND exit@@GLIBC_2.2.5 现在就只剩下三个函数了,并且都是我们自己实现的,其中printf由于只有一个参数会被编译器优化为puts函数,在编译时加-fno-builtin选项可以关掉优化 如果我们在_start函数中去掉exit(0)语句,程序执行会出core,这是因为_start函数执行完程序就结束了,而我们自己实现的_start里面没有调用exit()去清理内存 好不容易去掉了main函数,这时又发现必须得有一个_start函数,是不是让人很烦,其实_start函数只是一个默认入口,我们是可以指定入口的 /* hello_nomain.c */ #include <stdio.h> #include <stdlib.h> int nomain () { printf ( \"hello world! \\n \" ); exit ( 0 ); } 采用如下命令编译 $ gcc hello_nomain.c -nostartfiles -e nomain -o hello_nomain 其中-e选项可以指定程序入口符号,查看符号表如下 $ readelf -s hello_nomain | grep FUNC Num: Value Size Type Bind Vis Ndx Name 20: 0000000000000000 0 FUNC GLOBAL DEFAULT UND puts@@GLIBC_2.2.5 21: 0000000000000000 0 FUNC GLOBAL DEFAULT UND exit@@GLIBC_2.2.5 22: 0000000000400350 24 FUNC GLOBAL DEFAULT 10 nomain 对比hello_start的符号表发现只是将_start换成了nomain 到这里我们就很清楚了,程序默认的入口是标准库里的_start函数,它会做一些初始化工作,调用用户的main函数,最后再做一些清理工作,我们可以自己写_start函数来覆盖标准库里的_start,甚至可以自己指定程序的入口","tags":"Language","title":"没有main函数的helloworld"},{"url":"https://chukeer.github.io/强符号和弱符号.html","text":"之前在 extern \"C\" 用法详解 中已经提到过符号的概念,它是编译器对变量和函数的一种标记,编译器对C和C++代码在生产符号时规则也是不一样的,符号除了本身名字的区别外,还有强符号和弱符号之分 我们先看一段简单的代码 /* test.c */ void hello () ; int main () { hello () ; return 0 ; } 很显然,这段代码是没法链接通过的,它会报错 undefined reference to hello ,说的是hello未定义,因为这里我们只声明了函数hello,而没有定义它。但是我们把代码稍作修改如下 __attribute__((weak)) void hello(); int main() { hello(); return 0; } 这时你会发现,编译链接都可通过,但是运行会报错,因为这时我们将hello声明为了弱符号,在链接时弱符号会被链接器当做0,执行一个地址为0的函数当然会报错,改为如下代码就不会报错了,只是它没有任何输出 __attribute__((weak)) void hello(); int main() { if(hello) hello(); return 0; } 编译器认为, 函数和初始化了的全局变量为强符号,未初始化的全局变量为弱符号 ,链接器在处理强符号和弱符号时有如下规则 不同目标文件中,不允许有同名的强符号 如果一个符号在某个目标文件中是强符号,在其它目标文件中为弱符号,选择强符号 如果一个符号在所有目标文件中都是弱符号,选择占用空间最大的,比如目标文件A中有double global_var,文件B中有int global_var,double占用8字节,大于int的4字节,A和B链接后,符号global占8字节 对此我们可以简单的验证一下,有如下两个文件 /* 1.c */ char global_var ; int main () { return 0 ; } /* 2.c */ int global_var ; 全局变量global_var在两个文件中都没有初始化,因此都是弱符号,执行编译命令 gcc 1.c 2.c ,用readelf查看符号表 readelf -s a.out ,为了查看方便我们只输出最后几行 Num : Value Size Type Bind Vis Ndx Name 62 : 0000000000600818 4 OBJECT GLOBAL DEFAULT 25 global_var 63 : 0000000000400474 11 FUNC GLOBAL DEFAULT 13 main 64 : 0000000000400358 0 FUNC GLOBAL DEFAULT 11 _init 这里符号global_var占用的size是4,说明链接器选择的是占用空间更大的int global_var,我们再稍作修改,将1.c中的全局变量初始化,如下 /* 1.c */ char global_var = 1 ; int main () { return 0 ; } /* 2.c */ int global_var ; 这时1.c中的global_var为强符号,2.c中的global_var为弱符号,同样编译之后用readelf查看符号表 readelf -s a.out 如下 Num : Value Size Type Bind Vis Ndx Name 62 : 0000000000600818 1 OBJECT GLOBAL DEFAULT 25 global_var 63 : 0000000000400474 11 FUNC GLOBAL DEFAULT 13 main 64 : 0000000000400358 0 FUNC GLOBAL DEFAULT 11 _init 此时符号global_var占用的size是1,说明链接器选择的是强符号 在写代码时应该尽量避免有不同类型的符号,否则会引发非常诡异且不易察觉的错误,为了避免可以采取如下措施: 上策:消除所有的全局变量 中策:将全局变量声明为static类型,并提供接口供访问 下策:全局变量一定要初始化,哪怕初始化为0 必备:打开gcc的-fno-common选项,它会禁止有不同类型的符号 说了这么多,好像在说应该尽量用强符号,那弱符号有什么用呢,所谓存在即合理,有时候我们甚至需要显示定义弱符号,这对库函数会非常有用,比如库中的弱符号可以被用户自定义的强符号覆盖,从而实现自定义的库版本,或者在使用某些扩展功能时,用户可以定义一个弱符号,当链接了该功能时,功能模块可以正常使用,如果去掉功能模块,程序也可正常链接,只是缺少某些功能而已,比如我们可以通过下面的代码判断程序是否链接了pthread库,从而决定执行什么样的操作 /* test.c */ #include <stdio.h> #include <pthread.h> __attribute__ (( weak )) int pthread_create ( pthread_t * , const pthread_attr_t * , void * ( * )( void * ), void * ); int main () { if ( pthread_create ) { printf ( \"This is multi-thread version! \\n \" ); } else { printf ( \"This is single-thread version! \\n \" ); } return 0 ; } 编译运行结果如下 $ gcc test.c $ ./a.out This is single-thread version! $ gcc test.c -lpthread $ a.out This is multi-thread version! EOF 本文参考: 《程序员的自我修养》3.5.5章节 http://blog.csdn.net/astrotycoon/article/details/8008629","tags":"Language","title":"强符号和弱符号"},{"url":"https://chukeer.github.io/Mac必备软件推荐.html","text":"随着IOS的流行,Mac电脑也越来越多的进入人们的视野,和iPhone系列一样,苹果的Mac产品线也是软硬件完美结合,有着非同凡响的使用体验,而这主要的功劳,当属其操作系统Mac OS X,今天就来推荐一些Mac必备软件 首先要声明一点,OS X系统的很多软件和IOS一样,都是收费的,国人惯用了微软的盗版系统和大量windows盗版软件,转到Mac平台会有少许不适,当然Mac平台也有破解版软件,但本着程序员的良心,本文不会贴出破解软件的下载链接,对于收费软件也会专门指出,经济条件允许的同学,希望能多多支持正版。我主要是站在程序员的角度推荐软件,所以像QQ、搜狗输入法之类的日常软件不在推荐之列,当然,有些软件也适合普通用户,而且是强烈推荐,希望读者能各取所需 必备 Alfred 用神器来形容这款软件一点都不为过,至少我在windows平台还没用过让我这么舒适的软件 功能介绍 初级功能:搜索并打开软件与文件 高级功能:自定义搜索、通过插件实现特殊功能 Alfred的唤出方式为option+空格,下面的所有操作都是先按option+空格再输入的。Alfred的设计理念是将所有操作都集中到一个入口,这个很类似Linux的shell命令,不管你在任何目录下,所有系统命令都可以通过命令行输入使用,这可以省去你大量的查找和定位时间 搜索软件 有了Alfred,你不用去整理安装过的软件,只要你记得它的名字,或者哪怕是一个字母,都可以快速定位并打开软件,比如我要打开QQ,输入qq,它就会给我这样的选项 通过方向键选择软件,回车可以打开选中的软件,或者通过 cmd+数字 打开对应的软件,它会根据你每次的选择来自动对结果进行排序,因为我经常通过这种方式打开企业QQ,而我的QQ是直接在dock栏打开,所以企业QQ会排在QQ的前面,另外,它搜索软件时会通过两种方式进行匹配,一种是软件名,一种是软件对应的文件名,比如企业QQ的软件名是\"企业QQ\",而它的文件名是\"EIM.app\",这两种方式都可以用来定位并且对中文支持良好 搜索文件 搜索文件的方式大同小异,先输入空格,默认就会搜索文件,比如我输入 空格+python 就会有如下的搜索结果,回车打开文件,cmd+回车打开Finder进入文件所在目录 自定义网页搜索 接下来我要推荐它的自定义搜索功能,先看图 这里我输入 jd iphone ,回车之后就会跳转到京东的iphone搜索页面,也就是这个链接 http://search.jd.com/Search?keyword=iphone&enc=utf-8 ,这里用到了Alfred的web search功能,这需要自己进行配置,配置方式也很简单,打开Alfred的配置界面( option+空格 打开Alfred, cmd+, 打开配置项),在feature菜单中选择web search一项,点击右下角的Add Custom Search,按下图配置 最重要的是Search URL一栏,前面已经说过,京东搜索关键词iphone的链接是 http://search.jd.com/Search?keyword=iphone&enc=utf-8 ,这里我们只需要将链接中的iphone替换成{query}即可,这个链接是怎么发现的呢,很简单,你打开京东,随便输入一个关键词进行搜索(最好是搜英文,中文在URL中会被转码),看一下你输入的词在URL中的哪个地方,替换成{query}就可以了,下图是我自定义的一些搜索以及对应的链接 京东 :http://search.jd.com/Search?keyword={query}&enc=utf-8 百度 :http://www.baidu.com/s?wd={query} bt天堂 :http://www.bttiantang.com/s.php?q={query} 豆瓣电影:http://movie.douban.com/subject_search?search_text={query} 淘宝 :http://s.taobao.com/search?q={query} 有了这个,你就可以在任何界面下快速进行搜索,比如你在看一个PDF文档发现一个专有名词想用百度搜索,这时你无须打开浏览器进入百度再输入关键词,而是 option+空格 打开Alfred,输入 bd 你想要的balabala 就可以快速搜索 以上功能都是免费的!应付日常使用完全够了,如果想用高级功能,比如通过编写插件完成更复杂的动作,就需要升级到专业版,个人觉得免费版就已经够用了,除非你想深入研究这个东东的使用 效率 BetterTouchTool 这是一款免费软件,可以自定义触摸板和鼠标操作,添加操作的步骤如下 1. 选择操作的对象,可以对Magic Mouse,触摸板等进行操作 2. 选择动作执行的对象,可以是全局动作,也可以是针对某个应用的动作 3. 添加手势 4. 选择手势 5. 选择映射的快捷键或操作,二选一 这个软件全是英文说明,需要一点耐心来看,不过都是一些简单句子,相信英语过了四级的理解起来完全无压力。通过上图可以看到,我在全局范围添加了两个手势,分别轻按触摸板顶部中间位置和底部中间位置可以滚动到页面顶部或底部,滚动到页面顶部或底部是我在windows浏览器上最常用的鼠标手势,Mac下虽然没有那些浏览器插件和鼠标可用,但是通过这种方式我们可以实现同样的功能,甚至更加强大,这个动作是对所有软件都有效的! 同理,我们也可以对MagicMouse进行设置,注意必须是苹果的MagicMouse,普通鼠标是不支持的。MagicMouse的动作和触摸板会有所不同,细节就不说了,总之你可以将常用的操作全部集成到鼠标上,那时你就会明白为什么MagicMouse叫做MagicMouse。不了解MagicMouse的人会吐槽它很难用,了解的人只会暗自偷笑 另外,在Basic Settings标签下,建议将左下角的Enable Windows Snapping勾选上,这样可以实现和win7类似的将软件窗口拖到屏幕顶端实现放大的功能,除此之外,你还可以试试将软件窗口拖到屏幕左边、右边以及四个角落,看看是什么效果 AppClean 轻量级的卸载软件的工具,在windows下如果要卸载软件该如何操作?通过控制面板?那个太高端,很多普通用户都不会使用。通过360安全卫士?拜托,那简直就是一个杂货店,我只想要一瓶啤酒,它非得送我一包卫生纸。Mac下完全不需要像360安全卫士这样臃肿的软件,Unix软件设计的宗旨是只干一件事并做到极致,实现软件卸载,只需要AppClean就可以了 通过Alfred启动软件(现学现用嘛,option+空格唤出Alfred,输入cleaner,回车打开软件),如下图 它的搜索功能颜色比较淡,我好长时间才发现,通过搜索找到你要卸载的软件,或者直接在列表里找到,勾选之后点击右下角的Search按键,它会搜索出软件相关的目录,点击delete,搞定! 是不是觉得简单的不可思议,印象中windows下卸载一个软件得花老半天,其实卸载软件无非就是删除文件,在Mac下,软件包含的文件被有规律的组织在一起,这使得安装和卸载都变得异常简单 最后需要注意一点,AppCleaner的搜索功能只能对软件的文件名进行搜索,对于有些软件名和文件名不一致的,输入软件名是搜不到的,比如企业QQ的文件名是EIM.app,只能通过搜索EIM找到软件,或者浏览软件列表选中,至于怎么通过软件名得到文件名,试试Alfred:) PhoneClean 如果想深度清理系统垃圾,就需要用到这个软件,收费软件,这里不做过多介绍,使用起来非常容易 AndroidFileTransfer 浏览安装设备文件,无须多言 Windows Phone 同步WP设备文件,无须多言 开发 前面推荐的软件是适用于所有用户的,所以讲的比较详细,有些还贴出了使用步骤截图,下面介绍专门针对程序员的软件,由于程序员都有极强的动手能力和好奇心,所以下面的软件介绍都一笔带过,只做推荐,不做详解 Xcode IOS开发必备,即便不做IOS开发,也建议安装,它就像windows下的VS,可能其它软件使用时会依赖它,所以强烈建议安装,AppStore可免费下载 iTerm 终端模拟程序,虽然Mac自带Terminal程序,但这个更带感配置也更丰富,光看这个透明背景就让人醉了,更重要的是它是免费的! MacVim vim的GUI版,Mac专有,完美兼容vim所有插件以及语法,vim遇到Mac,是我用过的最好的编辑器! 想要最大发挥它的威力,前提是你必须是一个Vimer,建议先熟练使用vim后再转到MacVim Homebrew 二进制包管理工具,类似Ubuntu的apt-get和CentOS的yum。可以通过它安装很多Mac没提供或提供了但不好使的UNIX软件,比如ctags,wget,git等 官网可下载 http://brew.sh 安装brew $ ruby -e \" $( curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install ) \" 通过brew安装软件 $ brew install wget brew -h 查看详细使用说明 DiffMerge 文件/目录比较工具。虽然vim很强大,也可提供文件比较功能,但这种场景下图形界面会更直观 Mou 最后登场的是Mou,免费软件,基于Markdown语法的编辑器,我觉得我有必要专门花一篇文章来讲它,原因只有一个,我的所有博客都是用它来写的!但,今天就到这里了 EOF","tags":"Skill","title":"Mac必备软件推荐"},{"url":"https://chukeer.github.io/extern “C”用法详解.html","text":"今天是接着昨天谈extern的用法,纯技术贴。目前用户数以每天1-2的数量在增长,突破100不知何时到头啊,不过昨天的文章阅读数竟然超过了用户数,看来宣传宣传还是有用的,而且看到有更多人阅读,也给了我更大写作的动力,于是我决定不定期的在这里发放小米F码!周围有朋友有需求的赶紧号召过来关注哇,不过数量有限,每次发放一个,我会提前一天预告,第二天文章推送时将F码奉上,如果你看到F码并且也需要,请赶紧使用,不然有可能被别人抢走的哦^_^ ,明天要发放的F码是 米4联通3G版 简单来说,extern \"C\"是C++声明或定义C语言符号的方法,是为了与C兼容。说来容易,要理解起来还是得费些周折,首先我们要从C++和C的区别说起。 符号 大家都知道,从代码到可执行程序需要经过编译和链接两个过程,其中编译阶段会做语法检测,代码展开,另外它还会做一件事,就是将变量转成符号,链接的时候其实是通过符号来定位的。编译器在编译C和C++代码时,将变量转成符号的过程是不同的。本文所使用的编译器为gcc4.4.7 我们先来看一段简单的代码 /* hello.c */ #include <stdio.h> const char * g_prefix = \"hello \" ; void hello ( const char * name ) { printf ( \"%s%s\" , g_prefix , name ); } 注意,这里的文件名为hello.c,我们执行编译 gcc -c hello.c 得到目标文件hello.o,在Linux下用nm查看目标文件的符号表得到如下结果( $ 符号代表shell命令提示符) $ nm hello.o 0000000000000000 D g_prefix 0000000000000000 T hello U printf 这是C代码编译后的符号列表,其中第三列为编译后的符号名,我们主要看自己定义的全局变量g_prefix和函数hello,它们的编译后的符号名和代码里的名字是一样的。我们将hello.c重命名为hello.cpp,重新编译 gcc -c hello.cpp 得到hello.o,在用nm查看,结果如下 0000000000000000 T _Z5helloPKc U __gxx_personality_v0 0000000000000000 D g_prefix U printf 这是C++代码编译后的符号列表,gcc会自动根据文件后缀名来识别C和C++代码,这时我们发现g_prefix的符号没变,但函数hello的符号变成了 _Z5helloPKc ,这就说明gcc在编译C和C++代码时处理方式是不一样的,对于C代码,变量的符号名就是变量本身(在早期编译器会为C代码变量前加下划线 _ ,现在默认都不会了,在编译时可以通过编译选项 -fno-leading-underscore 和 -fleading-underscore 来显式设置),而对于C++代码,如果是数据变量并且没有嵌套,符号名也是本身,如果变量名有嵌套(在名称空间或类里)或者是函数名,符号名就会按如下规则来处理 1、 符号以 _Z 开始 2、 如果有嵌套,后面紧跟 N ,然后是名称空间、类、函数的名字,名字前的数字是长度,以 E 结尾 3、 如果没嵌套,则直接是名字长度后面跟着名字 4、 最后是参数列表,类型和符号对应关系如下 int -> i float -> f double -> d char -> c void -> v const -> K * -> P 这样就很好理解为什么C++代码里的void hello(const char*)编译之后符号为_Z5helloPKc(PKc翻译成类型要从右到左翻译为 char const * ,这是编译器内部的表示方式,我们习惯的表示方式是 const char* ,两者是一样的), c++filt 工具可以从符号反推名字,使用方法为 c++filt _Z5helloPKc 下面列举几个函数和符号的对应例子 函数和变量 符号 int func(int, int) _Z4funcii float func(float) _Z4funcf int C::func(int) _ZN1C4funcEi int C::C2::func(int) _ZN1C2C24funcEi int C::var _Z1C3varE 这样也很容易理解为什么C++支持函数重载而C不支持了,因为C++将函数修饰为符号时把函数的参数类型加进去了,而C却没有,所以在C++下,即便函数名相同,只要参数不同,它们的符号名是不会冲突的。我们可以通过下面一个例子来验证变量名和符号的这种关系 / * filename : test . cpp */ #include <stdio.h> namespace myname { int var = 42 ; } extern int _ZN6myname3varE ; int main () { printf ( \"%d \\n \" , _ZN6myname3varE ); return 0 ; } 这里我们在名称空间namespace定义了全局变量var,根据前面的内容,它会被修饰为符号 _ZN6myname3varE ,然后我们手动声明了外部变量 _ZN6myname3varE 并将其打印出来。编译并运行,它的值正好就是var的值 $ gcc test.cpp -o test -lstdc++ $ ./test 42 extern \"C\" 有了符号的概念我们再来看extern \"C\"的用法就很容易了 extern \"C\" { int func(int); int var; } 它的意思就是告诉编译器将extern \"C\"后面的括号里的代码当做C代码来处理,当然我们也可以以单条语句来声明 extern \"C\" int func(int); extern \"C\" int var; 这样就声明了C类型的func和var。很多时候我们写一个头文件声明了一些C语言的函数,而这些函数可能被C和C++代码调用,当我们提供给C++代码调用时,需要在头文件里加extern \"C\",否则C++编译的时候会找不到符号,而给C代码调用时又不能加extern \"C\",因为C是不支持这样的语法的,常见的处理方式是这样的,我们以C的库函数memset为例 #ifdef __cplusplus extern \"C\" { #endif void * memset ( void * , int , size_t ); #ifdef __cplusplus } #endif 其中 __cplusplus 是C++编译器定义的一个宏,如果这份代码和C++一起编译,那么memset会在extern \"C\"里被声明,如果是和C代码一起编译则直接声明,由于 __cplusplus 没有被定义,所以也不会有语法错误。这样的技巧在系统头文件里经常被用到。 点击阅读原文查看我的博客,如果觉得本文有价值,请为我点个赞,或者为我增加一个读者","tags":"Language","title":"extern \"C\"用法详解"},{"url":"https://chukeer.github.io/如何打造舒适的linux开发环境.html","text":"首先要说明的是今天讲的不是安装教程,而是讲使用方式。写这篇文章的初衷是考虑到很多在校大学生对Linux接触较少,即便接触也只是装装系统,没有实际用过Linux开发环境,而互联网公司普遍都是Linux开发环境,可以说整个互联网就是建立在Linux服务器之上,因此越早使用Linux,工作之后就能越快上手。但如果你的方向是windows客户端开发,那后面的内容都没必要看了。 请允许我先吐槽一下windows集成开发环境(IDE),在我读书的时候大家用的最多的是VC6.0,现在发展到了VS2012,并不是说IDE不好,相反它会极大的提高开发效率,但那是对老程序员来讲是这样,对于一个新手来说,我们在乎的不是写代码有多快,而是了解程序从开发到运行各个步骤是如何串联起来。在IDE中,你不用写makefile,不用关心程序的编译过程以及代码之间的相互关联,你要做的只是建立工程,写好代码,点击一下run,背后的一切IDE都帮你做好了,等你毕业后进入BAT这些互联网企业,发现一切都和学校里不一样,你是那么的不适应,这时候你想,要是在学校就能接触这样的开发环境那该多好。 互联网公司的工作环境一般是windows电脑+Linux远程主机的模式,windows电脑用来上网发邮件满足办公需求,Linux服务器用来写代码满足开发需求,所谓是各司其职配合周到。对于个人用户来说,我们虽没有远程Linux主机,但可以安装Linux虚拟机,不管是在windows下还是Mac下都可以通过虚拟机软件VMware来安装Linux系统,推荐选择Ubuntu或Centos,这两个版本网上对应教程较多,更重要的是它们都自带包管理工具(apt-get和yum),可以方便的安装软件。装好虚拟机后,如果你直接在虚拟机上操作,会让人觉得很别扭,比如你在windows下上网查到的东西没法复制到虚拟机里,完全用虚拟机里Linux自带的浏览器软件又觉得操作没那么方便,这时候你又开始怀念window,还好,在windows下我们有终端模拟器。 在百度这样的大公司有成千上万台Linux服务器,这些服务器位于几十甚至几千公里外的机房,而程序员们就坐在西二旗百度大厦的办公室,你有没有想过他们是如何工作的呢。其实我们只需要通过ssh协议登陆远程主机就可以,但windows上没有直接的ssh命令可用,因此我们需要先安装终端模拟器软件xshell或SecureCRT,其中xshell有免费版,因此我推荐xshell。正如互联网公司的windows+远程Linux服务器模式,我们可以将自己的Linux虚拟机当做远程服务器,在自己的windows系统下安装终端模拟器,通过ssh协议登陆Linux主机,登陆上之后你就可以在xshell的窗口下操作Linux主机,而xshell是命令行窗口,抛弃了Linux一切不相干的图形图像界面,可以让你沉浸在纯正的Linux环境中,全身心的投入开发,至于查资料聊QQ发邮件等办公需求,就交给windows去完成。如果你是Mac用户,那更简单,找到Terminal程序,打开之后直接通过ssh连接Linux主机即可,不过Mac自带的Terminal不太好用,可以使用iTerm来替代。 最后大致说一下ssh的使用方式,首先确保你的Linux主机开启了ssh服务(一般默认是开启的),远程登陆命令为 ssh username@hostname ,其中username是你的Linux用户名,hostname是主机名,或者是ip地址,回车输入密码即可登录。windows下的终端模拟器有很多图形化的设置选项,只要你配置好可以通过点击按钮自动连接主机,但你要知道它背后隐藏的其实就是这样一条简单的命令。题图就是我在Mac上通过iTerm使用ssh命令登陆我的博客主机的登陆界面,红框标记的是登陆命令,通过这种方式,我就可以登陆远在美国的Ubuntu主机,在上面去做任何我想做的事。 前面只是介绍了开发环境和使用原理,具体的安装细节可以百度Google,当你配置好了Linux开发环境,就可以徜徉在编程的乐趣中。Linux就像一盒巧克力,总有你喜欢的口味,只是需要你慢慢去发现","tags":"Skill","title":"如何打造舒适的linux开发环境"},{"url":"https://chukeer.github.io/让你的程序更优雅的sleep.html","text":"sleep的作用无需多说,几乎每种语言都提供了类似的函数,调用起来也很简单。sleep的作用无非是让程序等待若干时间,而为了达到这样的目的,其实有很多种方式,最简单的往往也是最粗暴的,我们就以下面这段代码来举例说明( 注:本文提及的程序编译运行环境为Linux ) /* filename: test.cpp */ #include <stdio.h> #include <unistd.h> #include <pthread.h> #include <signal.h> class TestServer { public : TestServer () : run_ ( true ) {}; ~ TestServer (){}; void Start () { pthread_create ( & thread_ , NULL , ThreadProc , ( void * ) this ); } void Stop () { run_ = false ; } void Wait () { pthread_join ( thread_ , NULL ); } void Proc () { int count = 0 ; while ( run_ ) { printf ( \"sleep count:%d \\n \" , ++ count ); sleep ( 5 ); } } private : bool run_ ; pthread_t thread_ ; static void * ThreadProc ( void * arg ) { TestServer * me = static_cast < TestServer *> ( arg ); me -> Proc (); return NULL ; } }; TestServer g_server ; void StopService () { g_server . Stop (); } void StartService () { g_server . Start (); g_server . Wait (); } void SignalHandler ( int sig ) { switch ( sig ) { case SIGINT : StopService (); default : break ; } } int main ( int argc , char * argv []) { signal ( SIGINT , SignalHandler ); StartService (); return 0 ; } 这段代码描述了一个简单的服务程序,为了简化我们省略了服务的处理逻辑,也就是Proc函数的内容,这里我们只是周期性的打印某条语句,为了达到周期性的目的,我们用sleep来实现,每隔5秒钟打印一次。在main函数中我们对SIGINT信号进行了捕捉,当程序在终端启动之后,如果你输入ctr+c,这会向程序发送中断信号,一般来说程序会退出,而这里我们捕捉到了这个信号,会按我们自己的逻辑来处理,也就是调用server的Stop函数。执行编译命令 g++ test.cpp -o test -lpthread 然后在终端输入 ./test 运行程序,这时程序每隔5秒会在屏幕上打印一条语句,按下ctl+c,你会发现程序并没有立即退出,而是等待了一会儿才退出,究其原因,当按下ctl+c发出中断信号时,程序捕捉到并执行自己的逻辑,也就是调用了server的Stop函数,运行标记位run_被置为false,Proc函数检测到run_为false则退出循环,程序结束,但有可能(应该说大多数情况都是如此)此时Proc正好执行到sleep那一步,而sleep是将程序挂起,由于我们捕捉到了中断信号,因此它不会退出,而是继续挂起直到时间满足为止。这个sleep显然显得不够优雅,下面介绍两种能快速退出的方式。 自定义sleep 在我们调用系统提供的sleep时我们是无法在函数内部做其它事情的,基于此我们就萌生出一种想法,如果在sleep中能够检测到退出变量,那岂不是就能快速退出了,没错,事情就是这样子的,通过自定义sleep,我们将时间片分割成更小的片段,每隔一个片段检测一次,这样就能将程序的退出延迟时间缩小为这个更小的片段,自定义的sleep如下 void sleep(int seconds, const bool* run) { int count = seconds * 10; while (*run && count > 0) { --count; usleep(100000); } } 需要注意的是,这个sleep的第二个参数必须是指针类型的,因为我们需要检测到它的实时值,而不只是使用它传入进来的值,相应的函数调用也得稍作修改,完整的代码如下 /* filename: test2.cpp */ #include <stdio.h> #include <unistd.h> #include <pthread.h> #include <signal.h> class TestServer { public : TestServer () : run_ ( true ) {}; ~ TestServer (){}; void Start () { pthread_create ( & thread_ , NULL , ThreadProc , ( void * ) this ); } void Stop () { run_ = false ; } void Wait () { pthread_join ( thread_ , NULL ); } void Proc () { int count = 0 ; while ( run_ ) { printf ( \"sleep count:%d \\n \" , ++ count ); sleep ( 5 , & run_ ); } } private : bool run_ ; pthread_t thread_ ; void sleep ( int seconds , const bool * run ) { int count = seconds * 10 ; while ( * run && count > 0 ) { -- count ; usleep ( 100000 ); } } static void * ThreadProc ( void * arg ) { TestServer * me = static_cast < TestServer *> ( arg ); me -> Proc (); return NULL ; } }; TestServer g_server ; void StopService () { g_server . Stop (); } void StartService () { g_server . Start (); g_server . Wait (); } void SignalHandler ( int sig ) { switch ( sig ) { case SIGINT : StopService (); default : break ; } } int main ( int argc , char * argv []) { signal ( SIGINT , SignalHandler ); StartService (); return 0 ; } 编译 g++ test2.cpp -o test ,运行 ./test ,当程序启动之后按 ctl+c ,看程序是不是很快就退出了。 其实这种退出并不是立马退出,而是将sleep的等待时间分成了更小的时间片,上例是0.1秒,也就是说在按下ctr+c之后,程序其实还会延时0到0.1秒才会退出,只不过这个时间很短,看上去就像立马退出一样。 用条件变量实现sleep 大致的思想就是,在循环时等待一个条件变量,并设置超时时间,如果在这个时间之内有其它线程触发了条件变量,等待会立即退出,否则会一直等到设置的时间,这样就可以通过对条件变量的控制来实现sleep,并且可以在需要的时候立马退出。 条件变量往往会和互斥锁搭配使用,互斥锁的逻辑很简单,如果一个线程获取了互斥锁,其它线程就无法获取,也就是说如果两个线程同时执行到了 pthread_mutex_lock 语句,只有一个线程会执行完成,而另一个线程会阻塞,直到有线程调用 pthread_mutex_unlock 才会继续往下执行。所以我们往往在多线程访问同一内存区域时会用到互斥锁,以防止多个线程同时修改某一块内存区域。本例用到的函数有如下几个,互斥锁相关函数有 int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr); int pthread_mutex_lock(pthread_mutex_t *mutex); int pthread_mutex_unlock(pthread_mutex_t *mutex); int pthread_mutex_destroy(pthread_mutex_t *mutex); 以上函数功能分别是初始化、加锁、解锁、销毁。条件变量相关函数有 int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr); int pthread_cond_timedwait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex, const struct timespec *restrict abstime); int pthread_cond_signal(pthread_cond_t *cond); int pthread_cond_destroy(pthread_cond_t *cond); 以上函数功能分别是初始化、超时等待条件变量、触发条件变量、销毁。这里需要解释一下pthread_cond_timedwait和pthread_cond_signal函数 pthread_cond_timedwait 这个函数调用之后会阻塞,也就是类似sleep的作用,但是它会在两种情况下被唤醒:1、条件变量cond被触发时;2、系统时间到达abstime时,注意这里是绝对时间,不是相对时间。它比sleep的高明之处就在第一点。另外它还有一个参数是mutex,当执行这个函数时,它的效果等同于在函数入口处先对mutex加锁,在出口处再对mutex解锁,当有多线程调用这个函数时,可以按这种方式去理解 pthread_cond_signal 它只有一个参数cond,作用很简单,就是触发等待cond的线程,注意,它一次只会触发一个,如果要触发所有等待cond的县城,需要用到pthread_cond_broadcast函数,参数和用法都是一样的 有了以上背景知识,就可以更加优雅的实现sleep,主要关注Proc函数和Stop函数,完整的代码如下 /* filename: test3.cpp */ #include <stdio.h> #include <unistd.h> #include <pthread.h> #include <signal.h> #include <sys/time.h> class TestServer { public : TestServer () : run_ ( true ) { pthread_mutex_init ( & mutex_ , NULL ); pthread_cond_init ( & cond_ , NULL ); }; ~ TestServer () { pthread_mutex_destroy ( & mutex_ ); pthread_cond_destroy ( & cond_ ); }; void Start () { pthread_create ( & thread_ , NULL , ThreadProc , ( void * ) this ); } void Stop () { run_ = false ; pthread_mutex_lock ( & mutex_ ); pthread_cond_signal ( & cond_ ); pthread_mutex_unlock ( & mutex_ ); } void Wait () { pthread_join ( thread_ , NULL ); } void Proc () { pthread_mutex_lock ( & mutex_ ); struct timeval now ; int count = 0 ; while ( run_ ) { printf ( \"sleep count:%d \\n \" , ++ count ); gettimeofday ( & now , NULL ); struct timespec outtime ; outtime . tv_sec = now . tv_sec + 5 ; outtime . tv_nsec = now . tv_usec * 1000 ; pthread_cond_timedwait ( & cond_ , & mutex_ , & outtime ); } pthread_mutex_unlock ( & mutex_ ); } private : bool run_ ; pthread_t thread_ ; pthread_mutex_t mutex_ ; pthread_cond_t cond_ ; static void * ThreadProc ( void * arg ) { TestServer * me = static_cast < TestServer *> ( arg ); me -> Proc (); return NULL ; } }; TestServer g_server ; void StopService () { g_server . Stop (); } void StartService () { g_server . Start (); g_server . Wait (); } void SignalHandler ( int sig ) { switch ( sig ) { case SIGINT : StopService (); default : break ; } } int main ( int argc , char * argv []) { signal ( SIGINT , SignalHandler ); StartService (); return 0 ; } 和test2.cpp一样,编译之后运行,程序每隔5秒在屏幕打印一行输出,输入ctr+c,程序会立马退出","tags":"Language","title":"让你的程序更优雅的sleep"},{"url":"https://chukeer.github.io/独自下场.html","text":"前天还在厦门游玩的时候,看新闻知道李娜微博发布了退役信,尽管前几天就有消息放出说要退役,但当事情真的发生时,还是会给人不少的触动,那天新闻头条都是关于这件事。 今天是亚运会比赛第二天,李娜在北京正式举办了退役发布会,这不得不说是一种巧合,一个体制外的运动员退役,一场体制内的赛事开始。新闻头条开始被亚运会占据,中国金牌榜暂时落后韩国,人们关注着这场似乎所有人都在关注的赛事,它只是一种茶余饭后的谈资,并不是因为个人兴趣而去关注,而今天我要站在一个网球爱好者的角度谈一件事。 两座大满贯奖杯,世界排名第二,这是李娜离开时所取得的成绩,可以说她是在职业生涯的顶峰选择了离开,就好像你走在一条铺满了金币的路上,只要你走下去,前面会有越来越多的金币属于你,而选择离开你将放弃那些金币。为什么我要以金币做比,因为网球赛事和奖金之间密不可分的关系。 网球是一项高度职业化和商业化的运动,而且主要是以个人项目为主,尽管也有双打,但双打的含金量和关注度无法和单打同日而语,这就是为什么郑洁06年就拿到澳网双打冠军,其影响力远不如李娜11年闯入澳网单打决赛。网球对于运动员来说是一项职业,它不同于足球和篮球这种群体运动,只要你加入了俱乐部,不管打得好坏,都是可以领到固定的工资的,而对于网球运动员来说,他们的工资就是参加比赛的奖金,取得的成绩越好,奖金也就越高,运动员想要生存,就需要不停的去参加比赛赚取奖金,因为参加这些比赛的机票酒店全部都需要自费,你不努力,没人可以帮你,所以当李娜赢得了比赛,她也只是为自己而赢。网球的商业化体现在它的巡回赛上,在世界最流行的几项球类运动中,网球是唯一一个女子比赛受关注度几乎和男子比赛齐平的运动,并且也是唯一你在世界各地都能看到高水平比赛的运动,比如去年中国网球公开赛的决赛,德约科维奇就和纳达尔上演了当今网坛最高水准的对决,刚刚开赛的武汉网球公开赛,也有小威和莎拉波娃这样的顶级女子选手。这不同于想看科比詹姆斯你只能去美国,想看梅西C罗你只能去欧洲。每年在世界各地会有很多巡回赛,运动员们会有选择的去不同的地方去参加比赛,所以并不是所有赛事都能见到所有最顶尖的选手,一般巡回赛只有32位选手参加,而只有一种赛事会强制排名前100的运动员都得参加,这种赛事就是大满贯。 在李娜拿了11年法网冠军时,国内很多人都跟着欢呼雀跃,以为大满贯就是拿下所有顶级赛事的冠军,好像只有这样的成绩才值得他们如此庆祝,对于这些人只能劝他们先去了解一下网球基本知识再说。网球大满贯一年有四次,按时间分别是澳大利亚网球公开赛、法国网球公开赛,温布尔顿网球公开赛和美国网球公开赛,四大满贯比赛都有接近百年的历史,在公开赛时代以前网球只是某些贵族的游戏,后来才慢慢开始向世界范围推广。让我来告诉你一个中国人夺得一次大满贯冠军有多难,先说男子,中国目前为止没有排名进100的男子选手,也就是说他们甚至没有直接参赛的资格,所以他们如果要参加大满贯,先要打资格赛,资格赛会产生28个参赛名额,加上排名前100的选手,一共128名选手参赛,要赢得冠军,必须连赢7场比赛,目前中国男子选手还从来没有赢得过一场大满贯比赛。可能有人说一年四次大满贯比赛,机会总会有,但事实是从2003年到目前为止,费德勒(17)纳达尔(14)德约科维奇(7),这三个人获得的大满贯冠军总数加起来是38个,你可以想象一下其它选手要从他们中间突围有多难。而女子比赛相对好一些,冠军并不总是集中在那几个人中间,但即便如此,近10年来,拿过女子大满贯冠军的球员只来自10个国家,这其中就包括中国,也是亚洲唯一的一个。所以当我们为李娜夺得法网冠军而兴奋时,不仅仅是因为她赢得了一项顶级赛事,更重要的是她打破了欧美选手对大满贯的垄断。 李娜向来也是以一个破坏者的姿态出现,她早年因不满全运会的安排而选择退役,后来又不满体制对自身的约束而选择单飞,她破坏的是顽固的制度,赢得的却是人生的巅峰。我以前总会有\"恨不早生\"的感叹,乔丹在NBA驰骋时我还在上小学,罗纳尔多赢得世界杯时我刚上初中,费德勒拿第一个大满贯时我初中未毕业,所幸的是在我拿起网球拍之后,我看着李娜一次次突破,仿佛是自己在一点一点进步一样。离世界第一只有一步之遥,但我们无法再过多要求,就到这里吧,这样就很好了。","tags":"View","title":"独自下场"},{"url":"https://chukeer.github.io/Vim c++开发环境插件安装详解.html","text":"C/C++是使用比例最高的程序语言,而vim是专为程序员开发的编辑器,当这两者结合起来,会给我们带来怎样的效果呢。 对于windows上做C++开发的用户来说,vs为他们做了大量的工作,语法高亮、自动缩进、智能提示等等,当你happy的使用vs时,请记住一点,这是一个收费软件,虽然在我大中华普遍都在使用微软的盗版软件,但作为程序员我们心里应该知道这是不对的行为,然后再去——等等,我们今天讨论的不是这个话题,我们要说的是Linux下的免费软件vim! vim插件安装的教程在互联网上已经数不胜数,但是质量也参差不齐,很多都是在其它地方转载copy,而且没有注明使用环境,很多人安装时发现无法work,最后搞得一团糟,今天这篇文章的宗旨只有两个:1、使得安装简单;2、保证可以用。 阅读这篇文章的前提是你至少使用过vim,知道基本的操作。下面就开始进入主题。 安装环境 CentOS release 6.3 (Final) VIM 7.2 本文的插件安装均在以上环境中进行,顺带也会提一下其它平台的安装方式。 准备工作 由于大多数vim插件都可以在github上找到,而且通过git安装、删除、升级插件都异常简单,因此我们需要首先安装git工具,如果不知道git和github,可以先自行Google centos安装 sudo yum install git ubuntu安装 sudo apt-get install git Mac安装 由于Mac没有像yum和apt-get一样的包管理工具,需要先安装一个类似的包管理工具,这对于安装一些Linux命令是非常有帮助的 安装包管理工具 homebrew ruby -e \"$(curl -fsSL https://raw.github.com/Homebrew/homebrew/go/install)\" 通过brew安装git brew install git 我将本文需要安装的插件和配置文件都放在了github上,如果你不想大费周折的了解每个插件的安装方式,可以直接下载所有插件即可使用,项目地址 https://github.com/handy1989/vim ,可以按照如下命令备份并一次安装所有插件(注意,从git上下载的vim目录下都是隐藏文件, ls -a 可以查看) mv ~/.vimrc ~/.vimrcbak mv ~/.vim ~/.vimbak git clone https://github.com/handy1989/vim.git mv vim/.vimrc vim/.vim ~/ 第一个插件 为了不让插件安装后目录显得凌乱,我们很有必要安装一个管理插件的插件,这样功能的插件有好几个,这里只推荐一个 pathogen 项目地址 https://github.com/tpope/vim-pathogen 安装 可以通过git安装,也可以直接下载插件文件,由于这个插件只有一个文件,我们选择后者,而该插件的说明文档上也是用的这种方法。 mkdir -p ~/.vim/autoload ~/.vim/bundle && curl -LSso ~/.vim/autoload/pathogen.vim https://tpo.pe/pathogen.vim 然后,就没有然后,这个插件就安装完了。是不是觉得so easy! 不过先别急,我们还要对插件进行一些配置,用vim打开~/.vimrc,输入如下三行类容 execute pathogen#infect() syntax on filetype plugin indent on 保存退出,这个插件的安装配置就完成了,怎么验证插件有没有生效呢,别急,后面有的是机会验证。我们先来讲讲vim插件是个什么东西。 此时你的~/.vim目录结构应该是这样的 ├── autoload │ └── pathogen.vim └── bundle 可见~/.vim目录下是有两个目录,其中autoload放的是pathogen插件,所谓的插件其实就是一个脚本,当vim启动时,它会自动加载~/.vim/autoload目录下的脚本,由于你~/.vimrc里配置了 execute pathogen#infect() ,所以它会去自动的执行脚本里这个函数,至于这个函数是怎么实现的,我们不用管它,总之这个函数的功能就是去加载~/.vim/bundle目录下你安装的所有插件,pathogen的管理方法大致就是这样,而我们也看到,vim的插件其实就是一个脚本文件,丰富一点的还会带有说明文档等其它内容,后面我们碰到了再讲解。 auto-pairs 项目地址 https://github.com/jiangmiao/auto-pairs (感兴趣的可以看) 功能 自动匹配括号、引号等 安装 git clone git://github.com/jiangmiao/auto-pairs.git ~/.vim/bundle/auto-pairs 这样就将auto-pairs在github上的项目文件下载到了~/.vim/bundle/auto-pairs目录下 测试 随便打开一个文件,输入左括号'(',看右括号是不是自动出现了,然后删除左括号,看右括号是不是也被删除了。当然功能远不止这些,但常用的就这几点。 NERDTree 项目地址 https://github.com/scrooloose/nerdtree 功能 显示目录树 安装 git clone https://github.com/scrooloose/nerdtree.git ~/.vim/bundle 此时~/.vim/bundle/nerdtree下应该有如下几个目录和文件 autoload doc lib nerdtree_plugin plugin README.markdown syntax 这几个目录是vim默认的插件目录,如果我们不是通过pathogen加载插件,就需要在~/.vim目录下创建对应的这几个目录,并将NERDTree对应的文件拷到相应的目录,这样每个目录下会放置多个插件的内容,比如~/.vim/doc下就会放置所有插件的帮组文档,而我们通过pathogen来管理,每个插件的所有内容都放在同一个目录中,比如这里的NERDTree插件的内容我们下载到了~/.vim/bundle/nerdtree下,直接删除这个目录即可删除插件,如果后续插件有更新,在该目录下通过git命令也可以很容易更新到最新版本。 打开vim,在命令行模式下输入:Helptags载入插件的帮组文档,或者用:helptags help_dir载入指定目录的帮组文档,其中help_dir是你需要加载的插件帮组文档所在的目录,比如这里是~/.vim/bundle/nerdtree/doc 测试 用vim打开一个文件,在命令行模式下输入:NERDTree,然后回车,窗口左侧就出现了vim工作目录的目录树,如下图所示 将光标置于NERDTree窗口,按'?'可查看帮助,NERDTree的操作很简单,通常是将光标置于一个目录/文件上,通过一个按键来操作,下面列出几个常用按键及其对应的操作 文件相关操作 o : 在光标所在的上一个窗口打开文件,并将光标置于新打开的窗口 go : 预览文件,光标停留在NERDTree窗口中 t : 在新标签中打开文件并激活 gt : 在新标签打开文件,光标留在NERDTree窗口中 i : 水平分割打开文件 gi : 水平分割预览 s : 垂直分割打开文件 gs : 垂直分割预览 目录树相关操作 o : 展开/关闭目录 O : 递归展开目录。慎用,如果目录层级多,打开会很慢 x : 关闭父目录 C : 切换光标所在目录为根目录 u : 切换目录树的根目录为上层目录 U : 切换目录树的根目录为上层目录,并保持旧的目录树的状态 r : 刷新当前目录 R : 刷新当前根目录(这个在新加入文件后会用到) cd : 切换vim工作目录为光标所在目录(命令模式下:pwd可查看当前工作目录) 为了方便打开NERDTree,我们可以设置快捷键,打开~/.vimrc,插入如下一行 map <C-n> :NERDTree<CR> 这样,当打开vim时,只要输入ctrl+n即可打开NERDTree MiniBufExplorer 项目地址 http://www.vim.org/scripts/script.php?script_id=159 功能 显示已打开的buffer 安装 以6.3.2版本为例,根据项目地址可以找到对应版本的下载链接 mkdir -p ~/.vim/bundle/minibufexplorer/plugin && wget \"http://www.vim.org/scripts/download_script.php?src_id=3640\" -O ~/.vim/bundle/minibufexplorer/plugin/minibufexpl.vim 测试 用vim打开一个文件,此时看不到minibufexplorer窗口,因为默认是只有一个buffer时不显示窗口的,在命令行模式下通过 :vsp filename 打开另一个文件(或者用NERDTree浏览打开其它文件),看看此时窗口上方是不是出现了MiniBufExplorer的窗口,如下所示 我们先来解释一下什么叫buffer,vim为每个打开的文件都创建了一个buffer,这个buffer存储在内存中,为了下次打开文件时快速加载,比如我们通过NERDTree浏览并打开了多个文件,即便某些文件你退出了编辑,它的buffer仍旧是存在的,在命令模式下,我们输入 :ls 可以查看打开的buffer列表,每一行前面的数字对应buffer的编号,通过输入 :b N ,其中N代表buffer编号,可以打开对应的buffer。对应上面的截图,我们打开了两个文件,此时minibufexplorer窗口显示有两个buffer,即便我们关闭一个文件,这两个buffer仍然存在,将光标移到某个buffer的名称上,回车可打开对应的buffer,按 d 可删除buffer,具体的操作可以直接阅读插件文件,路径为~/.vim/bundle/minibufexplorer/plugin/minibufexpl.vim 配置 在~/.vimrc文件中加入如下命令 let g:miniBufExplMaxSize = 2 该配置含义为minibufexplorer窗口最大高度为2行,默认是没有上限的,你打开的buffer足够多,一会一直增长下去,为了方便阅读我一般将它设为2,其它配置不怎么用到,需要用的时候可以参考插件文件,并在~/.vimrc中添加配置 ctags+taglist+omnicppcomplete 接下来就到了重磅戏了,前面还只是一些窗口相关的基本操作,是为了方便浏览文件,我们阅读C++代码时希望能快速定位函数、变量,类似于VS等其它IDE提供的功能 ctags 这个并不是插件,而是可执行程序,是用来对代码建索引,方便查找的,有些Linux版本是自带ctags的,如果没有,按如下方式安装 sudo yum install ctags ubuntu安装方式 sudo apt-get install ctags mac也是自带ctags的,但是那个不好用,可以重新安装 brew install ctags 如果安装失败,看是不是因为/usr/bin/ctags文件已经存在,可以先mv走,然后再执行 创建索引:在你代码的根目录执行如下命令,会生成一个tags文件,此时在代码根目录下打开一个文件(vim默认只加载工作目录下的tags文件),将光标置于一个函数或结构体名字上,按 ctr+] 即可跳转到该名称的定义处,如果出现多个选项,可以输入编号选择对应跳转的地方,按 ctr+o 可回到光标之前的位置 ctags -R --sort=yes --c++-kinds=+p --fields=+iaS --extra=+q --language-force=C++ taglist taglist是vim的一个插件,可以将代码内的函数、变量等按规律列出来,方便查找 下载地址 http://www.vim.org/scripts/script.php?script_id=273 ,或者直接按如下方式下载并安装 cd ~/.vim/bundle && wget \"http://www.vim.org/scripts/download_script.php?src_id=19574\" -O taglist.zip && unzip taglist.zip -d taglist 这样就生成了~/.vim/bundle/taglist目录,该目录的结构为 taglist ├── doc │ └── taglist.txt └── plugin └── taglist.vim 其中plugin目录下为插件文件,doc目录下为说明文档。打开vim,在命令行模式下输入 :helptags ~/.vim/bundle/taglist/doc 可以加载说明文档,然后输入 help taglist.txt 则可以显示说明文档 配置 打开~/.vimrc,输入如下内容 let Tlist_Show_One_File=1 \" 只展示一个文件的taglist let Tlist_Exit_OnlyWindow=1 \" 当taglist是最后以个窗口时自动退出 let Tlist_Use_Right_Window=1 \" 在右边显示taglist窗口 let Tlist_Sort_Type=\"name\" \" tag按名字排序 这几行配置看名字就能知道什么意思,引号后边是说明,在vim配置文件里,双引号代表注释,类似于C语言里的/ 测试 打开一个文件,在命令行模式下输入 :TlistToggle 即可显示taglist窗口,配合NERDTree一起效果如下 OmniCppComplete 这是一个对C++进行语法补全的插件,可以对函数、命名空间、类成员等进行补全,使用起来和绝大多数IDE差不多,不一样的是IDE为你做了很多你不知道的事情,而omni补全需要依赖tags文件,需要你用ctags命令自己生成 下载地址 http://www.vim.org/scripts/script.php?script_id=1520 也可用如下命令直接下载并安装 cd ~/.vim/bundle && wget \"http://www.vim.org/scripts/download_script.php?src_id=7722\" -O omnicppcomplete.zip && unzip omnicppcomplete.zip -d omnicppcomplete 该命令会生成~/.vim/bundle/omnicppcomplete目录,目录结构为 ├── after │ └── ftplugin │ ├── cpp.vim │ └── c.vim ├── autoload │ └── omni │ ├── common │ │ ├── debug.vim │ │ └── utils.vim │ └── cpp │ ├── complete.vim │ ├── includes.vim │ ├── items.vim │ ├── maycomplete.vim │ ├── namespaces.vim │ ├── settings.vim │ ├── tokenizer.vim │ └── utils.vim └── doc └── omnicppcomplete.txt 打开vim,在命令行模式下输入 :helptags ~/.vim/bundle/omnicppcomplete/doc 即可加载说明文档,输入 :help omnicppcomplete 查看说明文档 配置 打开~/.vimrc文件,输入 filetype plugin indent on set completeopt=longest,menu let OmniCpp_NamespaceSearch = 2 \" search namespaces in the current buffer and in included files let OmniCpp_ShowPrototypeInAbbr = 1 \" 显示函数参数列表 let OmniCpp_MayCompleteScope = 1 \" 输入 :: 后自动补全 let OmniCpp_DefaultNamespaces = [\"std\", \"_GLIBCXX_STD\"] 测试 前面说过,omni插件的补全是依赖于tags文件的,因此需要我们手动建立tags文件,假设现在有两个文件hello.h和hello.cpp如下 我们在和这两个文件所在的目录输入 ctags -R --sort=yes --c++-kinds=+p --fields=+iaS --extra=+q --language-force=C++ 这样就生成了tags文件,然后我们打开main.cpp来测试,当输入hello.h和hello.cpp中的函数或结构体时,通过按 ctr+x ctr+o 就可以自动补全了,效果如下 通过按ctr+n和ctr+p可以对候选项进行上下选择。通过这种方式可以实现对函数、结构体的补全 对STL补全 上面的方式只能对自己的代码进行补全,是当我们要使用其它库比如STL甚至是第三方库时就无法补全,原因很简单,我们只对自己的代码建立了tags文件,想要对其它库进行补全,就必须对它们的源代码建立tags文件,所幸的是vim的插件编写者们早已解决了这个需求,并且他们专门针对STL头文件进行了修改,以便能更好的适应omni的补全,首先下载STL源代码,地址为 http://www.vim.org/scripts/script.php?script_id=2358 ,可通过如下命令下载并解压 mkdir -p ~/.vim/tags && cd ~/.vim/tags && wget \"http://www.vim.org/scripts/download_script.php?src_id=9178\" -O - | tar jxvf - 这样就将STL的源码下载到了~/.vim/tags/cpp_src目录下,我们在该目录下执行ctags命令 ctags -R --sort=yes --c++-kinds=+p --fields=+iaS --extra=+q --language-force=C++ 这样就生成了~/.vim/tags/cpp_src/tags这个文件,然后打开~/.vimrc进行如下设置 set tags+=~/.vim/tags/cpp_src/tags 该命令是设置tags文件的搜索路径,默认只有vim工作目录的tags文件,这样设置之后就会同时加载指定目录的tags文件,你可以在后面添加更多其它第三方库的tags文件,现在对STL的补全效果如下 omnicppcomplete的补全设置虽然麻烦,但也让我们更加清楚了插件是怎样工作的,作为程序员,至少应该对某些东西的工作原理搞清楚,而不是像使用IDE一样不管任何东西,你想自定义一下东西也无从下手。 omnicppcomplete触发补全需要用到 ctr+x ctr+o ,显然这是不友好的,熟悉Linux命令行的人一定对shell命令的补全印象深刻,只需要按一下TAB键就可以进行补全,vim插件的强大之处在于,它可以实现你几乎所有的需求,想要用TAB键进行补全,就需要用到SuperTab SuperTab 项目地址 https://github.com/ervandew/supertab 安装 使用git进行安装是最方便的 cd ~/.vim/bundle && git clone https://github.com/ervandew/supertab.git 测试 无需任何配置即可使用,这时你想要补全变量名或函数名只需按一下TAB键即可,出现候选窗口之后也可以用TAB键进行选择,这样是不是方便了很多! 由于TAB键被映射成了补全快捷键,也就无法通过TAB键直接输入制表符,这时如果想要输入制表符可以通过 ctr+v TAB 即可,即先输入ctr+v再输入TAB键,在vim下通过ctl+v可以输入很多不可见字符,比如试试 ctr+v ctr+w 最终的配置文件 装了这么多插件,在配置文件~/.vimrc里也增添了不少内容,最后你的配置文件至少应该包含以下内容 set tags+=~/.vim/tags/cpp_src/tags \" 设置tags搜索路径 syntax on filetype plugin indent on map <C-n> :NERDTree<CR> \" pathongen execute pathogen#infect() \" taglist let Tlist_Show_One_File=1 let Tlist_Exit_OnlyWindow=1 let Tlist_Use_Right_Window=1 let Tlist_Sort_Type=\"name\" \" omnicppcomplete set completeopt=longest,menu let OmniCpp_NamespaceSearch = 2 \" search namespaces in the current buffer and in included files let OmniCpp_ShowPrototypeInAbbr = 1 \" 显示函数参数列表 let OmniCpp_MayCompleteScope = 1 \" 输入 :: 后自动补全 let OmniCpp_DefaultNamespaces = [\"std\", \"_GLIBCXX_STD\"] 这个配置文件全都是和插件相关的配置,你还可以在里面添加其它配置来灵活定制你的vim,由于本文主要介绍插件相关内容,就不展开介绍了 总结 vim的C++开发环境到此就配好了,总的来说我们安装了这么几类插件 管理插件的插件 管理窗口和buffer 语法增强 代码补全 通过这些插件我们可以将vim打造成一个好用的IDE,虽然经过了很多周折,但这些功夫都不会白费,一来我们可以通过这些插件了解IDE的一些工作原理,二来通过这些插件的安装我们也更加了解了vim插件的使用方法,有了这些基础,后续如果想满足一些个性化的需求,岂不易哉!","tags":"Skill","title":"Vim c++开发环境插件安装详解"},{"url":"https://chukeer.github.io/店大莫欺人.html","text":"锤子约架zealer事件高潮已经过去了好几天,不管是自媒体还是传统媒体也都发表了各种轮调,最后我们发现其实约架的两人都没赢,真正赢的是媒体,因为这两人给他们制造了足够多的话题讨论点,以至于我这个普通用户也想借此机会发表一下自己的一些看法,而选择这个时候写文章,一来是我不喜欢凑热闹,二来是我觉得高潮之后的宁静会带给人更冷静的思考。 锤子发布的时候我没有看直播,只是后来听说可以当一场不错的相声来看,于是才一口气看完了两个多小时的发布会。其实我以前没听过罗永浩这个人,第一次在网上看到他还是韩寒方舟子吵架那一次,老罗好像亲自跑到方舟子上班的地方找他理论,并找人拍成视频发到网上,那时我第一反应是觉得竟然有这么好事的哥们,因为那时我以为他只是一个普通的路人,直到最近才联想到那个就是大名鼎鼎的老罗。 看完锤子发布后我是支持他的,互联网老是被那几个巨头垄断,每天新闻都是BAT收购哪儿哪儿公司,看着也让人觉得腻,这时出来一个搅局者就好比一部沉闷的电影在邋遢的叙事中突然插入一个精彩的段子,让人一下子抖擞了精神。老罗是一个很会传销的人,他甚至将情怀这个词绑架在手机上并大肆宣传,这让那些自认为拥有情怀的少数分子一下觉得终于找到了党组织,不跟着吆喝几句都不好意思标榜自己曾经不可一世的情怀。对于一个从强者林立的手机圈异军突起的\"小公司\",这样的宣传无疑是起到了很大的作用,一时吸引了无数互联网用户的关注,风光无两。但渐渐地听多了情怀二字,我反而觉得有点怪异,就好比古时候给贞节女立的牌坊一样,锤子现在给我的感觉是贞节还没见到,牌坊却先立了。但是不管是贞节还是情怀,被大肆宣传后反而会因过度曝光而失去本有的色彩,有些东西本来就该静静的躺在那里,让别人去发现和欣赏。 对于王自如,我想如果不是因为这次的约架事件,恐怕他的名声还只停留在数码爱好者的小众圈子里。可能大家不知道的是在锤子开完发布会后,王自如也在北京开了个zealer2.0的发布会,据说观众有1800多人,但和锤子发布会的5000大众相比还是小巫见大巫。zealer的发布会我后来也看了,其实测评工具的发布会是相对无趣的,他也不能像老罗一样在台上将所有手机挨个儿黑个遍以满足广大网民吐槽的心理,如果不是一直以来都看zealer视频,我想没有人会愿意去看他们的发布会。这次约架之后,我发现身边平时不怎么关注锤子和zealer的人都在谈论这件事,它就好像互联网圈子里的一个流行指标,你不谈它就会out,广大网民知道锤子的人不在少数,但知道zealer并看过他们视频的估计不及前者十分之一,所以在谈论这件事时大多数人只能说说zealer不该拿手机厂商的投资,zealer好像在黑锤子等等不用多思考就能品头论足的事情,而这几个点恰好也是老罗对zealer展开猛烈攻击的突破点,可见老罗布道功力的深厚。但是在这几点尤其是拿手机厂商投资这件事上,zealer确实是有口难辩,所谓拿人手短,在利益和投资厂商的双重制约下,还标榜客观独立第三方,连小白用户都不信,更何况是老罗呢。 在谈论这个事情时先要抛开所有\"粉丝\"因素,不管是老罗的情怀粉还是zealer的视频粉,在忠实支持者的眼里他们各自都是无可侵犯的,因为对他们的侵犯代表着对支持者的侵犯,就好比当年的超女,社会大众越是批评他们,那些支持者反而越是疯狂,zealer和超女在某方面是有共性的,他们都来自草根,他们都和支持者一起成长,而不像老罗在创办锤子时已是名满天下,即便老罗在发布会一再强调\"作为一个小公司\",但是在zealer面前他是十足的\"大公司\",倒不是因为它真的大,而是zealer实在太小了。社会往往同情弱者,所以在王自如被老罗批斗的体无完肤之后,支持zealer的路人多数是因为对王自如的同情和对老罗的憎恨,在台上一个咄咄逼人不懂礼节甚至对人进行人身攻击,一个低调谦卑处处退让面对人身攻击也只能忍气吞声,在这一点上公众自有判断。而支持老罗的无非就是说zealer拿人钱财肯定会替人消灾,还标榜公平客观不厚道。在辩论中我们看到王自如有口难辩,在拿手机厂商投资上理亏词穷,而老罗则是在猛烈的批斗下也不惜通过承认苹果三星是这个行业最厉害的厂商而狠狠扇了自己\"东半球最好用的手机\"称号几巴掌。最后老罗带着胜利之姿耀武扬威的继续修造它情怀的牌坊,嘴角带着一丝微笑,而王自如则更加坚定信念去迎接艰难而未知的旅程,脸上又多了几颗痘痘。 不管怎样,zealer的视频我会继续看,因为他是免费并且带有娱乐和科普性的,老罗的锤子我不会去买,因为他的情怀已经变质。抛开辩论时老罗撒下的烟雾弹,只针对于zealer测评锤子这件事上,我挺zealer。最后,我默默的在小米官网拍下了米4的订单。","tags":"View","title":"店大莫欺人"},{"url":"https://chukeer.github.io/C++ extern声明变量详解.html","text":"extern声明变量无外乎如下两种: 1、声明全局变量 2、声明函数 今天我们只谈extern,什么const、static之类等等与之相关或不相关的一律忽略,下面就分别对以上两种情况一一讲解 声明和定义 既然提到extern声明变量,那我们就必须搞清楚声明和定义的区别。 这里我们将普通数据变量和函数统称变量。 从内存分配角度来说,声明和定义的区别在于声明一个变量不会分配内存,而定义一个变量会分配内存。 一个变量可以被声明多次,但是只能被定义一次。 基于以上前提,我们可以把声明和定义类比为指针和内存的关系。我们知道,指针其实就是指向内存的一个符号,变量的定义就好比一块内存区域,而声明就好比它的指针,可以有多个指针指向同一个内存区域,而一个指针只能指向一个内存区域,这样就很好理解为什么变量只能被定义一次,如果被定义多次,那就会分配多个内存,这样你通过变量的声明到底去找哪块内存区域呢,这会是个问题。 对于数据来说,声明和定义往往是同时存在的,比如下面的一行语句 int data; 这样既声明了 data 同时也定义了 data ,怎样做到只声明而不定义呢,用extern就可以了 extern int data; 对于函数来说,声明和定义就很容易区分了,一般我们会将声明放在头文件而将定义放在源文件里 void hello(); 这是一个函数的声明,而 void hello() { printf(\"hello world!\\n\"); } 这是一个函数的定义。当然,函数的声明和定义也可以同时发生,如果我们没有头文件而只有源文件,并且在源文件里并没有 void hello(); 这样的语句,那么这个函数的声明和定义就同时发生了,此时如果我们在原文件里想要调用函数 hello() ,你调用的代码必须在函数定义之后。 其实上面的要点只在于一句话: 使用变量之前必须声明,声明可以有多次,而定义只能有一次 。记住这句话,后面的就都很容易理解了。 extern声明全局变量 我们先来看如下例子,现有三个文件:test.h, test.cpp, main.cpp,其中main.cpp和test.cpp需要共享一个变量g_name,三个文件的内容如下 /* test.h */ #ifndef _TEST_H_ #define _TEST_H_ #include <string> std :: string g_name ; void hello (); #endif /* test.cpp */ #include <stdio.h> #include \"test.h\" void hello () { printf ( \"hello %s! \\n \" , g_name . c_str ()); } /* main.cpp */ #include \"test.h\" std :: string g_name ; int main () { g_name = \"Handy\" ; hello (); return 0 ; } 三者关系为,test.cpp包含了test.h,main.cpp也包含了test.h,这里的包含其实就是include。我们执行编译命令 g++ main.cpp test.cpp 编译报错 redefinition of 'g_name' ,说的是 g_name 被重定义了 我们看一下 g_name 出现的地方,一个是在test.h里,一个是在main.cpp里,两条语句都是 std::string g_name ,前面我们已经说过,这样的方式既声明也定义了变量,那g_name是如何被重定义的呢,首先我们需要理解include的含义,我们可以将include一个头文件理解为在该行展开头文件里的所有代码,由于main.cpp包含了test.h,我们在那一行将test.h的内容展开,就会发现main.cpp里有两句 std::string g_name; 所以在main.cpp里,g_name被定义了两次。 由于我们可以将include头文件理解为展开代码,所以编译的时候其实不需要指定头文件,只需要源文件就够了。需要注意的是,重定义并不是指在同一个原文件里定义多次,而是指在整个代码空间里,比如上面的例子是就是指在test.cpp和main.cpp里,其实上面的例子里g_name是被重定义了三次,其中test.cpp里一次,main.cpp里两次。 那上面重定义的问题怎么解决呢,很简答,将test.h里的 std::string g_name; 改为 extern std::string g_name; 就可以了,由于extern语句只声明变量而不定义变量,因此test.cpp和main.cpp展开头文件后,也只是将g_name声明了两次,而真正的定义还是在main.cpp里 extern声明函数 还是上面的例子,我们怎么在main.cpp里不包含头文件就可以调用hello函数呢,既然今天的主题是extern,不用提醒也知道,使用extern就可以了,代码如下 /* test.cpp */ #include <string> #include <stdio.h> // 声明g_name extern std :: string g_name ; // 声明和定义void hello() void hello () { printf ( \"hello %s! \\n \" , g_name . c_str ()); } /* main.cpp */ #include <string> // 声明和定义g_name std :: string g_name ; // 声明void hello() extern void hello (); int main () { g_name = \"Handy\" ; hello (); return 0 ; } 注意这里用到extern声明变量和函数两种场景,我分别在语句后面做了注释。编译命令如下 g++ main.cpp test.cpp 这里我们并没有用到头文件,但是依然可以在不同文件间共享变量和函数,这一切都是extern的功劳! 总结 要了解extern主要搞清以下几个概念: 1、声明和定义的区别。全局代码空间里,变量可以有多个声明,但只能有一个定义 2、include头文件等同于展开头文件里的代码 了解了以上两点,再来分析extern的用法,是不是就会清晰很多了","tags":"Language","title":"C++ extern声明变量详解"},{"url":"https://chukeer.github.io/Mac OS X重装操作系统.html","text":"一个月关一次机,两年都不重装系统,这在Mac上是很正常的事情。但是这只适合于普通的用户,对于爱折腾的人来说,不重装几次系统就好像自己对它爱得不够深一样。我这次也是为了尝鲜,从10.9升级到10.10的测试版,可能是测试版不够稳定,或者因为我是air低配版的原因,系统运行起来比较卡顿,而且借鉴IOS7扁平化的设计图标在电脑上看来实在是太刺眼,10.10的新特性也主要体现在和IOS系统的协同工作上,对于没有IOS设备的我来说,10.10实在是不如稳定的10.9让人舒服,因此我又将系统回滚到了10.9,这次我主要说说系统回滚和重装的问题。 这里的重装我们指重新安装同一版本的系统,回滚指从高版本退回到低版本的系统。为什么不说升级?因为升级很简单,直接去App Store下载最新的系统就行。 不管是哪种安装方式,主要有三个途径 通过Time Machine恢复 在线重装 自制U盘安装。 通过Time Machine恢复 这种方式是Mac的特色。Time Machine是Mac用来备份系统和文件的工具,当然,你需要有一个足够大的硬盘,在你第一次通过Time Machine备份系统时,它会将整个系统全部备份到指定硬盘上,第一次会比较耗时,比如我第一次备份总量是80G,花了一晚上还没备份好,最后甚至将移动硬盘分区表写坏了。所以建议刚买电脑的时候就备份一次,后续都可以增量备份,我重装之后备份50G只花了两个小时左右。Time Machine翻译过来叫时间机器,既然如此称呼,当然不只是有备份系统这样简单的功能,由于它是增量备份,每次备份时它都可以根据增量的内容建立快照,在你恢复系统时可以选择任意一个备份时间点,就好比乘坐时间机器可以回到过去任何时刻 通过Time Machine恢复系统的方式很简单,开机时按住CMD+R键不放,进入系统安装界面,里面有\"通过Time Machine恢复系统\"选项,直接选择就行。 这种方式既可以重装又可以回滚,取决于你Time Machine备份的系统版本,如果你用Time Machine备份的是低版本的系统,那么就会回滚到系版本 在线安装 这也是Mac有别于windows的地方,由于10.9是免费系统,因此苹果提供了在线安装的功能,如果你不小心将系统弄坏了,而且也没有用Time Machine备份,制作U盘启动盘安装又太麻烦,那么你可以选择这种方式。 同样是开机时按住CMD+R键不放,直到进入系统安装界面,有一个选项是通过网络安装系统,这时你需要先连接WiFi,根据我的经历,貌似此时是无法连接以太网的,也就是说插网线不能联网,所以最好是用WiFi,连上WiFi之后根据提示安装即可。 这种安装方式是不能回滚到,比如我是10.10系统,通过这种方式安装的还是10.10系统,这样安装之后只会替换系统文件,用户文件还会是原来的样子。 自制U盘安装 和windows一样,Mac也可以通过自制U盘启动盘来安装系统,这种安装方式是最复杂的,不过也是最随心所欲的,你可以升级、回滚、重装,都可以,在通过这种方式安装时,请严格安装下面的步骤 制作U盘启动 首先你需要一个至少8G的U盘,不过最好是用移动硬盘,可以给移动硬盘分一个10G左右的分区,将其制作成启动盘,一般来说移动硬盘的速度是快于U盘的。打开磁盘工具(Mac自带的),选中你要制作的分区(如果是U盘,只有一个分区,如果是移动硬盘可以自己先进行分区),点击\"抹点\"标签,格式选择\"Mac OS扩展\",名称填Mavericks(这个名字后面会用到)点击抹掉,此时分区被格式化为指定格式,这种格式只有Mac能识别,windows是不识别的。 下载系统安装包,对于不同版本的制作方式也不同,由于我制作的是10.9的系统,这里也以此为例,下载地址为 http://pan.baidu.com/s/1hqDrQSG ,如果直接下载貌似速度比较慢,通过百度云管家下载会快一些,但是百度云管家只有windows版本的,所以身旁如果有windows电脑可以先用windows电脑下载好再拷贝到Mac上 同时选中两个文件,双击解压,得到一个dmg后缀的文件,双击dmg文件,此时文件被挂载到了Mac上,通过Finder左侧的设备可以看到。怎么查看挂载的目录呢,打开终端软件,输入df -h,可以查看系统所有挂载的设备,最后一列是设备挂载的路径,相信聪明的你一定可以判断哪个路径对应哪个设备(一般通过名字和Size、Used等特征判断),此时确保U盘和系统安装包都被挂载了 打开终端软件,输入如下命令,其中有几个地方是需要替换成你自己的路径的 sudo /Volumes/Install\\ OS\\ X\\ Mavericks.app/Contents/Resources/createinstallmedia --volume /Volumes/Mavericks --applicationpath /Volumes/Install\\ OS\\ X\\ Mavericks.app --nointeraction 这条命令的意思是,通过路径为/Volumes/Install\\ OS\\ X\\ Mavericks.app/Contents/Resources/createinstallmedia的程序,将文件/Volumes/Install\\ OS\\ X\\ Mavericks.app安装到设备/Volumes/Mavericks中,其中/Volumes/Install\\ OS\\ X\\ Mavericks.app是挂载的系统安装包,/Volumes/Mavericks是挂载的U盘(前面提到过,在抹掉时命名为Mavericks),将这几个替换成自己对应的路径就可以,注意,如果文件名里有空格,前面是要加反斜杠\"\\\"的 回车之后等待一会儿,U盘启动器就安装成功了 从U盘启动系统 重启电脑,开机时按住option键不放,直到进入磁盘选择界面,选择你自己的磁盘 之后会进入一个界面,有\"通过Time Machine恢复\",\"安装OS X\"等选项,此时你有两个选择 1、通过菜单栏的磁盘工具抹掉系统硬盘,磁盘工具的使用和制作U盘启动时一样,将系统盘抹掉,格式化为Mac OS扩展格式。然后选择\"安装OS X\",将系统安装到抹掉的硬盘里。此种方式会删除所有数据 2、直接点击\"安装Os X\",将U盘里的系统安装到系统盘上,这种方式是覆盖安装,只会替换系统文件,用户文件还在 以上三种方式分别对应不同的场景需求,最简单也最保险的当然是通过Time Machine备份系统,所以在此也提醒各位在升级系统前一定要备份,否则你将尝到无尽折腾的味道。另外,由于Time Machine无法选择部分文件备份,觉得备份太慢且只想备份部分文件或软件时,可以自己用移动硬盘拷贝,由于Mac下的软件都类似于Windows下的绿色软件,也就是说你将/Applications目录下的软件考走,放到另一台Mac的/Applications目录下,一样是可以运行的,所以你可以像拷贝文件一样将软件拷贝的移动硬盘,重装系统后再将软件拷贝到/Applications下即可,这种方式经本人试验大部分软件都可用。 最后为这几天的折腾总结一下: 没事别折腾系统 就算要折腾系统,也一定要用Time Machine先备份 如果没有备份,且将系统折腾死了,那你就只能看这篇文章了 The end...","tags":"Skill","title":"Mac OS X重装操作系统"},{"url":"https://chukeer.github.io/C++管理读写锁的一种实现方法.html","text":"读写锁是一种常用的线程同步机制,在多线程环境下访问同一内存区域时往往会用到,本篇不是介绍读写锁的原理,而是在假设读者都知道读写锁的基本使用方式的前提下,介绍一种管理读写锁的方法 读写锁的基本使用 === 为了读起来好理解,还是先介绍一下基本概念和使用 基本概念 读写锁有三种状态:读模式加锁,写模式加锁,不加锁 读写锁的使用规则 在当前没有写锁的情况下,读者可以立马获取读锁 在当前没有读锁和写锁的情况下,写者可以立马获取写锁 也就是说,可以多个读者同时获取读锁,而写者只能有一个,且在写的时候不能读 基本使用 初始化和销毁 int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr); int pthread_rwlock_destroy(pthread_rwlock_t *rwlock); 读和写 int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock); int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock); 另外还有非阻塞模式的读写 int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock); int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock); 解锁 int pthread_rwlock_unlock(pthread_rwlock_t *rwlock); 示例 test.cpp #include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <pthread.h> #define ARRAY_SIZE 10 int g_array [ ARRAY_SIZE ]; pthread_rwlock_t g_mutex ; void * thread_func ( void * arg ) { while ( true ) { int index = random () % ARRAY_SIZE ; if ( 0 == random () % 2 ) { // read pthread_rwlock_rdlock ( & g_mutex ); printf ( \"read array[%d]:%d \\n \" , index , g_array [ index ]); } else { // write pthread_rwlock_wrlock ( & g_mutex ); int value = random () % 100 ; g_array [ index ] = value ; printf ( \"write array[%d]:%d \\n \" , index , value ); } pthread_rwlock_unlock ( & g_mutex ); sleep ( 2 ); } } int main ( int argc , char * argv []) { pthread_rwlock_init ( & g_mutex , NULL ); srand (( unsigned ) time ( NULL )); int pthread_num = 10 ; pthread_t threads [ pthread_num ]; for ( int i = 0 ; i < pthread_num ; ++ i ) { pthread_create ( & threads [ i ], NULL , thread_func , NULL ); } for ( int i = 0 ; i < pthread_num ; ++ i ) { pthread_join ( threads [ i ], NULL ); } pthread_rwlock_destroy ( & g_mutex ); return 0 ; } 这段代码可以直接编译运行 g++ test.cpp -o test ./test 读写锁的管理 === 通过上面的代码我们可以了解读写锁的基本使用方法,在需要读的时候调用读锁命令,需要写的时候调用写锁命令,读写完后调用解锁命令,这样使用虽然简单易懂,但是有时候会让代码很繁琐,比如当你调用了读锁命令后,程序可能会有多个出口,如果不使用goto语句的话(goto语句在某些编程规范里是明令禁止的,苹果曾经因为goto语句导致SSL连接验证的bug,有一篇文章分析得很好,可以参考一下 由苹果的低级Bug想到的编程思考 ),那你需要在每个出口都调用一次解锁操作,这样就失去了程序的优雅性,我们用下面的伪代码片段来描述这种情况 int func() { pthread_rwlock_rdlock(&g_mutex); if (condition1) { // do something pthread_rwlock_unlock(&g_mutex); return -1 } else if(condition2) { // do something pthread_rwlock_unlock(&g_mutex); return -2; } else { // do something } pthread_rwlock_unlock(&g_mutex); return 0; } 这个程序有多个出口,在每个出口我们都要手动调用一次解锁,很显然这不是我们期望的样子,那理想的情况应该是怎样的呢,它应该是只需显式的调用一次加锁操作,在每个出口会自动调用解锁,于是我们很容易想到用类来管理,在程序入口定义一个类对象,在构造函数里调用加锁操作,当程序return后,类对象会自动析构,我们在析构函数里实现解锁,这样就不用每次手动去调用解锁操作了。管理读写锁的类的实现如下,我们把代码放在头文件rwlock.h下 #ifndef _RWLOCK_H_ #define _RWLOCK_H_ #include <iostream> #include <pthread.h> enum ELockType { READ_LOCKER , WRITE_LOCKER , }; class RWLockManager ; class RWLock { public : typedef RWLockManager Lock ; RWLock () { pthread_rwlockattr_t attr ; pthread_rwlockattr_init ( & attr ); pthread_rwlock_init ( & m_rwlock , & attr ); } virtual ~ RWLock () { pthread_rwlock_destroy ( & m_rwlock ); } int rdlock () { return pthread_rwlock_rdlock ( & m_rwlock ); } int wrlock () { return pthread_rwlock_wrlock ( & m_rwlock ); } int unlock () { return pthread_rwlock_unlock ( & m_rwlock ); } private : pthread_rwlock_t m_rwlock ; }; class RWLockManager { public : RWLockManager ( RWLock & locker , const ELockType lock_type ) : m_locker ( locker ) { if ( lock_type == READ_LOCKER ) { int ret = m_locker . rdlock (); if ( ret != 0 ) { std :: cout << \"lock failed, ret: \" << ret ; } } else { int ret = m_locker . wrlock (); if ( ret != 0 ) { std :: cout << \"lock failed, ret: \" << ret ; } } } ~ RWLockManager () { int ret = m_locker . unlock (); if ( ret != 0 ) { std :: cout << \"unlock failed, ret: \" << ret ; } } private : RWLock & m_locker ; }; #endif 注意类RWLockManager的成员变量m_lock必须是一个RWLock类型的引用 这样在使用起来的时候就很简单明了,还是上面那份伪代码,使用读写锁管理类之后如下 int func () { RWLock :: Lock lock ( g_mutex , READ_LOCKER ); if ( condition1 ) { // do something return -1 } else if ( condition2 ) { // do something return -2 ; } else { // do something } return 0 ; } 总结 === 以上就是管理读写锁的一种方式,只要稍微花点心思就可以让代码变得简洁清晰,程序的根本目的是消除重复劳动,如果我们在写代码的时候要重复写了很多遍某些语句,那么我们就应该考虑是不是设计一个方法消除这种重复。","tags":"Language","title":"C++管理读写锁的一种实现方法"},{"url":"https://chukeer.github.io/glog安装及使用.html","text":"glog是Google的开源日志系统,使用简单,配置灵活,适合大小型程序的开发。本文介绍Linux平台的安装和使用 安装 推荐从源码安装,下载地址 https://code.google.com/p/google-glog/downloads/list 下载解压后进入目录,和所有Linux程序的安装步骤一样 ./configuer make make install 如果没有权限请在命令前加上sudo,如果想安装在指定目录,使用./configuer --prefix=your_dir 使用 glog的使用简单到令人发指,现有测试程序test.cpp如下 #include <glog/logging.h> int main ( int argc , char * argv []) { google :: InitGoogleLogging ( argv [ 0 ]); LOG ( INFO ) << \"info: hello world!\" ; LOG ( WARNING ) << \"warning: hello world!\" ; LOG ( ERROR ) << \"error: hello world!\" ; VLOG ( 0 ) << \"vlog0: hello world!\" ; VLOG ( 1 ) << \"vlog1: hello world!\" ; VLOG ( 2 ) << \"vlog2: hello world!\" ; VLOG ( 3 ) << \"vlog3: hello world!\" ; DLOG ( INFO ) << \"DLOG: hello world!\" ; return 0 ; } 假设你的glog库的路径为/usr/local/lib/libglog.a,头文件路径为/usr/local/include/glog/logging.h,那么编译命令如下 g++ test.cpp -o test -L/usr/local/lib -lglog -I/usr/local/include/glog 看看是不是将日志打印到屏幕上了,但是你会发现有些日志没有打印出来。接下来我来一一解释 日志级别 使用日志必须了解日志级别的概念,说白了就是将日志信息按照严重程度进行分类,glog提供四种日志级别:INFO, WARNING, ERROR, FATAL。它们对应的日志级别整数分别为0、1、2、3, 每个级别的日志对应一个日志文件,其中高级别的日志也会出现在低级别的日志文件中,也就是说FATAL日志会出现在INFO、WARNING、ERROR对应的日志文件中。 FATAL日志会终止程序 ,没事别乱用 日志文件 glog的日志文件默认是保存在/tmp目录下的,当然你可以指定日志路路径和日志名称 指定日志文件名字 google::InitGoogleLogging(argv[0]) 你也可以指定其它字符串,比如本例指定的名字为test,那么日志文件就是test.INFO, test.WARNING这样的格式 指定参数 glog可以采用命令行的模式配置参数,这也是它灵活易用的体现,有两种指定参数的方法,一种依赖于gflag如下: ./your_application --logtostderr=1 或者通过环境变量指定: GLOG_logtostderr=1 ./your_application 所有的环境变量均以GLOG_开头,我们推荐使用第二种,一来不必依赖于gflag,二来当参数很多时,可以写成脚本的形式,看起来更直观,GLOG支持的flag如下(只列出常用的,如果想看全部的,可以在源码的logging.cc文件下看到): 环境变量 说明 GLOG_logtostderr bool,默认为FALSE,将日志打印到标准错误,而不是日志文件 GLOG_alsologtostderr bool,默认为FALSE,将日志打印到日志文件,同时也打印到标准错误 GLOG_stderrthreshold int,默认为2(ERROR),大于等于这个级别的日志才打印到标准错误,当指定这个参数时,GLOG_alsologtostderr参数将会失效 GLOG_minloglevel int,默认为0(INFO), 小于这个日志级别的将不会打印 GLOG_log_dir string类型,指定日志输出目录,目录必须存在 GLOG_max_log_size int,指定日志文件最大size,超过会被切割,单位为MB GLOG_stop_logging_if_full_disk bool,默认为FALSE,当磁盘满了之后不再打印日志 GLOG_v int,默认为0,指定GLOG_v=n时,对vlog(m),当m<=n时才会打印日志 知道了这些参数之后,我们可以在脚本中指定这些变量,还是以test程序为例,test.sh如下: 1 2 3 4 5 6 7 #!/bin/sh export GLOG_log_dir = log export GLOG_minloglevel = 1 export GLOG_stderrthreshold = 1 export GLOG_v = 3 export GLOG_max_log_size = 1 ./test 执行脚本sh test.sh即可。这样看上去就非常清晰,修改起来也方便 打印日志 普通模式 // 打印日志为 INFO 级别 LOG ( INFO ) << \"info: hello world!\" ; // 满足 num_cookies > 10 时,打印日志 LOG_IF ( INFO , num_cookies > 10 ) << \"Got lots of cookies\" ; // 当日志语句被执行的第 1 次、 11 次、 21 次 ... 时打印日志,其中 google : :COUNTER 代表的是被执行的次数 LOG_EVERY_N ( INFO , 10 ) << \"Got the \" << google : :COUNTER << \"th cookie\" ; // 以上两者的组合 LOG_IF_EVERY_N ( INFO , ( size > 1024 ), 10 ) << \"Got the \" << google : :COUNTER << \"th big cookie\" ; // 前 20 次执行的时候打印日志 LOG_FIRST_N ( INFO , 20 ) << \"Got the \" << google : :COUNTER << \"th cookie\" ; debug模式 debug模式的语句如下 DLOG ( INFO ) << \"Found cookies\" ; DLOG_IF ( INFO , num_cookies > 10 ) << \"Got lots of cookies\" ; DLOG_EVERY_N ( INFO , 10 ) << \"Got the \" << google : :COUNTER << \"th cookie\" ; 当编译的时候指定-D NDEBUG时,debug日志不会被输出,比如test.cpp的编译命令改为 g++ test.cpp -o test -L/usr/local/lib -lglog -I/usr/local/include/glog -D NDEBUG 那么就不会有debug日志输出 check模式 CHECK( fd != NULL ) << \" fd is NULL, can not be used ! \"; 当check的条件不成立时,程序打印完日志之后直接退出,其它命令包括CHECK_EQ, CHECK_NE, CHECK_LE, CHECK_LT, CHECK_GE, CHECK_GT。以CHECK_EQ为例,适用方式如下 CHECK_NE(1, 2) << \": The world must be ending!\"; 自定义日志级别 VLOG ( 1 ) << \"vlog1: hello world!\" ; VLOG ( 2 ) << \"vlog2: hello world!\" ; 这个是独立于默认日志级别的,可以配合GLOG_v参数适用 总结 GLOG使用就是如此方便,你只需要在代码里指定日志文件名,然后就可以放心的在代码里添加日志而不需要管那些初始化和销毁的操作,其它都可以以命令行的方式来配置,简单灵活,而且基本功能也比较齐全。另外,如果想了解GLOG详细适用,可以参考官方文档 http://google-glog.googlecode.com/svn/trunk/doc/glog.html","tags":"Skill","title":"glog安装及使用"},{"url":"https://chukeer.github.io/c++实战——多人会话聊天室2.html","text":"前面已经讲过一次多人会话聊天室的实现 C++实战——多人会话聊天室(一) ,只不过上一篇是用最简单的方式,服务端每接收一个连接就起一个线程,而且是阻塞模式的,也就是说服务端每次调用accept函数时会一直等待有客户端连接上才会返回。今天介绍一种基于epoll模型的非阻塞方式的实现。 === 阻塞与非阻塞 顾名思义,阻塞就是当你调用一个函数后它会一直等在那里,知道某个信号叫醒它,最典型的例子就是read之类的函数,当你调用时它会等待标准输入,直到你在屏幕上输完敲下回车,它才会继续执行。Linux默认IO都是阻塞模型的 非阻塞就是当你调用函数之后它会立马返回,同样还是拿read举例,它不会阻塞在屏幕上等待你输入,而是立马返回,如果返回错误,那就代表没有数据可读。下面的例子可以大致说明一下差别 #include <unistd.h> #include <stdio.h> #include <fcntl.h> #include <stdlib.h> int main ( int argc , char * argv []) { int res ; res = fcntl ( 0 , F_GETFL ); if ( - 1 == res ) { perror ( \"fcntl error!\" ); exit ( 1 ); } #ifdef NONBLOCK res |= O_NONBLOCK ; if ( fcntl ( 0 , F_SETFL , res ) == - 1 ) { perror ( \"error\" ); exit ( 1 ); } #endif char buf [ 100 ]; int n = 0 ; n = read ( 0 , buf , 100 ); if ( - 1 == n ) { perror ( \"read error\" ); exit ( 1 ); } else { printf ( \"read %d characters \\n \" , n ); } return 0 ; } 代码的意思很好理解,我们从标准输入读取数据,并打印出读取了多少字节,但是我们做了个测试,当定义了宏NONBLOCK后,我们会将标准输入句柄改变成非阻塞的,宏可以通过编译时的-D参数指定,我们分别按如下指令编译,假设文件名为test.cpp g++ test.cpp -o test_block g++ test.cpp -D NONBLOCK -o test_nonblock 然后我们运行./test_block,程序会阻塞在屏幕上等待输入,输入hello world并回车,程序运行结束 但是当我们运行./test_nonblock时,程序报错 read error: Resource temporarily unavailable ,这是因为此时的标准输入是非阻塞模式,当调用read后它会立马返回,而此时并没有数据可读取,就会返回错误,但是我们按如下方式就可运行成功 echo \"hello world\" | ./test_nonblock 因为在read调用之前,管道里已经有了数据,所以它会去读取管道里的数据而不会出错。 to be continued...","tags":"Language","title":"c++实战——多人会话聊天室2"},{"url":"https://chukeer.github.io/vpn简介以及国内外分流设置.html","text":"为了维护共产主义的纯洁性,为了阻挡资本主义万恶势力的入侵,我大中华局域网平地拔起,多少互联网用户搜索是用百度,看新闻是用新浪、QQ,社交是用人人微博之流,但总有一份不安分子想要使用Google、Facebook等一些不纯净的网站,以窥探资本主义的罪恶,为了满足这些用户的好奇心,VPN服务营运而生 vpn简介 === 在没有使用VPN的时候,我们访问网站的数据线路大致是这样的,我们以百度举例 你在浏览器输入www.baidu.com,DNS服务器给你解析出对应的ip 你的路由器将你的请求转给下一个路由器,下一个路由器再转给下下个路由器,一直到百度的服务器 百度服务器接收到请求,将百度的网页打包传给你,并带上你的ip 再经过一层层路由器的转发,百度返回的数据包回到你电脑上,由浏览器展示成网页的形式 如果你想知道你访问百度经过了哪些路由跳转,可以通过如下命令查看到 Linux: traceroute www.baidu.com windows(Dos界面下): tracert www.baidu.com 同样,你访问国外的网站比如Google的时候也是通过以上的线路,只不过在你的请求数据到达Google服务器之前,要经过国内的统一出口,这个出口检测到你要访问Google,它可能会给你拦截下来,至于为什么,开篇已经说了,大家都懂的。这种拦截可能是根据ip拦截(比如facebook),也可能是根据你访问的数据内容拦截(比如Google),要知道HTTP请求是没有加密的,如果别人截获了你的HTTP请求,是可以知道你访问的具体内容的 但是并不是所有访问国外的请求都会被拦截,比如访问 who.is 就没事,于是VPN就有了用武之地。VPN主机可以理解为国外没有被拦截的服务器,当你连上VPN再访问国外网站比如google时,它的数据线路大致是这样的 你的请求通过国内出口到达VPN主机,这时你的数据是经过加密的,所以无法通过内容来过滤你的请求 VPN主机从数据里解密,知道你要访问Google,VPN主机再向Google发起请求,这时由于VPN主机和Google服务器都在国外,不会被拦截 Google服务器将数据返回给VPN主机,VPN主机将数据加密,再返回给你的电脑 所以我们有了VPN,就可以去一窥万恶的资本主义网络世界到底是个什么样子了。要连VPN,得知道VPN主机地址,有免费的也有收费的,为了用的心安理得我一直都是用收费的,我是在 vpnso.com 这个网站买的服务,支持Mac、windows、Android、IOS等设备,经济又实惠 VPN分流 === 但是有了VPN问题又来了,上面说到我们的所有请求都会通过VPN主机,也就是说我们如果连上VPN之后再访问百度,也要绕那么一大圈,这样既耗流量又浪费时间。但是聪明的程序员也想到了解决的办法,那就是设置路由表来分流 执行 大致的原理就是通过一些命令设置电脑的路由表,每次访问时系统会先去路由表查一下,如果在路由表里则不通过VPN访问,不在才走VPN,这样就可以实现访问国内国外网站的分流了。github上有一个项目专干这事儿,地址在 https://github.com/jimmyxu/chnroutes 。下面我简要列出主要操作 首先下载chnroute.py Mac下执行 python chnroutes.py -p mac ,生成ip-up和ip-down两个文件,将文件cp到/etc/ppp目录下,如果目录不存在则新建一个 windows下执行 python chnroutes.py -p win ,生成vpnup.bat和vpndown.bat,由于很多windows电脑没有装Python,可以直接去下载vpnup.bat和vpndown.bat两个文件,然后执行vpnup.bat。vpndown.bat其实没什么用,它是用来清除路由表的,但是电脑关机后自动清除 完成上面操作后再连上VPN,就可以实现分流了 测试 通过访问网站 分别访问 www.123cha.com 和 who.is ,如果显示你的ip不同,那么就成功了。前者显示的是你国内的ip,后者显示的是你VPN主机的ip 通过命令行 通过前面介绍的traceroute和tracert命令,我们以windows下的tracert命令举例 在DOS下执行 tracert www.baidu.com ,第一跳ip地址应该是192.168.xx.xx 执行 tracert www.google.com ,第一跳地址应该是10.10.xx.xx 可能遇到的问题 这里列举一个我遇到的问题 在公司内部大家都在一个局域网里,有时候为了方便共享会在自己机器上搭建Apache服务,然后把地址给别人访问,比如我的局域网ip是192.168.32.91,别人的是192.168.7.35,别好奇为什么最后两个域值不一样,那是因为我们不是连在同一个路由器上,这时候如果我连上了VPN,就无法访问到对方的主机,甚至ping都会失败。为什么呢,很简单,因为请求都是走VPN的,而VPN主机是无法访问公司内部局域网的ip的,所以就会失败 解决办法 在前面提到的路由表里添加一行记录,我们以windows平台为例,打开vpnup.bat文件,建议不要用记事本,可以装一个editplus,编辑文本文件很方便。在最后按照他的格式添加一行记录,路由地址配192.168.0.0,子网掩码配255.255.0.0。断开VPN,重新执行vpnup.bat(注意这时候可能会显示路由表已添加,因为你前面已经执行过一次vpnup.bat,不要管它,一直让它执行到最后一条),再连上VPN,看看是不是可以访问了","tags":"Skill","title":"vpn简介以及国内外分流设置"},{"url":"https://chukeer.github.io/c++实战——多人会话聊天室1.html","text":"无他,但手熟尔 ——《卖油翁》 任何一门编程语言,要想熟练,唯有多练。即便是读了一千本小说,若不自己写文章,也成不了作家,编程技术更是需要日复一日反复练习,我将自己学习C++过程中的练习经历与大家分享,如果能给其他初学者以帮助,那是最让人感到欣慰的,当然,本人也是初学者,不足之处难免,望高手多多指教 功能概述 === 多人会话聊天室的练习起源于学习多线程和socket编程,服务端和客户端的大致功能如下: 服务端进程代表一个聊天室,响应多个客户端的请求,客户端以用户名登陆之后,可以发表消息,服务端将消息推送给所有登陆的用户,类似于QQ的讨论组一样的功能 客户端向服务端发起连接,通过特定指令登陆,登陆之后可发表消息。客户端支持的指令包扩 login name, 以name为用户名登陆 look, 查看当前登陆的所有用户 logout, 退出当前用户 quit, 退出客户端 本文所有代码在Ubuntu和Mac OS X上编译运行通过,如果是windows用户不保证能编得过。 项目地址 https://github.com/handy1989/chatserver/tree/version1.0.1 实践分析 === 由于在实践过程中我只保留了最终可运行的版本,所以我这里只给出最终版本的源代码,一些中间状态的代码只会分析一下逻辑。毕竟任何程序都不是一开始就完成所有功能,我也是先从简单功能开始实现,然后一点一点添加。 小试牛刀 先看一下最简单的服务器客户端程序逻辑,这种源代码Google一下到处都是 服务端 1. 调用socket函数建立一个连接,返回一个文件描述符sockfd 2. 调用bind函数将sockfd和服务器地址绑定 3. 调用listen函数,使sockfd可以接受其它连接 4. 调用accept函数,接受客户端的连接,返回这个连接的文件描述符connfd 5. 调用send函数向connfd发送消息 6. 调用recv函数从connfd接受消息 客户端 客户端的逻辑就简单多了 1. 调用socket得到sockfd 2. 调用connect和sockfd建立连接 3. 调用send向sockfd发送消息 4. 调用recv从sockfd接受消息 以上函数具体用法在Linux下都可以通过man手册查到(比如man socket),虽然英文的阅读效率低一点,但绝对是最权威的 这里需要说明一下文件描述符的概念,在Linux下,所有设备、网络套接字、目录、文件,都以file的概念来对待,打开一个对象就会返回一个文件描述符,通过文件描述符就可以实际的去操作对象,比如read, write, close等。其中最典型的文件描述符就是0、1、2,分别代表标准输入、标准输出、标准错误 。 从上面可以看出服务器端和客户端的区别,服务端先创建一个文件描述符sockfd,这个是负责接受客户端的连接请求的,当客户端请求成功后服务端会得到这个连接的一个专属文件描述符connfd,如果有多个客户端,那么这多个客户端的connfd都是不同的,服务端和客户端的消息读写都是通过connfd进行,而客户端和服务端的消息读写都是通过sockfd进行。 客户端实现 客户端逻辑很简单,先建立连接,然后收发消息。但要注意一点,客户端的收发消息并不是同步的,也就是说并不是发一条就收一条,由于是多人会话,即便你不发消息,也有可能收到别人的消息,所以这里需要将收消息和发消息分离,这里我们用多线程来实现,一个线程专门负责接收消息,并将消息打印到屏幕上,一个线程专门读取标准输入,将消息发送出去。 客户端代码见 https://github.com/handy1989/chatserver/blob/version1.0.1/client.cpp 服务端实现 根据文章开始给出的服务端功能,我们画出服务端的处理流程图 我们在服务端对每个连接建立一个线程,由线程来单独管理和客户端的通信,线程里的处理逻辑就和最简单的服务器客户端模型一样,先接收客户端消息,再给客户端返回信息,不过由于是多人会话,每个客户端发表一条消息,服务端需要给其它所有用户推送消息,这就需要服务端记录登陆进来的所有用户,为了简化,我没有设置密码,并且每次服务端重启后,所有用户信息清零 服务端的数据结构见文件 https://github.com/handy1989/chatserver/blob/version1.0.1/chatserver.h ,下面分别说明几个关键变量 std : :map < int , std : :string > m_users ; std : :set < std : :string > s_users ; m_users用来存储和客户端连接的文件描述符与用户名的对应关系,s_users存储的是所有登陆的用户,也就是m_users的value的集合,为很么还要设置一个s_users呢,因为每次用户登陆的时候需要查找用户是否已注册,而m_users是以文件描述符为key的,查value是否存在不太好操作,于是就将value单独存储起来,便于查找 int connfd_arr[MAX_THREAD_NUM]; connfd_arr存储的时当前连接的文件描述符,设置了一个最大连接数,当有用户连接时,如果连接数超过了最大值,服务端将不会建立线程去通信,否则,服务端会从数组里找一个未被占用的分配给该连接,当线程退出时,数组对应的值会置为-1 typedef void ( ChatServer ::* p_func )( char * arg , bool & is_logged , int connfd , ChatServer * p_session , std : :string & user_name ); std : :map < std : :string , p_func > m_func ; 由于服务端需要根据用户输入的消息来调用相应处理函数,比如login name对应的登录函数,look对应查看用户的函数,所以服务端需要根据字符串去调用一个函数,最简单的实现就是写若干个if语句一一比较,但我们用了一种更优雅的方式,首先我们将所有处理函数定义成一样的类型,也就是参数和返回值都一样,然后定义一个map型变量,key为命令的关键字,如\"login\"\"logout\"等等,value就是对应的处理函数的地址,这样我们接收到客户端的消息后,解析出是哪种命令,然后直接查找map得到函数地址,就可以调用对应函数了 服务端和客户端完整代码见 https://github.com/handy1989/chatserver/tree/version1.0.1 ,客户端运行之后的效果如下 这里只是演示了一个用户登录的情况,感兴趣的可以多个客户端同时连接看看效果 小结 === 本文实现了多人会话的基本功能,服务端通过线程与客户端建立连接,并且自己管理线程,为了简单,线程同步等因素都没有考虑进去。这样做只是为了能尽快对网络通信有个感性的认识,咱又不是想把它做成产品,能运行起来就是最终目的。但是明显的缺陷也摆在这里,比如多线程的管理,accept的阻塞等等,下次将会分享一个基于epoll模型的多人会话聊天室,有了epoll的管理,服务端的代码将会变得清晰而又简洁","tags":"Language","title":"c++实战——多人会话聊天室1"},{"url":"https://chukeer.github.io/C++ const知多少.html","text":"const修饰变量 关于const最常见的一个面试题是这样的:char *const和const char*有什么区别,大家都知道const修饰符代表的是常量,即const修饰的变量一旦被初始化是不能被更改的,这两个类型一个代表的是指针不可变,一个代表指针指向内容不可变,但具体哪个对应哪个,很多人一直搞不清楚。 有这样一个规律,const修饰的是它前面所有的数据类型,如果const在最前面,那么把它和它后面第一个数据类行交换.比如上面的const char*交换之后就是char const *,这样一来就很清楚了,char *const p中的const修饰的是char *(注意,我们这里把char和*都算作一种类型,这时候const修饰的是char和*的组合,也就是字符串指针),是一个指针类型,所以这时候指针p是不能变的,比如下面这段代码就会报错 char str1[]=\"str1\"; char str2[]=\"str2\"; char *const p = str1; p = str2; 这时候p是一个指针常量,它是不能指向别的地方的,但是它本身指向的内容是可以变的,比如下面的操作就是允许的 char str1[]=\"str1\"; char *const p = str1; p[0] = 'X'; printf(\"%s\", str1); 这时候str1的值就变成了\"Xtr1\" 我们再来看const char *p,根据前面提到的规律,将const和它后面一个类型交换变成char const *p(其实这种写法也是允许的,只是人们习惯将const写在最前面),这时候const修饰的是char,也就是说p指向的字符内容是不能变的。将上面两个例子的char *const p全部改成const char *p,则结果正好相反,第一个可以编译通过,第二个会报错。 其它时候就很好区分了,比如const int ,const string等等,总之,const修饰的是什么类型,这个类型的变量就不能被改变。 const修饰函数 先来看这样一个函数 const char * func(const char *str) const; 这样的函数比较夸张,有三个const,我们从左到右来一一说明: 1、第一个const修饰的是返回值,前面已经说过,这里的const修饰的是char,也就是说返回值的内容是不能被更改的 2、第二个const和第一个是一样的,这种用的比较多,它作为函数参数,表示的是这个参数在函数体内是不能被改动的(被传进来的实参并不要求是const类型),这样做是为了防止函数对实参做一些意外的操作,你试想下,当你调用一个函数时,你传进去一个变量是\"hello world!\",调完函数之后变成了\"fuck the world!\",这实在是不可忍的,所以我们在设计函数的时候,如果传进来的参数只作为读取使用,最好是将参数设成const类型。很多公司在面试让写代码的时候都会看中这个细节,你注意了这个细节不一定说明你牛逼,但你若没注意那肯定是会减分的。 3、再来说第三个const,按照我们最开始说的规律,const修饰的是它前面的所有数据类型,这里它前面的所有数据类型组合起来就是一个函数,这种类型一般出现在类成员函数里,当然,这里并不是说这个函数是不能变的,它代表的时这个函数不能改变类的成员变量,不管是public的还是private的 我们下面举例主要说明第三种情况,来看这样一个简单的程序 #include < stdio .h > class A { public : A () : x ( 0 ) , y ( 0 ) { } void func ( const int p ) { x = p ; y = p ; } int getY () { return y ; } int x ; private : int y ; } ; int main ( int argc , char * argv [] ) { A a ; printf ( \"x:%d y:%d\\n\" , a . x , a . getY ()); a . func ( 2 ); printf ( \"x:%d y:%d\\n\" , a . x , a . getY ()); return 0 ; } 这段代码是可以直接编译过的,运行结果是 x : 0 y : 0 x : 2 y : 2 我们稍作修改,将void func(const int p)改成void func(const int p) const再编译,就会直接报错,报错的两行代码是 x = p; y = p; 也就是说const类型的函数试图去修改类的成员变量是非法的,但是有一种情况例外,我们再在上面修改的基础上做一点修改,将int x改成mutable int x,将int y改成mutable int y,这时候程序又可以正常运行了,也就是说,如果成员变量是mutable类型的,它可以在任何场景下被修改。","tags":"Language","title":"C++ const知多少"},{"url":"https://chukeer.github.io/wordpress自定义页面显示所有文章列表.html","text":"wordpress博客里有两种类型的网页,一种叫文章,一种叫页面(page),文章就是你发表的每篇博客所在的网页,页面就是你网站导航栏里的那些链接,比如\"首页\",\"关于我\"这样的网页,这种网页的特点是集中展示某一类信息,比如首页展示每篇博客的摘要,\"关于我\"展示博主简介等等,自定义文章列表毫无疑问也是属于这一类的 page类型的网页都是根据模板生成的,wordpress默认没有这一类模板,因此需要自己写一个PHP脚本,首先我们找到模板所在的目录,假设你的wordpress所在目录为/var/www,那么模板脚本在/var/www/wp-content/themes/your_theme,其中your_theme是你所使用的主体包,在里面建立一个文件page-allpost.php,内容如下 <?php get_header (); ?> <style type= \"text/css\" > #table-allpost{border-collapse:collapse;} #table-allpost td,#table-allpost th{border:1px solid #98bf21;padding:3px 7px 2px 7px;text-align:center;} #table-allpost th{font-size:1.1em;text-align:center;padding-top:5px;padding-bottom:4px;background-color:#A7C942;color:#ffffff;} #table-allpost td{border:1px dotted #98bf21;} #table-allpost .td-left{text-align:left;} </style> <head><meta http-equiv= \"Content-Type\" content= \"text/html; charset=utf-8\" /></head> <div style= \"padding-bottom:10px\" ><strong> 全部文章 </strong></div> <div id= \"page-allpost\" > <table id= \"table-allpost\" > <tr> <th><strong> 编号 </strong></th> <th><strong> 发布时间 </strong></th> <th><strong> 标题 </strong></th> </tr> <?php $Count_Posts = wp_count_posts (); $Num_Posts = $Count_Posts -> publish ; query_posts ( 'posts_per_page=-1&caller_get_posts=1' ); while ( have_posts () ) : the_post (); $Num = sprintf ( \"%03d\" , $Num_Posts ); echo '<tr>' ; echo '<th>' . $Num . '</th>' ; echo '<td>' ; the_time ( get_option ( 'date_format' )); echo '</td><td class=\"td-left\";><a href=\"' ; the_permalink (); echo '\" title=\"' . esc_attr ( get_the_title () ) . '\">' ; the_title (); echo '</a></td></tr>' ; $Num_Posts -- ; endwhile ; wp_reset_query (); ?> </table> </div> <?php get_sidebar (); ?> <?php get_footer (); ?> 保存好之后,再去wordpress后台新建一个页面,注意不是发表文章,而是在仪表盘的\"页面\"一栏里选择新建页面,标题写\"全部文章\",内容为空,别名(固定链接)设置为\" allpost \",注意这里的别名必须和之前的脚本名page-allpost.php对应。点击保存,然后刷新你的站点首页,看看导航栏里是不是有了\"全部文章\"选项,点击进去看看是不是如下效果","tags":"Skill","title":"wordpress自定义页面显示所有文章列表"},{"url":"https://chukeer.github.io/python网页解析利器——BeautifulSoup.html","text":"python解析网页,无出BeautifulSoup左右,此是序言 安装 BeautifulSoup4以后的安装需要用eazy_install,如果不需要最新的功能,安装版本3就够了,千万别以为老版本就怎么怎么不好,想当初也是千万人在用的啊。安装很简单 wget \"http://www.crummy.com/software/BeautifulSoup/download/3.x/BeautifulSoup-3.2.1.tar.gz\" tar zxvf BeautifulSoup-3.2.1.tar.gz 然后把里面的BeautifulSoup.py这个文件放到你python安装目录下的site-packages目录下 site-packages是存放Python第三方包的地方,至于这个目录在什么地方呢,每个系统不一样,可以用下面的方式找一下,基本上都能找到 sudo find / -name \"site-packages\" -maxdepth 5 -type d 当然如果没有root权限就查找当前用户的根目录 find ~ -name \"site-packages\" -maxdepth 5 -type d 如果你用的是Mac,哈哈,你有福了,我可以直接告诉你,Mac的这个目录在/Library/Python/下,这个下面可能会有多个版本的目录,没关系,放在最新的一个版本下的site-packages就行了。使用之前先import一下 from BeautifulSoup import BeautifulSoup 使用 在使用之前我们先来看一个实例 现在给你这样一个页面 http://movie.douban.com/tag/%E5%96%9C%E5%89%A7 它是豆瓣电影分类下的喜剧电影,如果让你找出里面评分最高的100部,该怎么做呢 好了,我先晒一下我做的,鉴于本人在CSS方面处于小白阶段以及天生没有美术细菌,界面做的也就将就能看下,别吐 http://littlewhite.us/douban/xiju/ 接下来我们开始学习BeautifulSoup的一些基本方法,做出上面那个页面就易如反掌了 鉴于豆瓣那个页面比较复杂,我们先以一个简单样例来举例,假设我们处理如下的网页代码 < html > < head >< title > Page title </ title ></ head > < body > < p id = \"firstpara\" align = \"center\" > This is paragraph < b > one </ b > . </ p > < p id = \"secondpara\" align = \"blah\" > This is paragraph < b > two </ b > . </ p > </ body > </ html > 你没看错,这就是官方文档里的一个样例,如果你有耐心,看官方文档就足够了,后面的你都不用看 http://www.leeon.me/upload/other/beautifulsoup-documentation-zh.html 初始化 首先将上面的HTML代码赋给一个变量html如下,为了方便大家复制这里贴的是不带回车的,上面带回车的代码可以让大家看清楚HTML结构 html = ' <html><head><title> Page title </title></head><body><p id= \"firstpara\" align= \"center\" > This is paragraph <b> one </b> . </p><p id= \"secondpara\" align= \"blah\" > This is paragraph <b> two </b> . </p></body></html> ' 初始化如下: soup = BeautifulSoup(html) 我们知道HTML代码可以看成一棵树,这个操作等于是把HTML代码解析成一种树型的数据结构并存储在soup中,注意这个数据结构的根节点不是 ,而是soup,其中html标签是soup的唯一子节点,不信你试试下面的操作 print soup print soup.contents[0] print soup.contents[1] 前两个输出结果是一致的,就是整个html文档,第三条输出报错IndexError: list index out of range 查找节点 查找节点有两种反回形式,一种是返回单个节点,一种是返回节点list,对应的查找函数分别为find和findAll 单个节点 根据节点名 ## 查找head节点 print soup.find('head') ## 输出为 <head><title> Page title </title></head> ## or ## head = soup.head 这种方式查找到的是待查找节点最近的节点,比如这里待查找节点是soup,这里找到的是离soup最近的一个head(如果有多个的话) 根据属性 ## 查找id属性为firstpara的节点 print soup.find(attrs={'id':'firstpara'}) ## 输出为 <p id= \"firstpara\" align= \"center\" > This is paragraph <b> one </b> . </p> ## 也可节点名和属性进行组合 print soup.find('p', attrs={'id':'firstpara'}) ## 输出同上 根据节点关系 节点关系无非就是兄弟节点,父子节点这样的 p1 = soup.find(attrs={'id':'firstpara'}) ## 得到第一个p节点 print p1.nextSibling ## 下一个兄弟节点 ## 输出 <p id= \"secondpara\" align= \"blah\" > This is paragraph <b> two </b> . </p> p2 = soup.find(attrs={'id':'secondpara'}) ## 得到第二个p节点 print p2.previousSibling ## 上一个兄弟节点 ## 输出 <p id= \"firstpara\" align= \"center\" > This is paragraph <b> one </b> . </p> print p2.parent ## 父节点,输出太长这里省略部分 <body> ... </body> print p2.contents[0] ## 第一个子节点,输出u'This is paragraph' contents上面已经提到过,它存储的是所有子节点的序列 多个节点 将上面介绍的find改为findAll即可返回查找到的节点列表,所需参数都是一致的 根据节点名 ## 查找所有p节点 soup.findAll('p') 根据属性查找 ## 查找id=firstpara的所有节点 soup.findAll(attrs={'id':'firstpara'}) 需要注意的是,虽然在这个例子中只找到一个节点,但返回的仍是一个列表对象 上面的这些基本查找功能已经可以应付大多数情况,如果需要各个高级的查找,比如正则式,可以去看官方文档 获取文本 getText方法可以获取节点下的所有文本,其中可以传递一个字符参数,用来分割每个各节点之间的文本 ## 获取head节点下的文本 soup.head.getText() ## u'Page title' ## or soup.head.text ## 获取body下的所有文本并以\\n分割 soup.body.getText('\\n') ## u'This is paragraph\\none\\n.\\nThis is paragraph\\ntwo\\n.' 实战 有了这些功能,文章开头给出的那个Demo就好做了,我们再来回顾下豆瓣的这个页面 http://movie.douban.com/tag/%E5%96%9C%E5%89%A7 如果要得到评分前100的所有电影,对这个页面需要提取两个信息:1、翻页链接;2、每部电影的信息(外链,图片,评分、简介、标题等) 当我们提取到所有电影的信息后再按评分进行排序,选出最高的即可,这里贴出翻页提取和电影信息提取的代码 ## filename: Grab.py from BeautifulSoup import BeautifulSoup , Tag import urllib2 import re from Log import LOG def LOG ( * argv ): sys . stderr . write ( * argv ) sys . stderr . write ( ' \\n ' ) class Grab (): url = '' soup = None def GetPage ( self , url ): if url . find ( 'http://' , 0 , 7 ) != 0 : url = 'http://' + url self . url = url LOG ( 'input url is: %s ' % self . url ) req = urllib2 . Request ( url , headers = { 'User-Agent' : \"Magic Browser\" }) try : page = urllib2 . urlopen ( req ) except : return return page . read () def ExtractInfo ( self , buf ): if not self . soup : try : self . soup = BeautifulSoup ( buf ) except : LOG ( 'soup failed in ExtractInfo : %s ' % self . url ) return try : items = self . soup . findAll ( attrs = { 'class' : 'item' }) except : LOG ( 'failed on find items: %s ' % self . url ) return links = [] objs = [] titles = [] scores = [] comments = [] intros = [] for item in items : try : pic = item . find ( attrs = { 'class' : 'nbg' }) link = pic [ 'href' ] obj = pic . img [ 'src' ] info = item . find ( attrs = { 'class' : 'pl2' }) title = re . sub ( '[ \\t ]+' , ' ' , info . a . getText () . replace ( '&nbsp' , '' ) . replace ( ' \\n ' , '' )) star = info . find ( attrs = { 'class' : 'star clearfix' }) score = star . find ( attrs = { 'class' : 'rating_nums' }) . getText () . replace ( '&nbsp' , '' ) comment = star . find ( attrs = { 'class' : 'pl' }) . getText () . replace ( '&nbsp' , '' ) intro = info . find ( attrs = { 'class' : 'pl' }) . getText () . replace ( '&nbsp' , '' ) except Exception , e : LOG ( 'process error in ExtractInfo: %s ' % self . url ) continue links . append ( link ) objs . append ( obj ) titles . append ( title ) scores . append ( score ) comments . append ( comment ) intros . append ( intro ) return ( links , objs , titles , scores , comments , intros ) def ExtractPageTurning ( self , buf ): links = set ([]) if not self . soup : try : self . soup = BeautifulSoup ( buf ) except : LOG ( 'soup failed in ExtractPageTurning: %s ' % self . url ) return try : pageturning = self . soup . find ( attrs = { 'class' : 'paginator' }) a_nodes = pageturning . findAll ( 'a' ) for a_node in a_nodes : href = a_node [ 'href' ] if href . find ( 'http://' , 0 , 7 ) == - 1 : href = self . url . split ( '?' )[ 0 ] + href links . add ( href ) except : LOG ( 'get pageturning failed in ExtractPageTurning: %s ' % self . url ) return links def Destroy ( self ): del self . soup self . soup = None 接着我们再来写个测试样例 ## filename: test.py #encoding: utf-8 from Grab import Grab import sys reload ( sys ) sys . setdefaultencoding ( 'utf-8' ) grab = Grab () buf = grab . GetPage ( 'http://movie.douban.com/tag/喜剧?start=160&type=T' ) if not buf : print 'GetPage failed!' sys . exit () links , objs , titles , scores , comments , intros = grab . ExtractInfo ( buf ) for link , obj , title , score , comment , intro in zip ( links , objs , titles , scores , comments , intros ): print link + ' \\t ' + obj + ' \\t ' + title + ' \\t ' + score + ' \\t ' + comment + ' \\t ' + intro pageturning = grab . ExtractPageTurning ( buf ) for link in pageturning : print link grab . Destroy () OK,完成这一步接下来的事儿就自个看着办吧 本文只是介绍了BeautifulSoup的皮毛而已,目的是为了让大家快速学会一些基本要领,想当初我要用什么功能都是去BeautifulSoup的源代码里一个函数一个函数看然后才会的,一把辛酸泪啊,所以希望后来者能够通过更便捷的方式去掌握一些基本功能,也不枉我一字一句敲出这篇文章,尤其是这些代码的排版,真是伤透了脑筋 The end.","tags":"Language","title":"python网页解析利器——BeautifulSoup"}]}