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
使用步骤
- 设定信号处理例程,默认情况下内核发送的信号是SIGIO
- 设定文件描述符的属主,通常调用进程会是属主
fcntl(fd, F_SETOWN, pid)flags=fcntl(fd, F_GETFL) fcntl(fd, F_SETFL, flags | O_ASYNC | O_NONBLOCK)- 等待内核的信号(边缘触发),尽可能多执行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)