什么是IO
IO
:指既能收数据也能发数据。Linux
下socketfd
(文件描述符)就是一种IO
观察程序启动流程
strace 可执行文件
- 使用该条命令可以查看到进程是如何启动的
是由bash
所在的进程调用execve
来启动的进程
调用main
函数前操作系统会进行什么操作
- 加载可执行文件:操作系统会将程序的可执行文件从磁盘读取到内存中,并进行一些必要的校验工作,例如校验文件格式、校验文件权限等。
- 分配内存:操作系统会为程序分配内存空间,包括代码段、数据段、堆和栈等。代码段存储程序的指令,数据段存储程序的全局变量和静态变量,堆和栈用于动态内存分配和函数调用。
- 解析动态链接库:如果程序使用了动态链接库,操作系统会在加载程序时解析这些库,并将它们链接到程序中。
- 初始化进程环境:操作系统会初始化进程的环境,包括设置进程的根目录、文件描述符、信号处理器等。
- 启动程序:最后,操作系统会定位
main
函数的入口地址,并跳转到这个地址开始执行程序。
Linux
内核kernel
没有main
函数,他是如何执行的
通过硬件引导程序(bootloader
)启动
- 在计算机启动时,硬件会首先加载硬件引导程序(
bootloader
),它通常位于硬盘的第一个扇区(也称为主引导记录)。硬件引导程序的作用是加载内核映像(kernel image
)到内存中,并跳转到内核的入口点开始执行。 - 在
Linux
内核中,入口点的符号名为_start
。当硬件引导程序将内核映像加载到内存中后,会将控制权转移给_start
符号所在的地址,从而开始执行内核的初始化过程。 - 在内核的初始化过程中,会进行各种硬件初始化、内存管理、进程管理等操作,最终启动一个称为
init
的用户进程作为系统的第一个进程,并将控制权转交给它,让它继续进行系统初始化和启动其他进程。
服务器常用IP
地址IADDR_ANY
、127.0.0.1
、虚拟机网卡指定IP
之间的区别
IADDR_ANY
:值为0
,用点分十表达则为0.0.0.0
,表示绑定本机随便选一个网卡的ip
127.0.0.1
:表示监听监听本地回环接口,只能用于本机访问- 指定
IP
:绑定指定的本地网卡IP
地址
例子:远程访问非本机的Mysql
的服务失败,在确定了其防火墙是关闭的情况,可以考虑是其配置文件的bind-address
默认是设置的127.0.0.1
只允许其本机访问,所以可以将其设置为0.0.0.0
,监听所有可用的网络接口,这样也许就能解决无法访问远程mysql
服务问题
服务器基本框架的简单理解
socket、bind、listen
- 去吃饭,进门找到接引人(
listen
),他会将你带去找到点菜(发送数据)的人(accept
)
注意一种情况
send返回>0
不等于发送成功,(只是代表将数据发送到了协议栈)
阻塞与非阻塞
简单的服务器框架socket、bind、listen、accept
阻塞在accept
阻塞的主要原因是监听的文件描述符默认是阻塞的
实现多个客户端连接,并且发送数据
多线程与多进程实现
来一个请求accept
之后开辟一个线程去处理任务
**好处:**逻辑简单,线程只为一个fd
服务,不用担心其他线程or进程来处理
**缺点:**代价太大
io
多路复用
如何单线程实现多个客户端同时连接?
- 不采取任何措施下多个客户端的连接(
listenfd
的io
有效)是没有问题的
但是不知道什么时候执行accept
,
(后续的业务处理)不知道什么时候recv
,不知道什么时候send
io
多路复用就是检测io
是否有事件(事件:描述符上是否有可读可写)- 如何标识一个
IO
的事件->可读(有还是没有)可写(有还是没有)->select
用一个bit
位表示
select
如何理解select
…
- 一个人(server)喜欢去东莞,他跟很多技师(
fd
)关系很好,经常去,但是每次去的时候都要访问所有技师(fd
)关于以下信息
- 晚上有没有时间?(
io
可读) - 是否还愿意?(
io
可写)
- 后来他找了一个秘书(
select
),他会叫她先去东莞采集完这些信息后再去,这个秘书的主要职责就是看这些技师(fd
)是否可以办正事(recv\send
)
select
接口介绍
IO
多路复用是一种同时监控多个文件描述符(包括套接字)的技术,它允许一个进程可以同时等待多个IO
操作完成,而不是阻塞在单个IO
操作上。其中一个常用的IO
多路复用技术是select
。
select
函数可以同时监视多个文件描述符,等待其中任何一个文件描述符就绪(可读、可写或异常),然后通知应用程序进行相应的操作。使用select可以避免阻塞在单个IO
操作上,提高程序的效率和响应速度。
在使用select
函数时,需要准备一个文件描述符集合,包括需要监视的所有文件描述符,然后将该集合传递给select
函数。select
函数会不断地监视这些文件描述符,直到其中任何一个文件描述符就绪,然后返回就绪文件描述符的数量,并更新原始的文件描述符集合。
在编写基于select
的程序时,需要注意以下几点:
- 设置文件描述符为非阻塞模式,以避免阻塞在单个IO操作上。
- 每次调用select时,需要重新设置原始的文件描述符集合,以避免之前的就绪文件描述符被遗漏。
- 在处理就绪文件描述符时,需要注意其对应的IO操作是否已经完成,以避免出现错误的操作。
函数原型
#include <sys/select.h> int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout );
nfds
:需要监视的文件描述符集合中所有文件描述符的最大值加1。readfds
:监视可读性的文件描述符集合。writefds
:监视可写性的文件描述符集合。exceptfds
:监视异常性的文件描述符集合。timeout
:select()
超时时间,设置为NULL
表示阻塞等待,设置为0
表示立即返回,设置为大于0
的值表示等待指定时间后返回。
小demo
只判断了io
是否可读
- 注册
select
读检测
// socket bind listen.. struct sockaddr_in clientaddr; socklen_t len = sizeof(clientaddr); fd_set rfds, rset; FD_ZERO(&rfds); // 清空bit位 FD_SET(sockfd, &rfds); // 设置listenfd位 int maxfd = sockfd; // 设置最大fd int clientfd = 0;
select
while (1) { // master rset = rfds; // 设置副本 保证同一循环下检测队列不会改变 int nready = select(maxfd+1, &rset, NULL, NULL, NULL); if (FD_ISSET(sockfd, &rset)) { // 判断是否有客户端连接 clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len); printf("accept: %d\n", clientfd); FD_SET(clientfd, &rfds); if (clientfd > maxfd) maxfd = clientfd; if (-- nready == 0) continue; } int i = 0; for (i = sockfd+1; i <= maxfd;i ++) { if (FD_ISSET(i, &rset)) { // 判断客户端io是否可读 char buffer[BUFFER_LENGTH] = {0}; int ret = recv(clientfd, buffer, BUFFER_LENGTH, 0); if (ret == 0) { close(clientfd); break; } printf("ret: %d, buffer: %s\n", ret, buffer); send(clientfd, buffer, ret, 0); } } }
细节注意点:
关于accept
所做的事情
listenfd
在监听到客户端连接后执行accept
会清空掉其描述符上客户端写进来的数据。这样只有在下次客户端到来的时候listenfd
描述符上才会有io
提示,select
才会刚好在客户端连接的时候检测到listenfd
的动静。
如果使用了select
来检测listenfd
,之后没有写accept
,则select
每次循环都会检测listenfd
有数据
关于select
的读写集合设置副本的原因
- 是为了在同一循环中保证一开始想要检测的固定数量的文件描述符
io
不会变多或变少
maxfd
设置为什么要+1
- 内核中轮询的代码就类似于下面这种
for(int i = 0; i < maxfd_; i++){ // }
- 这种判断是
<
要想包含maxfd
那就必须+1