linux高性能服务器的搭建
IO模型
阻塞IO和非阻塞IO的区别就在于对非就绪事件的处理, 对于设置了O_NONBLOCK的文件描述符来说,若此时文件描述符非可读或者可写,针对其进行的系统调用会返回-1,设置errno,而对未设置的文件描述符的connect,recv,send会阻塞进程
管道pipe是环形缓冲区
O_NONBLOCK对一般文件而言没有什么影响,所有一般文件都是可读或者可写的,它主要用来在fcntl时设置socket或者字符设备的属性,详情请见O_NONBLOCK on regular file
字符设备,即流设备,如tty,pts,拥有自己的缓冲区,并且采用行缓冲,每个进程的STDIN_FILENO,STDOUT_FILENO软链接到当前终端中(/dev/stdin及其他也和这三个文件描述符软链接),所以使用splice等系统调用从标准输入读取中会读取换行符
同步或者异步I/O主要是指访问数据的机制(即实际I/O操作的完成方式),同步一般指主动请求并等待I/O操作完毕的方式,I/O操作未完成前,会导致应用进程挂起;而异步是指用户进程触发IO操作以后便开始做自己的事情,而当IO操作已经完成的时候会得到IO完成的通知(异步的特点就是通知),这可以使进程在数据读写时也不阻塞。阻塞或者非阻塞I/O主要是指I/O操作第一阶段的完成方式(进程访问的数据如果尚未就绪),即数据还未准备好的时候,应用进程的表现,如果这里进程挂起,则为阻塞I/O,否则为非阻塞I/O。说白了就是阻塞和非阻塞是针对于进程在访问数据的时候,根据IO操作的就绪状态来采取的不同方式,说白了是一种读取或者写入操作函数的实现方式,阻塞方式下读取或者写入函数将一直等待,
而非阻塞方式下,读取或者写入函数会立即返回一个状态值。
而在并发模式中,同步指的是程序严格按照代码顺序执行, 异步指的是程序的执行需要系统事件来驱动
关于splice函数的盲点
SPLICE_F_MORE对于写入端是socket的情况下有效,如果设置了 SPLICE_F_MORE 那么会追加 MSG_MORE flag;如果当前 pipe 中还有其他的 page,则追加 MSG_SENDPAGE_NOTLAST flag。
首先来看 MSG_MORE 的作用,熟悉 TCP 的应该已经知道了这个标志表示调用者有更多的数据要发送。在 TCP socket 中,该标志用于获取与 TCP_CORK 相同的效果,不同之处在于该标志可以在每次调用时设置。顾名思义 TCP_CORK 就是给 TCP 发送数据的时候加了一个木塞子。往这个 socket 写入的数据都会聚集起来。虽然堵上了塞子,但是数据总得发送,取决于:
程序取消设置 TCP_CORK 这个选项
socket 聚集的数据大于一个 MSS 的大小
自从堵上塞子写入第一个字节开始,已经经过 200ms
socket 被关闭了
此外,当设置SPLCICE_NONBLOCK标志时,若读取端设置为非阻塞且没有准备好数据,则会触发EAGAIN错误
三种设备驱动
即为dev目录下的设备文件是如何实际硬件进行交互的,一般是通过驱动程序进行
进一步-VFS虚拟文件系统
Linux中的文件描述符与打开文件之间的关系
两个不同的文件描述符,若指向同一个打开文件句柄,将共享同一文件偏移量
信号
常用信号 | ||
---|---|---|
信号 | 含义 | 默认行为 |
SIGINT | 键盘输入以中断进程(Ctrl + C) | term(终止进程) |
SIGQUIT | 键盘输入使进程退出(Ctrl + ) | core(结束进程并生成核心转储文件) |
SIGSTOP | 暂停进程(Ctrl + S), 该信号不得被捕获或者忽略 | stop(暂停进程) |
SIGCHLD | 子进程状态发生变化(退出或者暂停) | ign(忽略信号) |
SIGPIPE | socket连接对方的读端已经关闭或者管道的读端已经关闭,此时发送方使用send或者往管道的写端写数据,都会触发该信号 | term |
SIGHUP | 控制终端被挂起(Ctrl + Z),可以使用kill -HUP来向进程发送该信号 | term |
SIGTERM | kill命令默认发送信号 | term |
- 信号处理函数指针__sighandler_t
- 系统定义两种处理函数
- SIG_DFL (__sighandler_t) 0 采用信号默认处理方式 /整形->函数指针强制类型转换/
- SIG_IGN (__sighandler_t) 1 忽略信号
- 系统定义两种处理函数
- 自定义信号处理函数:sigaction系统调用
参数:- sig,要捕获的信号
- act,struct sigaction*
- 结构体中的sa_handler是信号处理函数指针,用于指定函数处理函数
- sa_mask,类型为信号集sigset_t,用于设定增加的信号掩码(在原进程基础上)
- sa_mask指定了一个信号集的掩码,这些信号在信号处理程序执行期间应该被阻塞(即添加到调用信号处理程序的线程的信号掩码中)。此外,除非使用了SA_NODEFER标志,否则触发处理程序的信号也将被阻塞。
- sa_flags, 整形数据,用于设置程序接收到信号时的行为
- SA_RESTART, 含义是重新调用被该信号终止的系统调用
- SA_NODEFER, 一般情况下,在接收信号并进入其信号处理函数时,该信号会被加入至线程的信号掩码中,防止接收同种信号,引发竞态条件,设置该选项可以使得同种信号也被线程接收
成功返回0,失败返回-1并设置errno
- struct sigset 用于表示一组信号集,其中定义了一个长整型数组,每个元素的每个位都代表着一个信号, 这组信号集可以用来设置进程掩码,标志着某个信号将会被阻塞,手册上给出了掩码和阻塞的说明:
- 一个信号可能会被阻塞,这意味着直到解除阻塞之前它将不会被传递。在生成和传递之间的时间内,信号被称为待处理的(pending)。
- 进程中的每个线程都有一个独立的信号掩码,它指示线程当前阻止的信号集。线程可以使用pthread_sigmask(3)来操作其信号掩码。在传统的单thread线程应用程序中,可以使用sigprocmask(2)来操作信号掩码。
- sigprocmask函数也可以用sigset来指定新的进程掩码,通过how参数可以设置新掩码集和当前掩码集的交互方式
- 可以使用sigpending函数获取被挂起的信号集,当通过设置进程掩码导致新的信号被屏蔽时,新的信号会被操作系统设置为挂起的信号,若取消对信号的屏蔽,则可以立刻被进程接收到
- fork出的子线程会继承父线程的信号集合,但会拥有空的挂起信号集
- 正在阻塞的系统调用可以被信号处理函数中断(interrupt), 若在sigaction建立信号处理函数时设置了SA_RESTART标志,则某些系统调用可以在信号处理函数返回之后重启(restart), 有些则不论是否设置该标志都会失败,并设置errno为EINTR
- SA_RESTART标志有效的系统调用包括: read,write,ioctl,open(如果能被阻塞,比如当打开FIFO时),以及常见的socket接口
- SA_RESTART标志无效的系统调用包括: 通过setsockopt设置socket选项为超时SO_RECVTIMEO的输入输出socket接口, 以及IO复用select,poll,epoll_wait
- sleep函数永远不会重启但会成功返回,并返回剩余的sleep秒数
- 当挂起进程的控制终端时,SIGHUP将被触发,对于没有控制终端的网络后台程序来说,通常利用SIGHUP信号来强制服务器重读配置文件
- linux环境下,内核通知应用程序**带外数据(大小1字节,通常被tcp接收端存储到特殊缓存中,称为带外缓存)**到来的方式有两种:
- 使用IO复用函数监听事件集中的异常事件,select等系统调用在接收到带外数据时将返回,并向应用程序报告socket上的异常事件,此时应用程序可以调用FD_ISSET等函数来查看异常事件是否发生
- 使用SIGURG信号,此时应用程序可设置信号处理函数来读取带外数据
对于带外缓存来说,可以设置连接socket的SO_OOBINLINE选项来将带外数据存放在接收端的普通数据输入队列中,此时我们不能使用MSB_OOB来读取数据
进程间通信 IPC
如果文件事先已经存在,
open(pathname, O_RDWR | O_CREAT,0666); 打开成功,返回一个大于0的fd
open(pathname, O_RDWR | O_CREAT | O_EXCL,0666); 打开失败,返回-1O_EXCL表示的是:如果使用O_CREAT时文件存在,就返回错误信息,它可以测试文件是否存在。并且此时检查文件和打开文件是原子性操作,避免了进程临界区竞争
在NFS(网络文件系统)上,O_EXCL可能不是原子性的,此时可以调用link系统调用创建hard link来查看文件是否存在(通过使用stat可以查看st_nlink的数量)
IPC对象包括:信号量(semaphore),共享内存,消息队列, System V 和 POSIX对于三种进程间通信的接口不同,具体内容如下:
特性 | System V IPC | POSIX IPC |
---|---|---|
IPC 机制 | 管道、命名管道、消息队列、信号、信号量、共享内存、套接字、Unix 域套接字 | 管道、命名管道、消息队列、信号量、共享内存、套接字 |
共享内存接口调用 | shmget(),shmat(),shmdt(),shmctl() | shm_open(),mmap(),shm_unlink() |
消息队列接口调用 | msgget(),msgsnd(),msgrcv(),msgctl() | mq_open(),mq_send(),mq_receive(),mq_unlink() |
信号量接口调用 | semget(),semop(),semctl() | 命名信号量:sem_open(),sem_close(),sem_unlink(),sem_post(),sem_wait(),sem_trywait(),sem_timedwait(),sem_getvalue() 无名或基于内存的信号量:sem_init(),sem_post(),sem_wait(),sem_getvalue(),sem_destroy() |
IPC 对象的标识 | 键和标识符 | 名称和文件描述符 |
监视 POSIX 消息队列 | select(),poll(),epoll() | NA |
消息队列控制 | msgctl() | 提供函数(mq_getattr()和mq_setattr()) |
线程同步 | 互斥锁、条件变量、读写锁 | 多线程安全 |
消息队列通知 | NA | 提供少量通知特性(例如mq_notify()) |
状态/控制操作 | 需要系统调用(shmctl())、命令(ipcs, ipcrm) | 可使用系统调用(例如fstat(), fchmod())检查和操作共享内存对象 |
共享内存段的大小 | 在创建时固定(通过shmget()) | 可使用ftruncate()调整底层对象的大小,然后重新创建映射使用munmap()(或Linux特定的mremap()) |
进程的退出
当exec失败时,应该使用_exit(或其同义词_Exit)来终止子进程,因为在这种情况下,子进程可能通过调用其atexit处理程序、调用其信号处理程序和/或刷新缓冲区来干扰父进程的外部数据(文件)。
1
2
3
4
5
6
7
8
9
10switch (fork()) {
case 0:
// we're the child
execlp("some", "program", NULL);
_exit(1); // <-- HERE
case -1:
// error, no fork done ...
default:
// we're the parent ...
}当使用fork(),尤其是vfork()时,exit()和_exit()之间存在一些差异,这些差异变得很重要。
fork()会将父进程的虚拟地址空间复制一份给子进程,并在需要时写时复制(copy on write), 否则二者映射到内存同一位置,若某方对数据发生了改变,则父进程将复制一份数据给子进程,此时二者的函数调用栈在内存中的位置不同,vfork()则是二者共享一片内存区域,此时子进程若使用return语句,则会弹栈,破坏父进程函数调用栈,exit()则不会更改函数栈结构,而是将回收内存任务交给操作系统,接下来看一个例子
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22#include <stdio.h>
#include <unistd.h>
void stack1() {
vfork();
}
void stack2() {
_exit(0);
}
int main() {
stack1();
printf("%d goes 1\n", getpid());
stack2();
printf("%d goes 2\n", getpid());
return 0;
}
- 父进程stack1后阻塞在vfork()调用,子进程从vfork()返回,并执行stack2()并退出, 在执行stack2()之前子进程会将main函数的返回地址改为stack2()的下一句,所以父进程从stack1()返回时,会执行goes2语句,很神奇!
exit()和_exit()的基本区别在于前者执行与库中用户模式结构相关的清理,并调用用户提供的清理函数,而后者仅执行进程的内核清理,比如关闭所有文件描述符。
在fork()的子分支中,通常不使用exit(),因为这可能导致stdio缓冲区被刷新两次,并且临时文件被意外删除。在C++代码中,情况更糟,因为静态对象的析构函数可能会被错误地运行。(有一些不寻常的情况,如守护进程,其中父进程应调用_exit()而不是子进程;适用于绝大多数情况的基本规则是:每次进入main时只应调用一次exit()。)
在vfork()的子分支中,使用exit()甚至更加危险,因为它会影响父进程的状态。
从glibc 2.27开始,abort()函数终止进程时不会刷新流。POSIX.1允许两种可能的行为,即abort()函数“可能会尝试对所有打开的流执行fclose()操作”。
- std::terminate调用的就是abort()函数