时分复用
CPU单核在同一时刻只能做一件事情,一种解决办法是对CPU进行时分复用(多个事件流将CPU切割成多个时间片,不同事件流的时间片交替进行)。在计算机系统中,我们用线程或者进程来表示一条执行流,通过不同的线程或进程在操作系统内部的调度,来做到对CPU处理的时分复用。这样多个事件流就可以并发进行,不需要一个等待另一个太久,在用户看起来他们似乎就是并行在做一样。
PC寄存器 程序计数器 代码执行到哪里了 程序下一条要执行什么。
有没有一种可以在单线程/进程中处理多个事件流的方法呢?一种答案就是IO多路复用。
因此IO多路复用解决的本质问题是在用更少的资源完成更多的事。
IO模型
- 1、阻塞IO
- 2、非阻塞IO EAGAIN 忙等待 errno
- 3、信号驱动IO SIGIO 用的相对少(了解)
- 4、并行模型 进程,线程
- 5, IO多路复用 select、poll、epoll
1、阻塞IO ===》最常用 默认设置
没有数据到来时,可以让任务挂起,节省CPU资源消耗,提高系统效率。
以管道读写为例子:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <errno.h>int main(int argc, const char *argv[])
{int ret = mkfifo("fifo",0666);if(-1 == ret){if(EEXIST == errno){}else{perror("mkfifo error");exit(1);}}int fd = open("fifo",O_RDONLY);if(-1 == fd){perror("open error");exit(1);}while(1){char buf[100] = {0};read(fd,buf,sizeof(buf));printf("fifo:%s\n",buf);bzero(buf,sizeof(buf));fgets(buf,sizeof(buf),stdin);printf("terminal:%s",buf);fflush(stdout);}close(fd);return 0;
}
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <errno.h>
int main(int argc, const char *argv[])
{int ret = mkfifo("fifo",0666);if(-1 == ret){if(EEXIST == errno){}else{perror("mkfifo failed");exit(1);}}int fd = open("fifo", O_WRONLY);if(-1 == fd){perror("open failed");exit(1);}while(1){char buf[100] = "hello,this is fifo test";write(fd,buf,strlen(buf));sleep(3);}close(fd);return 0;
}
2、非阻塞IO ===》在阻塞IO的基础上调整其为不再阻塞等待。
在程序执行阶段调整文件的执行方式为非阻塞:
===》fcntl() ===>动态调整文件的阻塞属性
程序未接收到数据时一直执行,效率低。
fcntl()
#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* arg */ );
功能:修改指定文件的属性信息。
参数:fd 要调整的文件描述符
cmd 要调整的文件属性宏名称
... 可变长的属性值参数。
返回值:成功 不一定,看cmd
失败 -1;
int flag = fcntl(fd,F_GETFL);
fd 是要修改的文件描述符,可能是打开的文件、套接字或其他I/O设备。F_GETFL 是一个预定义的常量,告诉 fcntl 函数获取与文件描述符关联的标志。
fcntl(fd,F_SETFL ,flag | O_NONBLOCK );
F_SETFL 常量用于设置文件描述符的标志。flag | O_NONBLOCK 是将获取到的当前标志与 O_NONBLOCK 常量进行按位或操作,目的是在现有标志的基础上添加非阻塞标志。
//noblock_fifo_r.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <errno.h>
#include <fcntl.h>
int main(int argc, const char *argv[])
{int ret = mkfifo("fifo",0666);if(-1 == ret){if(EEXIST == errno){}else{perror("mkfifo error");exit(1);}}int fd = open("fifo",O_RDONLY);if(-1 == fd){perror("open error");exit(1);}int flag = fcntl(fd,F_GETFL);//获取当前文件描述符的标志:fcntl(fd,F_SETFL,flag|O_NONBLOCK);//设置文件描述符为非阻塞模式:flag = fcntl(0,F_GETFL);// 0 是标准输入的文件描述符 stdinfcntl(0,F_SETFL,flag|O_NONBLOCK);while(1){char buf[100] = {0};if(read(fd,buf,sizeof(buf))>0){printf("fifo:%s\n",buf);}bzero(buf,sizeof(buf));if(fgets(buf,sizeof(buf),stdin)){ printf("terminal:%s",buf);fflush(stdout);}}close(fd);return 0;
}
//noblock_fifo_w.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <errno.h>
int main(int argc, const char *argv[])
{int ret = mkfifo("fifo",0666);if(-1 == ret){if(EEXIST == errno){}else{perror("mkfifo failed");exit(1);}}int fd = open("fifo", O_WRONLY);if(-1 == fd){perror("open failed");exit(1);}while(1){char buf[100] = "hello,this is fifo test";write(fd,buf,strlen(buf));sleep(3);}close(fd);return 0;
}
3.select循环服务器
- select监听的集合中的文件描述符有上限限制
- select有内核层向用户层数据空间拷贝的过程,占用系统资源开销
- select必须轮询检测产生事件的文件描述符
- select只能工作在水平触发模式(低速模式),无法工作在边沿触发(高速模式)
本来是read和fgets分别监听fd和stdin , 轮回监听数据描述符。
用select函数来动态检测有数据流动的文件描述符。
- 创建fd集合
- 加入fd
- select()
- 检测fd
- read/write
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
功能:完成指定描述符集合set中有效描述符的动态检测。
该函数具有阻塞等待功能,在函数执行完毕后
目标测试集合中将只保留最后有数据的描述符。
参数:nfds 描述符的上限值,一般是链接后描述符的最大值+1;
readfds 只读描述符集
writefds 只写描述符集
exceptfds 异常描述符集
以上三个参数都是 fd_set * 的描述符集合类型
timeout 检测超时 如果是NULL表示一直检测不超时 。等待时常。
返回值:超时 0
失败 -1
成功 >0
放入set集合中
为了配合select函数执行,有如下宏函数:
void FD_CLR(int fd, fd_set *set);
功能:将指定的set集合中编号为fd的描述符号删除。
int FD_ISSET(int fd, fd_set *set);
功能:判断值为fd的描述符是否在set集合中,
如果在则返回真,否则返回假。
void FD_SET(int fd, fd_set *set);
功能:将指定的fd描述符,添加到set集合中。
void FD_ZERO(fd_set *set);
功能:将指定的set集合中所有描述符删除。
不包含超时的情况:
****
fgets(tmpbuff, sizeof(tmpbuff), 0); 会发生段错误
•将 0 替换为 stdin,即 fgets(tmpbuff, sizeof(tmpbuff), stdin);。
•stdin 是标准输入流,通常代表从键盘输入。
并且会把\n 回车也会被写入
#include "head.h"
int main(int argc, const char *argv[])
{int fd = 0;char tmpbuff[4096] = {0};mkfifo("myfifo",0664);fd = open("myfifo",O_WRONLY);if(-1 == fd){perror("fail to open");return -1;}while(1){fgets(tmpbuff,sizeof(tmpbuff),stdin);write(fd,tmpbuff,strlen(tmpbuff));}close(fd);return 0;
}
read.c
#include "head.h"int main(int argc, const char *argv[])
{int fd = 0;int flags = 0;char *pret = NULL;ssize_t nsize = 0;char tmpbuff[4096] = {0};fd_set rdfds;fd_set tmpfds;int ret = 0;mkfifo("myfifo",0664);fd = open("myfifo",O_RDONLY);if(-1 == fd){perror("fail to open");return -1;}FD_ZERO(&rdfds);FD_SET(fd,&rdfds);FD_SET(0,&rdfds);while(1){tmpfds = rdfds;ret = select(fd+1,&tmpfds,NULL,NULL,NULL);if(-1 == ret){perror("fail to select");return -1;}if(FD_ISSET(fd,&tmpfds)){memset(tmpbuff,0,sizeof(tmpbuff));read(fd,tmpbuff,sizeof(tmpbuff));printf("FIFO:%s",tmpbuff);}if(FD_ISSET(0,&tmpfds)){memset(tmpbuff,0,sizeof(tmpbuff));fgets(tmpbuff,sizeof(tmpbuff),stdin);printf("STDIN:%s",tmpbuff);}}close(fd);return 0;
}
4.
TCP并发模型
TCP服务器和客户端的收发计数。
头文件:
#ifndef __HEAD_H__
#define __HEAD_H__#include <sys/types.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <dirent.h>
#include <time.h>
#include <pwd.h>
#include <grp.h>
#include <pthread.h>
#include <semaphore.h>
#include <signal.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <sys/shm.h>
#include <sys/sem.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/ip.h>
#include <netinet/in.h>
#endif
客户端:
服务器:
在服务器端如何高效地处理多个TCP连接,以实现高并发性能。
为什么 TCP需要并发模型而UDP不需要?
tcp是面向连接的,其中的accept()和recv()如果未收到数据都是会阻塞。
TCP:
1. 面向连接:TCP是面向连接的协议,意味着在数据传输开始前,需要先建立连接,这通常涉及到三次握手的过程。每个TCP连接都是独立的,需要单独管理。
2. 可靠性:TCP提供了可靠的数据传输,它确保数据按顺序到达,且不会丢失。这通过序列号、确认应答、重传机制和流量控制等机制实现。
3. 拥塞控制:TCP具有拥塞控制机制,以防止网络拥塞。当网络拥堵时,TCP会减慢数据发送速率。
UDP:由于UDP的无连接和不可靠特性,通常不需要像TCP那样精细的并发模型。当一个UDP服务器需要处理多个客户端时,它可以简单地接收数据报,处理后回应,而不需要维护每个连接的状态。因此,即使在处理大量并发请求时,UDP服务器的实现通常也较为简单,可能只需要一个线程或进程来处理所有数据报。
TCP多进程、多线程模型:
多进程模型(Multi-process Model): 在这种模型中,每当一个新的连接请求到来时,服务器就会创建一个新的进程来处理这个连接。每个进程负责一个或多个连接,处理完后退出。这种方法的优点是每个连接都有独立的地址空间,错误不会互相影响,但是创建进程的开销较大,而且每个进程都需要消耗一定的系统资源,如内存和文件描述符。
多线程模型(Multi-threaded Model): 与多进程模型类似,但使用线程代替进程。当新的连接请求到达时,服务器会在现有的线程池中选择一个线程来处理这个连接。线程的切换和上下文切换开销比进程要小,因此可以支持更高的并发数。但是线程共享相同的地址空间,所以需要小心处理线程安全问题。
缺点:多进程和多线程的创建会带来资源开销,能够实现的并发量比较有限 。
逻辑控制流在时间上的重叠叫做 并发。
多线程并发:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <errno.h>
#include<pthread.h>void *th1(void *arg)
{int fd = *(int *)arg;while(1){char buf[100] = {0};read(fd,buf,sizeof(buf));printf("fifo:%s\n",buf);}return NULL;
}void *th2(void *arg)
{while(1){char buf[100] ={0};fgets(buf,sizeof(buf),stdin);printf("terminal:%s",buf);fflush(stdout);}return NULL;
}int main(int argc, const char *argv[])
{int ret = mkfifo("fifo",0666);if(-1 == ret){if(EEXIST == errno){}else{perror("mkfifo error");exit(1);}}int fd = open("fifo",O_RDONLY);if(-1 == fd){perror("open error");exit(1);}pthread_t tid1,tid2;pthread_create(&tid1,NULL,th1,&fd);pthread_create(&tid2,NULL,th2,NULL);pthread_join(tid1,NULL);pthread_join(tid2,NULL);close(fd);return 0;
}