接下来四周时间,我将会做一个高并发服务器相关的项目。
前置知识:操作系统系统编程、网络编程、基础的数据结构、C语言。
开发环境:VMware虚拟机:Ubuntu 20.04.6 LTS、vscode
今天先回顾一些基础知识。
1.文件与IO
标准IO(缓冲IO)
- fread->FILE*
- fwrite
- fclose
基础IO
- open->fd
- read(recv recvfrom)
- write
- close
一切接文件
缓冲池、线程池、连接池
非阻塞IO(nonblock)
- 如何使用?
- 如何设置一个非阻塞IO
- O_NONBLOCK
- 应用场景?
- 返回-1:EAGAIN
- 轮询地去查看IO是否完成
- 或者使用IO多路复用机制:EPOLL(边缘模式、水平模式)
同步和异步
协议三要素:语法、语义、同步
异步适合于并发。
高级IO
- select:select count(*) from fds where fd become ready;
- poll
- epoll
IO多路复用、IO感知、IO多路转接
select、poll和epoll是Linux系统中用于实现IO多路复用的三种机制。它们允许程序同时监控多个文件描述符(file descriptor),以便知道哪个或哪些文件描述符已经准备好进行读写操作。这三种机制的发展体现了从简单到高效的递进关系。
1. select
- 基本概念:`select`是最早的IO多路复用机制。它允许程序监控多个文件描述符,等待一个或多个文件描述符成为非阻塞状态。
- 限制:
- 它支持的文件描述符数量有限,通常受到FD_SETSIZE的限制,默认值通常是1024。
- 每次调用`select`时,都需要把整个文件描述符集合从用户空间复制到内核空间,这在文件描述符数量较多时会造成较大的性能开销。
- 每次调用返回时,需要遍历整个文件描述符集合来找出已经准备好的描述符,这也增加了额外的开销。
2. poll
- 基本概念:`poll`提供了与`select`类似的功能,但解决了`select`的一些限制。
- 改进:
- `poll`使用链表而非固定大小的数组,因此不再受FD_SETSIZE的限制,可以监控更多的文件描述符。
- 与`select`类似,`poll`也需要在每次调用时将整个文件描述符集合从用户空间复制到内核空间,并在返回时检查哪些文件描述符已准备就绪。
3. epoll
- 基本概念:`epoll`是在Linux 2.6中引入的,它在性能和可扩展性方面对`select`和`poll`进行了显著的改进。
- 改进:
- `epoll`可以处理数以万计的并发连接,而不会显著降低性能。
- 它通过在内核中使用一个事件表来避免每次调用时复制整个文件描述符集合的开销。只有当IO状态真正改变时,应用程序才需要与内核交互。
- 支持两种模式:LT(水平触发)和ET(边缘触发)。ET模式可以进一步减少系统调用的次数,提高效率。
- 只有准备就绪的文件描述符会被返回,减少了不必要的遍历。
从`select`到`poll`,再到`epoll`,这三种机制在设计上越来越高效和灵活。`select`和`poll`适合管理少量连接,而`epoll`适合处理大量并发连接,特别是在高性能服务器环境中。随着网络应用对高并发和高性能的需求不断增加,`epoll`成为了Linux下实现高效IO多路复用的首选方案。
2.进程
什么是进程?
程序的映像(image),实例(instance)、运行的程序,动态的、资源分配的基本单位(虚拟内存)、封闭性、独立性
程序、进程、线程、管程、协程的区别?
- 程序(Program): 是一组指令和数据的集合,是静态的,存储在磁盘上,需要加载到内存中才能执行。
- 进程(Process): 是程序的一次执行过程,是操作系统进行资源分配和调度的基本单位,具有独立的内存空间和系统资源,进程之间相互独立。
- 线程(Thread): 是进程中的实际执行单位,一个进程可以包含多个线程,线程共享进程的内存空间和系统资源,但拥有独立的执行路径。
- 管程(Monitor): 是一种用于并发编程的同步机制,通过提供对共享资源的互斥访问和条件变量的支持来实现线程之间的协作。
- 协程(Coroutine): 是一种轻量级的线程,可以在不同的执行路径之间切换,但不需要操作系统的支持,由程序员自行控制协程的调度。
fork干了什么事情?
pid判断谁是父亲,谁是孩子、统计信息等不同。
进程三大部分:PCB、数据段、程序段完全拷贝父进程。
写拷贝,如果不做修改,就不拷贝。进程采用段页式管理。拷贝只拷贝一页。
linux采用完全公平算法,放弃了时间片轮转算法。但是仍然具有时间片轮转调度算法的特点。所以往往是父进程先运行。不太可能刚好卡在时间片结束的时候创建子进程。
exec族函数
- p - path (路径): 使用环境变量`PATH`来查找可执行文件。
- v - vector (向量): 参数以字符串数组形式传递。
- l - list (列表): 参数逐个列出。
- e - environment (环境): 允许设置环境变量。
#include <stdio.h>
#include <unistd.h>
int main() {
// 使用execlp执行ls命令
execlp("ls", "ls", "-l", NULL);
// 使用execvp执行ps命令
char *args[] = {"ps", "aux", NULL};
execvp("ps", args);
// 使用execl执行echo命令
execl("/bin/echo", "echo", "Hello, World!", NULL);
// 使用execve执行自定义程序
char *cmd = "./custom_program";
char *args[] = {"arg1", "arg2", NULL};
char *env[] = {"PATH=/usr/bin", NULL};
execve(cmd, args, env);
// 如果exec函数执行成功,下面的代码将不会被执行
perror("exec failed");
return 1;
}
特殊进程
-
孤儿进程:当一个父进程结束或终止时,它的子进程还在运行,这些还在运行的子进程就会变成孤儿进程。操作系统通常会让init进程(进程号为1的进程)接管这些孤儿进程。接管后,init进程将成为它们的新父进程,负责收集它们的退出状态。
-
僵尸进程:当一个子进程结束运行,但其父进程尚未通过调用wait()或waitpid()函数来收集子进程的退出状态时,该子进程将成为僵尸进程。僵尸进程已经释放了大部分资源,不再执行任何代码,但在进程表中仍保留一个条目,直到父进程收集其状态信息。
-
闲逛进程(空闲进程):在操作系统中,闲逛进程(也称为空闲进程或空转进程)是一个特殊的系统进程,当系统中没有其他可运行的进程时,它就会被执行。闲逛进程的主要目的是占用CPU,保证CPU不会处于空闲状态。在多数系统中,闲逛进程的进程号为0。
-
守护进程:是一种在后台运行的特殊进程,它独立于控制终端,周期性地执行某种任务或等待处理某些发生的事件。守护进程通常在系统引导装入时启动,在系统关闭时终止。与普通的前台进程相比,守护进程的特点是独立于用户和终端,不与任何终端交互。
进程间通信
- 基于文件的进程间通信
- 共享内存(shmget、mmap数据的拷贝)
- pipe(匿名、有名)
- 条件变量:pthread_cond_signal、pthread_cond_wait(惊群效应)
- 消息队列
- socket
- 信号
3.线程
什么是线程?
Pthread(Process thread):共享性(共享的是进程空间、竞争、同步 & 互斥:PV操作、信号量)、调度的基本单位,虚拟处理器(让线程感觉自己独占CPU)。
用户线程 & 内核线程
内核能感知到线程吗?内核能感知到内核线程,但是感知不到用户线程。
线程模型
- 1:1:频繁地对内核线程进行切换。
- N:1:内核就一个线程,用户N个线程。假设N中的一个阻塞,所有的都会阻塞。
- M:N:M代表程序的逻辑分支,N代表着程序被调度的机会。
线程的同步
- PV
- 互斥锁
- 死锁问题
- 怎么处理死锁(死锁的定义:两个以上资源,资源有限,进程&线程推进不当、死锁避免:银行家算法、死锁预防、死锁检测)
线程的安全
- 什么是线程的安全?
- 临界资源:多线程环境下recv能否正确接收
- 临界区:访问临界资源的那段代码,锁的是临界资源
- 某个函数线程安全吗?
线程池
- 池化技术(TCP三次连接的时候很费事,搞个池子一直连着,不需要频繁地创建销毁线程)
- 怎么实现:构建一个循环任务队列(理发店的等候区)、多个线程(理发店的理发师)、CPU(理发店的工作台)
4.网络编程
socket
应用程序和运输层之间的接口:进程寻址(端口+IP地址)、地址复用
socket编程基础
- socket
- bind
- listen:backlog(
backlog
参数的意义在于指定了socket可以排队的最大连接数) - accept:返回一个新的socketfd
- connect
- send & recv(read, write)
- close
UDP & TCP
- 区别
- 三次握手
- 为什么不是两次,四次可以吗
- 每次发的都是什么包?
- 四次挥手
- 为什么要等2个MSL
- 为什么四次?
- 状态转换
- TCP拥塞控制
- UDP编程
- connect
- recvfrom
- sendto
网络编程只是系统编程的点缀,本质还是进程间通信。
参数解释及常用的函数方法
socket(AF_INET, SOCK_STREAM, 0)
调用是在 C/C++ 中创建网络套接字的方式之一,主要用于网络通信。这个函数调用涉及到的参数非常关键,下面将对这些参数进行解释:
-
AF_INET: 这是地址族(Address Family)参数,
AF_INET
表示使用 IPv4 网络协议。这是目前最广泛使用的网络类型之一,用于在网络上进行通信。除了AF_INET
,还有AF_INET6
用于 IPv6 地址,以及其他一些选项用于特定类型的通信(如AF_UNIX
用于同一台机器上的进程间通信)。 -
SOCK_STREAM: 这是套接字类型参数。
SOCK_STREAM
表示创建的套接字是面向连接的,提供序列化的、可靠的、双向的和基于连接的字节流。这种类型的套接字通常用于 TCP(Transmission Control Protocol)协议。除了SOCK_STREAM
,还有SOCK_DGRAM
用于无连接的数据报服务(通常与 UDP 协议一起使用),以及其他一些选项。 -
0: 这是协议参数。在大多数情况下,给这个参数传递 0 就可以根据地址族和套接字类型自动选择合适的协议。对于
AF_INET
和SOCK_STREAM
的组合,这通常意味着使用 TCP 协议。如果需要使用特定协议,可以在此处指定,如 IPPROTO_TCP 或 IPPROTO_UDP,但对于 TCP 和 UDP,通常传递 0 就足够了。
htons(prot)
函数用于将主机字节顺序(Host Byte Order)的无符号短整型数值转换为网络字节顺序(Network Byte Order)。这个函数在进行网络编程时非常重要,因为不同的计算机架构可能有不同的字节顺序(即大端序和小端序),而在网络上传输数据时,需要统一使用网络字节顺序,即大端序。
inet_addr("0.0.0.0")
接受一个以点分十进制格式表示的 IP 地址字符串(如 "192.168.1.1"
)作为参数,并将其转换为用于网络通信的网络字节顺序(大端序)的整数值。
"0.0.0.0"
是一个特殊的 IP 地址,用于表示所有可用的主机地址。在服务器端使用此地址作为监听地址时,它意味着服务器将接受连接到该主机上的任何可用网络接口的客户端连接。这对于多网卡的主机尤其有用,因为它允许服务器在所有网络接口上监听,而不需要指定具体的 IP 地址。
setsockopt()
函数用于设置套接字选项,允许程序员控制套接字行为的各个方面。在这个特定的调用中,它被用来设置 SO_REUSEADDR
套接字选项,这对于开发网络应用特别有用。
函数原型
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);
- sockfd: 套接字文件描述符,指定要操作的套接字。
- level: 指定选项的代码级别;要访问套接字的选项,则此参数应该设置为
SOL_SOCKET
。 - optname: 需要访问的选项名;在这个例子中,它是
SO_REUSEADDR
。 - optval: 指向包含新选项值的缓冲区的指针。
- optlen: 现在选项的值的大小,以字节为单位。
SO_REUSEADDR
选项
- 目的: 允许重用本地地址和端口。这对于服务器应用尤其重要,因为它允许服务器重启时立即重新绑定到其端口上,即使之前的连接仍处于 TIME_WAIT 状态。没有这个选项,你可能会遇到 "Address already in use" 错误。
- 参数: 在这个调用中,
reuse
是一个整数变量,其值一般设置为非零值(通常是1),以启用SO_REUSEADDR
选项。
int socket_create(int port) {
int sockfd;
if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
return -1;
}
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
addr.sin_addr.s_addr = inet_addr("0.0.0.0");
int reuse = 1;
setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, (char *)&reuse, sizeof(int));
if (bind(sockfd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
return -1;
}
if (listen(sockfd, 8) < 0) {
return -1;
}
return sockfd;
}
fcntl(fd, F_GETFL)
这行代码是C语言中用于操作文件描述符标志的函数调用。它使用了`fcntl`函数,该函数是一个多功能的文件控制函数,可以对打开的文件描述符进行各种控制操作。`fcntl`函数的原型如下:
int fcntl(int fd, int cmd, ... /* arg */ );
- `fd`:要操作的文件描述符。
- `cmd`:指定要执行的操作类型。
- `...`:根据`cmd`的不同,可能需要提供额外的参数。
在你提供的代码中,`fcntl`函数用于获取文件描述符`fd`的当前状态标志。参数解释如下:
- `fd`:是一个文件描述符,它是一个非负整数,用于标识一个打开的文件。
- `F_GETFL`:是`cmd`参数的一个值,表示获取文件状态标志的操作。
函数调用`fcntl(fd, F_GETFL)`的作用是查询`fd`指向的文件的当前访问模式(如读、写)和文件状态标志(如非阻塞和同步I/O标志)。
函数返回值:
- 成功时,返回文件描述符的访问模式和文件状态标志。
- 失败时,返回-1,并设置`errno`以指示错误原因。
简而言之,这行代码的目的是获取指定文件描述符`fd`的当前文件状态标志。