linux/unix编程手册-61_64

368 阅读11分钟

title: linux/unix编程手册-61_64 date: 2018-10-07 11:53:07 categories: programming tags: tips

linux/unix编程手册-61(SOCKET 高级特性)

流套接字上的部分读和部分写

  • 如果没有足够的缓冲区来传输所有字节并且满足以下任意一个(关于write的fd不是普通文件时的原子性可以再搜搜)
    • write()调用之后被信号中断(一次write,buf小于缓冲区的大小,是原子性的操作?)
    • 套接字工作在非阻塞模式下。可能当前只传输了一部分请求字节(非阻塞模式下缓冲区不足只写入部分?)
    • 部分字节传输完成后出现了异步错误,比如TCP链接出现问题
  • readn和writen的实现,避免部分读写
#include<unistd.h>
#include<errno.h>

ssize_t readn(int fd, void *buffer, size_t n){
    ssize_t numRead;
    size_t totRead;
    char *buf;
    
    buf = buffer;
    for (totRead = 0; totRead < n;){
        numRead = read(fd, buf, n-totRead);
        if (numRead == 0)
            return totRead;
        if (numRead == -1){
            if(errno==EINTR)
                continue;
            else
                return -1;
        }
        totRead += numRead;
        buf += numRead;
    }
    return totRead;
}

ssize_t writen(int fd, const void *buffer, size_t n){
    ssize_t numWritten;
    size_t totWritten;
    const char* buf;
    
    buf = buffer;
    for (totWritten = 0; totWritten < n){
        numWritten = write(fd, buf, n-totWritten);
        
        if (numWritten <= 0){
            if (numWritten == -1 && errno == EINTR)
                continue;
            else
                return -1;
        }
        totWritten += numWritten;
        buf += numWritten;
    }
    return totWritten;
}

shutdown()系统调用

#include<sys/socket.h>

int shutdown(int sockfd, int how);

socket上调用close会关闭双向两端,shutdown会调节,可使一个方向上进行套接字传输,通过how

  • SHUT_RD:关闭读端,之后的读操作返回文件EOF,但数据可以写入;在UNIX域流套接字执行SHUT_RD,对端进程会受到SIGPIPE
  • SHUT_WR:关闭写端,后续本地的写操作会产生SIGPIPE信号和EPIPE错误,对端写入可以在套接字上读取,ssh连接会用到
  • SHUT_RDWR:shutdown会关闭套接字通道无论套接字是否关联其它文件描述符,是针对打开文件来的,而close是针对文件描述符,需要所有socket的fd都关闭才会断开
  • 后两个TCP会主动关闭,避免对TCP使用SHUT_RD
#include<sys/socket.h>

ssize_t recv(int sockfd, void *buffer, size_t length, int flags);

ssize_t send(int sockfd, const void *buffer, size_t length, int flags);

flags的具体参数略,flags给对于socket的I/O,提供了read,write的拓展

sendfile的优化

#include<sys/sendfile.h>

ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);
// 返回实际传输的字节数,-1 error
  • out_fd通常指向套接字
  • in_fd文件必须是可以mmap的,套接字不行

linux的TCP sendfile()的优化 (例如http请求,首部信息胡通过write,页面数据可以sendfile,导致TCP传输2个报文,网络带宽利用率低)

  • linux TCP_CORK选项,
#include<sys/socket.h>

int getsockname(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

int getpeername(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
// 0 success, -1 error

其他基于TCP的一些略

一些常用的命令

  • netstat:显示系统中Internet和UNIX域套接字状态
  • tcpdump:

其他高级功能(当手册查吧)

linux/unix编程手册-62(终端(串行终端,键盘,显示器之类的))

终端驱动程序

  • 规范模式:终端输入按行处理,且打开了行编辑功能(可以删除行等等之类)
  • 非规范模式:终端输入不会被装配成行 ,禁用了行编辑,读操作的完成时间等也是由c_cc数组中的位或其他参数决定的

获取和修改终端属性

#include<termios.h>

struct termios {
        tcflag_t c_iflag;       /* Input flags */
        tcflag_t c_oflag;       /* Output flags */
        tcflag_t c_cflag;       /* Control flags */
        tcflag_t c_lflag;       /* Local modes */
        cc_t c_line;            /* Line discipline (nonstandard)*/
        cc_t c_cc[NCCS];        /* Terminal special characters */
        speed_t c_ispeed;       /* Input speed (nonstandard; unused) */
        speed_t c_ospeed;       /* Output speed (nonstandard; unused) */
};

int tcgetattr(int fd, struct termios *termios_p);

int tcsetattr(int fd, int optional_actions, const struct termios *termios_p);

// 0 s, -1 e
// fd必须是指向终端的描述符

具体略,各种flag略

stty

stty -a

略(偏了)

使用命令tty,表示当前终端对应的设备文件,(以下#表示数字)

  • 结果显示:/dev/pts/# 表示伪终端
  • 结果显示:/dev/tty# 表示虚拟终端
  • 结果显示:/dev/console 表示物理终端(控制台)
  • 结果显示:/dev/ttys# 表示串行终端

linux/unix编程手册-63(其他被选I/O模型)

传统的read,write,没有设置O_NONBLOCK时,会以阻塞模式打开文件,无法处理以下需求

  • 以非阻塞检查文件描述符是否可执行I/O操作
  • 同时检查多个文件描述符看看能不能执行I/O操作

之前不好的处理方案

  • 非阻塞I/O盲轮训
  • 多进程或多线程

好的解决方案,这些技术不会实际执行I/O只是告诉我们某个文件描述符就绪了,等准备好了才会进行I/O调用。

  • I/O多路复用(select(), poll())
    • 可移植性好
    • 延展性差,fd多时效率低
  • 信号驱动I/O(和异步I/O的区别是,内核通知信号后,会等待数据从内核态传到用户态,异步I/O不会等待内核传输过程,因为会将数据准备好复制到用户空间之后才会通知,看看posix的aio,go的话是基于CSP模型 CSP和Actors)
    • 完全利用信号I/O的特点需要用到不可移植的linux的专有特性,移植性不好
    • 可以高效大量fd
  • linux epoll api(也属于I/O多路复用)
    • 专属linux
    • 和信号驱动I/O比较
      • 避免了处理信号复杂性
      • 指定检查类型(读就绪还是写就绪)
      • 可以选择水平触发或者边缘触发

插一个

libevent 提供了I/O事件的抽象(不包含异步I/O?)

水平触发/边缘触发

  • 水平触发通知:如果文件描述符可以非阻塞的执行I/O系统调用,认为它准备就绪
    • 水平触发允许我们重复检测文件描述符的就绪状态,因此没有必要每次触发后尽可能多的执行I/O会导致其他文件描述符饥饿状态
  • 边缘触发通知:如果文件描述符自上次状态检测以来有了新的活动(I/O),需要触发通知
    • 只有I/O事件发生时我们才会收到通知,应当在收到通知后尽可能多的执行I/O,不然的话下次轮训虽可用但不会通知,同时文件描述符通常应当设置成非阻塞。

备选I/O模型应当需要和O_NONBLOCK标志一起使用

I/O多路复用

#include<sys/time.h>
#include<sys/select.h>

int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
//return ready fd 数量, 0 timeout, -1 error
// fdset最大1024,即FD_SETSIZE
//select返回后,fdset只包含就绪的fd

void FD_ZERO(fd_set *fdset);
//置空
void FD_SET(int fd, fd_set *fdset);
//加
void FD_CLR(int fd, fd_set *fdset);
//删
int FD_ISSET(int fd, fd_set *fdset);
// 1在,否则不在
  • readfds:输入是否就绪的文件描述符集合
  • writefds:输出。。。
  • exceptfds:一些特殊情况的文件描述符集合
    • 流式套接字上收到了带外数据
    • 连接到信包模式下的伪终端主设备上的从设备状态发生了改变
  • nfds需要比三个fdset最大文件描述符大1。提高select效率(select返回之后,可以根据0-nfds来遍历)
  • timeout
    • 两个域都为0则不会阻塞(没有情况吧)
    • NULL或timeout不为0会阻塞置
      • fdset至少一个就绪
      • 被信号终端
      • 超时
  • 返回值为正数时,如果同一fd在对个set中都存在,都就绪了,会被重复计算
#include<poll.h>

int poll(struct pollfd fds[], nfds_t nfds, int timeout);
//return ready fd 数量, 0 timeout, -1 error
//select 将fd放在三个集合中,poll则在pollfd上标明需要检查的类型

struct pollfd{
    int     fd;
    short   events;         //需要检查的类型掩码
    short   revents;        //实际发生的类型掩码
};
// 掩码常亮,略
  • timeout:
    • -1,一直阻塞直到有ready的
    • 0,不会阻塞
    • 大于0,阻塞毫秒数

不同fd,select和poll的表现(可参考5.9,相关内容是基础)

  • 普通文件(因为read,write不会阻塞)
    • 总会被select标记为可读,可写,poll后再revents中返回POLLIN,POLLOUT
  • 终端和伪终端
  • 管道和FIFO
  • 套接字

select 和 epoll对比

  • 实现都使用了内核poll例程集合(不同于poll系统调用,具体以后查一下)
    • poll是为每个fd调用内核poll例程
    • select 通过宏将例程信息转化为select事件
  • 性能
    • fd范围小或者密集时效率差不多
    • fd稀疏时poll性能会好很多(不需要遍历0~nfd)
  • api(略)

select和poll的问题

  • 每次调用内核必须检查所有被指定的fd
  • 每次调用需要传递一个表示所有被检查文件fd的数据结构到内核(select还每次都要初始化这个数组)
  • 每次返回。需要检查数据结构中的每个元素
  • select和poll的调用结果内核不会缓存

信号驱动I/O

使用步骤

  1. 设定信号处理例程,默认情况下内核发送的信号是SIGIO
  2. 设定文件描述符的属主,通常调用进程会是属主fcntl(fd, F_SETOWN, pid)
  3.  flags=fcntl(fd, F_GETFL)
     fcntl(fd, F_SETFL, flags | O_ASYNC | O_NONBLOCK)    
    
  4. 等待内核的信号(边缘触发),尽可能多执行I/O直到失败

信号驱动I/O 可应用在套接字,终端/伪终端及其他设备,管道/FIFO, inotify文件描述符

曾今信号驱动I/O也叫异步I/O,现在异步I/O特指, POSIX AIO规范提供的功能,POSIX AIO 进程会请求内核执行一次I/O操作,当I/O完成或出错之后,进程会得到内核的通知

不同fd触发I/O就绪的信号的条件(略)

优化信号驱动I/O的使用

  • 在同时检查大量fd时,信号驱动I/O的优势在于,内核会记住要检查的文件描述符(O_ASYNC),仅当I/O事件实际发生时,才会发生信号
  • 可以通过使用实时信号取代SIGIO来优化性能
    • 因为SIGIO不会排队,处理了第一个,后续的通知会丢失
    • 如果信号处理例程是通过sigaction来安装,可以通过sa.sa_flags指定SA_SIGINFO,传递是哪个fd,发生了何种事件
  • 具体略

epoll 编程接口

  • 性能上和信号驱动I/O相似but
    • 避免复杂的信号处理流程(ex:信号队列溢出)
    • 可以指定检查类型
#include<sys/epoll.h>

int epoll_create(int size);
// return fd  success ,-1 error
// size 已经被忽略了,之前是内核为数据结构划分的初始大小

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *ev);
// 0 s, -1 e

struct epoll_event {
    uint32_t        events;     /* 感兴趣事件集合*/
    epoll_data_t    data;       /* fd就绪时,回传的信息*/
};

typedef union epoll_data {
    void            *ptr;       /* Pointer to user-defined data */
    int             fd;         /* File descriptor */
    uint32_t        u32;        /* 32-bit integer */
    uint64_t        u64;        /* 64-bit integer */
} epoll_data_t;

int epoll_wait(int epfd, struct epoll_event *evlist, int maxevents, int timeout);
// return ready fd 数量, 0 timeout, -1 error
// timeout的处理poll

epoll_create()

  • 内核会在内存中创建一个新的i-node,并打开文件描述符

epoll_ctl()

  • fd 不能是普通文件,否则会EPERM, fd可以是epoll fd ,建立层次关系
  • op
    • EPOLL_CTL_ADD:fd已存在会EEXIST
    • EPOLL_CTL_MOD:通过ev修改fd上的设定事件,fd需要已经在列表中,否则ENOENT
    • EPOLL_CTL_DEL:fd 不存在会ENOENT
  • 多线程中一个线程,epoll_ctl修改了列表,另一个线程epoll_wait()的列表会同步,会立刻更新

epoll_wait()

  • maxevents应该只是如果超过,只会返回这么多,没有等待这么多才返回的语义
  • EPOLLONESHOT, fd就绪一次之后,会将fd置为非激活状态,若重新监控需要epoll_ctl重设
  • 使用dup()类似函数复制一个epoll_fd时,或fork()时,新的epoll_fd指向同一epoll数据结构(中文版翻译错了)
  • 监控和的fd映射的打开文件描述(第二层)关联的所有fd都关闭后,epoll会自动把它从监控列表删除

epoll和I/O多路复用的对比

  • 每次调用poll/select,内核会检查所有指定的文件描述符,每次调用都会和内核传递一次文件描述符的数据结构;
  • epoll在epoll_ctl指定监控fd时,内核会在打开文件描述上下文想关联列表记录这个fd,每当一个fd就绪时,会在epoll_fd的就序列表添加一个元素(一个文件的I/O会使所有关联fd就绪),每次调用epoll_wait()不需要从用户空间传数据到内核空间,只有内核返回。
  • epoll默认是水平触发,和select/epoll类似
  • epoll设置边缘触发之后,一次epoll_wait时,一个fd有多个I/O事件,会合并成一次单独的通知(信号驱动会是多个)
    • 优化,记录下每个就绪fd,不要一次读完一个fd,设定一个限度。(水平的话与需要做限度,但是不用记录)

linux/unix编程手册-64(SOCKET 伪终端)

伪终端程序通过IPC,连接终端程序

伪终端程序流程(server 端,例如ssh)

  • 驱动程序打开伪终端主设备(sshd)
  • fork()一个子进程
    • 调用setsid()改变会话id,创建一个新会话,子进程会成为新会话的首进程
    • 打开从设备,子进程成为从设备的控制进程
    • 用dup()类系统调用复制标准的fd,0,1,2
    • 调用exec()启动连接到伪终端从设备的面向终端程序(shell)

伪终端的系统调用(略)

伪终端I/O(类似双向管道,不同fd关闭的异常,信包模式)(略)

END