study notes: high performance linux server programming

1:linux网络API分为:socker地址API,socker基础API,网络信息API

  1,socker地址API:包含IP地址和端口(ip, port)。表示TCP通信的一端。

  2,socker基础API:创建/命名/监听socker,接收/发起链接,读写数据,获取地址信息,检测带外标记和读取/设置socker选项。sys/socket.h

  3,网络信息API:主机名和IP地址的转换,服务名和端口号的转换。netdb.h

2:socket和API的函数 和 相关知识。

  1,函数。

1 IP地址转换函数
  <arpa/inet.h>
  in_addr_t inet_addr( const char* strptr );  // 格式转换:十进制字符串IP -> 网络字节序(大端)IP
  int inet_aton( const char* cp, struct in_addr* inp );  // 格式转换:如上,但将数据保存到参数inp中
  char* inet_ntoa( struct in_addr in );  //  格式转换:网络字节序(大端)IP -> 十进制字符串IP
  int inet_pton( int af, const char* src, void* dst ); // 格式转换:字符串IP地址 -> 网络字节序(大端)整数IP
  const char* inet_ntop( int af, const void* src, char* dst, socklen_t cnt );  格式转换:网络字节序(大端)整数IP -> 字符串IP地址
  // inet_ntoa函数不可重入。
  // inet_ntop返回目标储存单元的地址。
2 创建socket
  <sys/types.h> <sys/socket.h>
  int socket( int domain, int type, int protocol );
  // 1 参数domain:使用的底层协议族。
  // 2 参数type:指定服务类型。SOCK_STREAM(TCP) SOCK_UGRAM(UDP)
  // 3 参数protocol:指定具体协议。一般默认为0.(前面参数已经确定了协议)
  // 4 返回:socket文件描述符。失败-1,设置errno
3 命名socket
  <sys/types.h> <sys/socket.h>
  int bind( int sockfd, const struct sockaddr* my_addr, socklen_t addrlen );
  // 1 参数addrlen:socket地址长度。
  // 2 返回:成功0,失败-1,设置errno
4 监听socket
  <sys/socket.h>
  int listen( int sockfd, int backlog );
  // 1 参数sockfd:指定被监听socket
  // 2 参数backlog:监听队列最大长度。典型值:5
5 接收链接。
  <sys/types.h> <sys/socket.h>
  int accept( int sockfd, struct sockaddr* addr, socklen_t* addrlen );
  // 1 参数sockfd:执行过listen的socket。
  // 2 参数addr:被接受链接的远程socket地址。
  // 3 参数addrlen:地址长度。
  // 4 返回:成功新的链接socket,失败-1设置errno
  // 5 accept只从监听队列取出连接,并不检测网络状态。
6 发起链接
  <sys/types.h> <sys/socket.h>
  int connect( int sockfd, const struct sockaddr *serv_addr, socklen_t addrlen );
  // 1 参数sockfd:由socket系统调用返回一个socket
  // 2 参数serv_addr:服务器监听的socket地址。
  // 3 返回:成功0,失败-1设置errno
7 关闭连接
  <unistd.h>
  int close( int fd );
  // 1 不会立即关闭,只是将引用减一。只有引用为0时才会真正关闭。
  <sys/socket.h>
  int shutdown( int sockfd, int howto );
  // 1 立即关闭连接。
  // 2 参数howto:决定函数的行为。SHUT_RD/WR/RDWR:可以分别控制读写关闭。
  // 3 返回:成功0,失败-1设errno
8 TCP数据读写。
  <sys/types.h> <sys/socket.h>
  ssize_t recv( int sockfd, void* buf, size_t len, int flags );  // 返回:实际读取到的数据长度。失败-1errno
  ssize_t send( int sockfd, const void* buf, size_t len, int flags );  // 返回:实际写入的数据长度。失败-1reeno
  // 1 参数buf,len:缓冲区的地址和大小。
  // 2 参数flags:数据收发额外控制。通常设0.
9 UDP数据读写。
  <sys/types.h> <sys/socket.h>
  ssize_t recvfrom( int sockfd, void* buf, size_t len, int flags, struct sockaddr* src_addr, socklen_t *addrlen );
  ssize_t sendto( int sockfd, const void* buf, size_t len, int flags, const struct sockaddr* dest_addr, socklen_t *addrlen );
  // 1 参数src/dest_addr:确定发送/接收端地址。(因UDP没有连接概念)
  // 2 这两个函数同样可以应用于TCP读写数据。
10 通用数据读写函数。
  <sys/socket.h>
  ssize_t recvmsg( int sockfd, struct msghdr* msg, int flags );
  ssize_t sendmsg( int sockfd, struct msghdr* msg, int flags );
  struct msghdr{
    void* msg_name;             // socket 地址
    socklen_t msg_namelen;    // 地址长度
    struct iovec* msg_iov;      // 分散的内存块
    int msg_iovlen;             // 分散内存块的数量
    void* msg_control;      // 辅助数据地址
    socklen_t msg_controllen;   // 辅助数据大小
    int msg_flags;              // 保存函数flag参数
  }
11 确定下一个数据是否时带外数据。
  <sys/socket.h>
  int sockatmark( int sockfd );  // 返回:带外1,否则0
12 获取地址信息。
  <sys/socket.h>
  int getsockname( int sockfd, struct sockaddr* address, socklen_t* address_len );  // 本端
  int getpeername( int sockfd, struct sockaddr* address, socklen_t* address_len );  // 远端
  // 1 返回:成功0,失败-1errno
13 获取/设置socket选项
  <sys/socket.h>
  int getsockopt( int sockfd, int level, int option_name, void* option_value, socklen_t* restrict option_len );
  int setsockopt( int sockfd, int level, int option_name, const void* option_value, socklen_t* restrict option_len );
  // 1 参数level:具体协议选项。
  // 2 返回:成功0,失败-1errno。

// 此后为网络信息API
14 获取主机完整信息。
  <netdb.h>
  struct hostent* gethostbyname( const char* name );  // 通过主机名称获取
  struct hostent* gethostbyaddr( const void* addr, size_t len, int type );  //通过IP地址获取
  // 1 参数type:IP地址的类型。
  // 2 返回:主机信息。
15 获取服务的完整信息。
  <netdb.h> 
  struct servent* getservbyname( const char* name, const char* proto );  // 通过名字获取
  struct servent* getservbyport( int port, const char* proto );  // 通过端口号获取
  // 1 参数proto:确定服务类型:UDP/TCP
16 获取主机和服务的综合函数。
  int getaddrinfo( const char* hostname, const char* service, const struct addrinfo* hints, struct addrinfo** result );
  int getnameinfo( const struct sockaddr* sockaddr, socklen_t addrlen, char* host, socklen_t hostlen, char* serv, socklen_t servlen, int flags );

 

   2,88-94在完成初版IM时,需要进行测试

3:高级IO(高编已由函数省略)

  1,函数

1 两个文件描述符之间直接传递数据
  ssize_t sendfile( int out_fd, int in_fd, off_t* offset, size_t count );
  // 1 参数count:传输字节数
  // 2 返回:成功返回传输字节数,失败-1errno
  // 3 in_fd:必须时真实文件,out_fd:必须是socket
2 两个文件描述符之间移动数据。零copy操作
  ssize_t splice( int fd_in, loff_t* off_in, int fd_out, loff_t* off_out, size_t len, unsigned int flags );  // 具体需要再了解和使用。
3 两个管道文件描述符之间复制数据。零copy操作
  ssize_t tee( int fd_in, int fd_out, size_t len, unsigned int flags );

 

4:linux服务程序规范。

  1,函数。

1 和日志进程通信。
  <syslog.h>
  void syslog( int priority, const char* message );
  // 1 参数priority:设置值与日志级别按位与。
2 设置日志进程的输出方式。
  void openlog( const char* ident, int logopt, int facility );
  // 1 参数ident:添加到日志消息的日期和时间之后。
  // 2 参数facility:用来修改syslog函数中的默认值。
3 设置日志进程的掩码。(设置屏蔽信息)
  int setlogmask( int maskpri );
4 关闭日志功能。
  void closelog();
5 使进程称为后台进程:
  <unistd.h>
  int daemon( int nochdir, int noclose );
  // 1 参数nochdir:传0,则工作目录为根目录/。否则不变。
  // 2 参数noclose:传0,则被重定向为/dev/null。
  // 3 返回:成功0,失败-1errno。

 

  2,基本规范。

    1)一般以后台进程形式运行。后台进程又称为守护进程。

    2)通常有一套日志系统。至少能输出日志到文件。

    3)一般以某个专门的非root身份运行。

    4)通常时可配置的。

    5)启动时,会生成一个PID文件并存入/var/run目录中。

    6)通常需要考虑系统资源和限制。

  3,日志进程:rsyslogd。

    1)能够接收 用户/内核 的日志。

  4,ps命令可查看进程、进程组和会话之间的关系。

5:高性能服务器框架。

  1,函数。

  2,零散细节。

    1)客户连接请求时随即到达的异步事件,服务器需要使用某种IO模型来监听。

    2)服务器可以解构成三个主要模块:IO处理单元,逻辑单元,储存单元。

    3)服务器基本模块的功能描述。

模块 单个服务器程序 服务器机群
IO处理单元 处理客户连接,读写网络数据 作为接入服务器,实现负载均衡
逻辑单元 业务进程或线程 逻辑服务器
网络储存单元 本地数据库、文件或缓存。 数据库服务器
请求队列 各单元之间的通信方式。 各服务器之间的永久TCP连接

    4)IO服用函数本身时阻塞的,它们能提高程序效率的原因:它们具有同时监听多个IO事件的能力。

    5)同步IO向应用程序通知的是IO就绪事件,异步IO向应用程序通知的是IO完成事件。

    6)服务器程序通常需要处理三类事件:IO事件,信号和定时事件。

    7)如果程序是计算密集型, 并发没有任何优势。但如果是IO密集型,并发就会有意义。因为IO速度比CPU缓慢。

    8)并发模式中,同步指程序完全按照代码序列顺序执行(用于处理客户逻辑),异步指程序执行需要由系统事件来驱动(用于处理IO)。

    9)主线程向工作线程发送scoket最简单的方式:往管道中写数据。

    10)池是一组资源的集合。这组资源在服务器启动之初就被完全创建好并初始化。常见的有:内存池,线程池,进程池和连接池。

    11)高性能服务器应该避免不必要的数据复制。(避免数据复制,共享内存是最块的方式,但不一定是最好的。

    12)并发程序必须考虑上下文切换的开销。

    13)尽量减少锁的开销。

  3,P2P模式:peer to peer。

    1)P2P模式时,主机之间很难相互发现。

  4,reactoer模式。

    1)它要求主线程(IO处理单元)只负责监听文件描述符是否有事件发生,有的话就立即将该事件通知工作线程(逻辑单元)。

    2)特点:主线程只确定有请求,工作线程针对具体事件类型针对处理。

  5,proactor模式:它将所有IO操作都交给主线程和内核来处理。工作线程仅仅负责业务逻辑。。

  6,半同步/半反应堆模式:主线程用来监听IO,并发送请求到请求队列。然后工作线程竞争抢夺处理权限。

    1)主线程和工作线程共享请求队列。需要锁来保证读写,消耗CPU

    2)每个工作线程同一时间只能处理一个客户请求。

  7,leader/follewer模式:leader监听scoket。当有新请求时,leader去处理请求,并从follower中推选新leader。

    1)需要包含几个组件:句柄集,线程集,事件处理器,具体事件处理器。

6:IO复用

  1,函数。

1 epoll系列:linux特有的IO复用函数。
  <sys/epoll.h>
  int epoll_create( int size ); // 创建一个描述符(标识事件表)
  int epoll_ctl( int epfd, int op, int fd, struct epoll_event *event ); // 对 事件表 进行操作(添加,修改,删除注册事件)。
  int epoll_wait( int epfd, struct epoll_event* events, int maxevents, int timeout ); // 等待事件。
2

 

  2,零散细节。

    1)IO复用虽然能同时监听多个文件描述符,但它本身是阻塞的。

    2)epoll系列有两种模式:LT和ET。ET是相对高效的模式。

    3)ET模式的文件描述符都应该是非阻塞的。(如果是阻塞的,可能会一直阻塞下去)

    4)EPOLLONESHOT事件:使事件只能触发一次。若需要再次触发,需要重置。可以防止重复读写。

  3,select,poll,epoll的区别

系统调用 select poll epoll
事件集合 用户通过三个参数分别传入可读,可写和异常事件。内核通过这些参数在线修改就绪事件。每次调用都会重读 统一处理所有事件类型。需要一个事件集。用户通过pollfd.event传入事件。内核通过修改pollfd.event反馈就绪事件。 内核通过事件表直接管理所有事件。无需反复传入事件。epoll_wait仅用来反馈就绪事件(不需要多余判断)
应用程序索引就绪文件描述符的时间复杂度 n n

1

最大支持文件描述符数 一般有最大值限制 65535 65535
工作模式 LT LT LT/ET
内核实现和工作效率 轮询方式检测就绪事件 轮询 回调方式检测就绪事件。

 

7:信号。

  1,函数

  2,零散细节。

    1)服务器程序必须处理(至少能忽略)一些常见信号。以避免异常终止。

    2)信号是一种异步事件:信号处理函数和程序的主循环是两条不同的执行路线。所以信号需要尽快完成。典型的解决方案:信号仅提供信号,具体处理尽量方在主循环中。

8:时间堆:将所有定时器超时时间最小的一个定时器的超时值作为心搏间隔。

9:高性能IO框架库。

  1,常见框架:ACE,ASIO,Libevent。

  2,linux服务器程序必须处理三类事件:IO,信号和定时事件。但需要考虑三个问题

    1)统一事件源。需要统一管理三类事件。一般方法为:利用IO复用系统调用来管理所有事件。

    2)可移植性。

    3)对并发编程的支持。

  3,IO框架库以库函数的形式,封装了较为底层的系统调用,给应用程序提供了一组更便于使用的接口。

  4,Libevent框架库。

    1)跨平台支持,拥有统一事件源,线程安全,基于Reactor模式。

    2)官方网站:libevent.org

  5,P240,需要执行代码确认

10:多线程编程。

  1,线程实现方式:完全在用户空间实现,完全由内核调度和双层调度。

    1)完全在用户空间实现优点:创建和调度线程无需内核的干预,速度很快。创建多各线程对系统性能没有太大影响。但无法在多CPU情况下使用。

    2)内核调度:和用户空间完全相反。

    3)双层调度:上两种模式的混合体。

11:进程池和线程池。

  1,线程池中的线程数量应该和CPU数差不多。

  2,进程之间(有关联进程)传递数据最简单的方法是管道。

12:服务器调制,调试和测试。

  1,linux平台的一个优秀特性是内核微调:可以通过修改文件的方式来调正内核参数。

  2,调试方法:tcpdump抓包看数据,或者gdb。

  3,linux对应用程序能打开的最大文件描述符数量有两个层次的限制:用户级限制和系统级限制。

  4,gdb调试多进程的两个简单方法:

    1)找到PID,然后使用attach添加到gdb中。

    2)set follow-fork mode(parent or child)

  5,gdb调试多线程的方法。

    1)info threads:显示当前可调试的所有线程。

    2)threadID:调试ID线程

    3)set scheduler-locking(off|on|step):off所有线程执行,on只有当前线程执行,step单步调试时,只有当前线程会执行。

  6,压力测试:IO复用,多线程,多进程并发编程等方法(不懂- -)。

13:常用工具。

  1,tcpdump:经典的网络抓包工具

  2,lsof:列出当前系统打开的文件描述符的工具。

  3,nc:用来快速构建网络连接。

    1)服务器方式运行时:监听某个端口并接收客户端连接。

    2)客户端方式运行时:可以向服务器发起连接并收发数据。

    3)所以可以用来调试客户端和服务器。

  4,strace:测试服务器性能的工具。

    1)跟踪程序运行过程中执行的系统调用和接收到的信号,并将系统调用名、参数、返回值及信号名数处到标准输出或指定文件。

  5,netstat:网络信息统计工具。

    1)可以打印本地网卡接口上的全部连接、路由表信息、网卡接口信息。

  6,ifstat:interface statistics。简单的网络流量检测工具。

  7,mpstat:multi-processor statistics。能实时监测多处理器系统上的每个CPU的使用情况。

posted on 2015-02-05 16:43  zheng39562  阅读(266)  评论(0编辑  收藏  举报

导航