mmap
实现的细节与 DMA 的使用是分开的。当通过 mmap
访问映射的文件或设备时,数据可能通过传统的 I/O 路径(涉及 CPU、内存和 I/O 控制器)进行传输,也可能在某些情况下使用 DMA 来优化性能。但是,这种优化是由操作系统和硬件共同决定的,而不是由 mmap
系统调用直接控制的。
DMA(Direct Memory Access),即直接存储器访问,是一种在计算机系统中允许某些硬件子系统与主存储器(RAM)进行直接数据传输的技术,而无需通过中央处理器(CPU)的干预
设置监听的步骤:
- 定义文件描述符集合:使用fd_set类型的变量来定义需要监视的文件描述符集合。fd_set实际上是一个位数组,用于存储文件描述符的状态。
- 初始化文件描述符集合:使用FD_ZERO宏来清空文件描述符集合,以确保集合中不包含任何文件描述符。
- 添加文件描述符到集合:使用FD_SET宏将需要监视的文件描述符添加到相应的集合中(readfds、writefds或exceptfds)。
- 设置超时时间(可选):如果需要设置select的超时时间,可以定义一个timeval结构体并设置其值,然后将该结构体的地址传递给select函数的timeout参数。
- 调用select函数:将文件描述符集合和超时时间(如果有)传递给select函数进行调用。select函数会阻塞等待直到有文件描述符就绪或超时发生。
- 检查结果:select函数返回后,可以使用FD_ISSET宏来检查哪些文件描述符已经就绪。如果某个文件描述符在相应的集合中(readfds、writefds或exceptfds)被标记为就绪,则可以进行相应的读、写或异常处理操作。
select:
如何找到就绪的fd:
1、轮询(polling)
2、回调(callback)
我把任务的函数作为参数给工人,工人的参数为函数指针,接收一个函数。
#include <stdio.h>
// 任务函数:打扫房间
void cleanRoom() {
printf("房间正在被打扫...\n");
// ...(这里省略了具体的打扫步骤)
printf("房间打扫完毕!\n");
}
// 回调函数:通知任务发布者
void notifyManager() {
printf("工人通知管理者:房间已经打扫完毕!\n");
}
// 工人的函数:接收任务并执行,完成后调用回调函数
void worker(void (*taskFunc)(), void (*callbackFunc)()) {
// 执行任务
taskFunc();
// 任务完成后调用回调函数
callbackFunc();
}
int main() {
// 我将任务(打扫房间)和回调函数(通知管理者)交给工人
worker(cleanRoom, notifyManager);
return 0;
}
如果一方关闭了聊天,另一方陷入死循环,管道一直就绪导致的死循环
如果写端已经关闭,并且管道中的数据已经被完全读取,read
会返回0,但由于没有检查这个返回值,读端的循环不会退出,而是会继续执行并再次调用read
#include <43func.h>
int main(int argc,char *argv[]){
// ./chat1 1.pipe 2.pipe
ARGS_CHECK(argc,3);
int fdr = open(argv[1],O_RDONLY);
int fdw = open(argv[2],O_WRONLY);
puts("pipe open");
char buf[4096] = {0};
fd_set rdset;//创建监听集合
while(1){
FD_ZERO(&rdset);//清空
FD_SET(STDIN_FILENO,&rdset);//把标准输入加入监听
FD_SET(fdr,&rdset);//把管道读取加入监听
select(fdr+1,&rdset,NULL,NULL,NULL);
if(FD_ISSET(fdr,&rdset)){
puts("msg from pipe");
memset(buf,0,sizeof(buf));
int ret = read(fdr,buf,sizeof(buf));
if (ret == 0)
{
printf("the end!");
break;
}
puts(buf);
}
if(FD_ISSET(STDIN_FILENO,&rdset)){
puts("msg from stdin");
memset(buf,0,sizeof(buf));
int ret = read(STDIN_FILENO,buf,sizeof(buf));
if (ret == 0)
{
write(fdw,"over!",5);
break;
}
write(fdw,buf,strlen(buf));
}
}
}
读端设置ret返回值判断解决循环;写端设置ret输入,当按下ctrl +d时,写端关闭,管道对面读端也关闭;
ctrl + z :暂停
ctrl + d:输入一个EOF
ctrl + c:关闭进程
select设置超时timeout:
timeout:传入传出参数,每次循环开始要设置timeout;
struct timeval:
seconds:秒
microseconds:微秒
使用select返回值区分超时导致的就绪;
#include <43func.h>
int main(int argc,char *argv[]){
// ./chat1 1.pipe 2.pipe
ARGS_CHECK(argc,3);
int fdr = open(argv[1],O_RDONLY);
int fdw = open(argv[2],O_WRONLY);
puts("pipe open");
char buf[4096] = {0};
fd_set rdset;//创建监听集合
while(1){
FD_ZERO(&rdset);//清空
FD_SET(STDIN_FILENO,&rdset);//把标准输入加入监听
FD_SET(fdr,&rdset);//把管道读取加入监听
struct timeval timeout;
timeout.tv_sec = 2;//秒
timeout.tv_usec = 500000;//微秒
int sret = select(fdr+1,&rdset,NULL,NULL,&timeout);
if (sret == 0)
{
printf("\n");
puts("time out!");
continue;
}
if(FD_ISSET(fdr,&rdset)){
puts("msg from pipe");
memset(buf,0,sizeof(buf));
int ret = read(fdr,buf,sizeof(buf));
if (ret == 0)
{
printf("the end!");
break;
}
puts(buf);
}
if(FD_ISSET(STDIN_FILENO,&rdset)){
puts("msg from stdin");
memset(buf,0,sizeof(buf));
int ret = read(STDIN_FILENO,buf,sizeof(buf));
if (ret == 0)
{
write(fdw,"over!",5);
break;
}
write(fdw,buf,strlen(buf));
}
}
}
关于管道:
当write缓冲区非空时,无法继续写入;(触发写阻塞)
以非阻塞的方式打开管道一端;
open O_RDWR;
有一个进程:读写管道----避免永久阻塞----select
一个管道读写,但是写16次管道的write缓冲区满了所以管道写阻塞;
#include <43func.h>
int main(int argc,char * argv[]){
ARGS_CHECK(argc,2);
int fdr = open(argv[1],O_RDWR);
int fdw = open(argv[1],O_RDWR);
puts("pipe open");
char buf[4096];
int cnt = 0;
while(1){
printf("cnt = %d\n",cnt++);
write(fdw,buf,sizeof(buf));
}
}
用select监听读写
使用select同时监听读写:
#include <43func.h>
int main(int argc,char * argv[]){
ARGS_CHECK(argc,2);
int fdr = open(argv[1],O_RDWR);
int fdw = open(argv[1],O_RDWR);
puts("pipe open");
char buf[4096];
fd_set rdset;//定义文件描述符集合
fd_set wrset;//
int cnt = 0;
while(1){
FD_ZERO(&rdset);//清空集合
FD_SET(fdr,&rdset);//将文件描述符加入监听集合
FD_ZERO(&wrset);
FD_SET(fdw,&wrset);
select(fdw+1,&rdset,&wrset,NULL,NULL);//最大文件描述符+1,读,写,异常,超时;
if (FD_ISSET(fdr,&rdset))
{
printf("read cnt = %d\n",cnt++);
read(fdr,buf,2048);
}
if (FD_ISSET(fdw,&rdset))
{
printf("write cnt = %d\n",cnt++);
write(fdw,buf,4096);
}
sleep(1);
}
}
ulimit -a:
在 Unix 和 Linux 系统中用于显示当前 shell 会话的所有资源限制。这些限制可以包括打开的文件描述符数量、进程数量、堆栈大小等。
core file size
(blocks, -c):允许生成的核心文件(core dump)的最大大小。data seg size
(kbytes, -d):进程的数据段的最大大小。scheduling priority
(-e):进程的最大调度优先级(通常不允许更改)。file size
(blocks, -f):shell 和其子进程可以创建的文件的最大大小。pending signals
(-i):待处理的信号的最大数量。max locked memory
(kbytes, -l):进程可以锁定的物理内存的最大大小。max memory size
(kbytes, -m):进程可以使用的物理内存的最大大小。open files
(-n):每个进程可以打开的最大文件描述符数量。pipe size
(512 bytes, -p):管道的大小(以字节为单位)。POSIX message queues
(bytes, -q):POSIX 消息队列的最大字节数。real-time priority
(-r):进程的实时调度优先级(通常不允许更改)。stack size
(kbytes, -s):进程的堆栈大小。cpu time
(seconds, -t):进程可以使用的 CPU 时间(以秒为单位)。max user processes
(-u):用户可以拥有的最大进程数量。virtual memory
(kbytes, -v):进程可以使用的虚拟内存的最大大小。file locks
(-x):进程可以持有的文件锁的最大数量。
注意:这些限制可能因系统配置和用户身份而异。例如,root 用户可能能够设置更高的限制值。
- 通常,name pipe这个缓冲区的大小为4096字节(4K),也就是1页的大小。
select认为写就绪的条件为:写缓冲区为空;
select实现的原理:
fd_set结构:
# define __FDS_BITS(set) ((set)->fds_bits)
#else
__fd_mask __fds_bits[__FD_SETSIZE / __NFDBITS];
# define __FDS_BITS(set) ((set)->__fds_bits)
#endif
} fd_set;
__fds_bits该数组内有1024/8*sizeof(long)个元素,每个元素大小为long,所以该数组大小为1024/8
整个数组的大小为1024bit,1024/8bit;
位图数组(Bitmap Array)是一种基于位操作的数据结构,主要用于表示一个固定大小的集合或序列中的元素状态(存在或不存在)。
- 基本定义:
- 位图数组本质上是一个仅包含0和1的数组,其中每个元素(位)对应集合中的一个元素。
- 当集合中的某个元素存在时,位图数组中对应的位被设置为1;当元素不存在时,对应的位被设置为0。
- 位图数组通常使用位数组(bit array)来实现,即一个整数数组,但操作的是数组中的位(bit)。
- 一个位数组中的每个位可以表示一个集合中的元素状态,从而实现高效的存储和访问。
- 位图数组常用于判断某个元素是否属于某个集合,或者对多个集合做交集、并集或差集等集合运算。
- 它特别适用于处理大规模数据,尤其是当数据是不重复的简单数据时,如整数集合。
- fd_set实际上是一个位图数组,用于存储文件描述符的状态。每一位对应一个文件描述符。
- 在Linux系统中,fd_set的实现通常是一个long int类型的位图数组,数组的大小由__FD_SETSIZE和__NFDBITS决定。例如,如果__NFDBITS为64,那么数组的大小就是__FD_SETSIZE / __NFDBITS = 1024 / 64 = 16个long int。
位图:
1024位图,一个进程可以打开1024个文件,一般open file为1024;
fd_set把所有打开文件用位图管理;一共有1024个文件描述符;
调用select:
把从用户态中的栈区的rdset拷贝到内核态的监听集合中,每当硬件异常响应,
nfds;
每当有一个硬件产生响应,select在位图中检查从0到nfds-1的文件描述符是否匹配是不是该硬件;
找到就绪的文件描述符就把对应的位置标成1,把监听集合变成就绪集合,然后把就绪集合写回rdset;
用fd_isset 一个一个匹配要监听的集合;
劣势:
(1)监听和就绪连在一起
(2)文件数量为固定的
(3)每一次select都要大量拷贝,大量用户态到内核态的拷贝;
(4)寻找就绪浪费了大量时间;不管有没有就绪都要轮询监听集合;海量连接,少量就绪(专门用一个就绪集合结构来存)
- 用户态到内核态的传递:
- 应用程序在调用
select
时,会传递一个或多个文件描述符集合(如读集合rdset
、写集合wrset
和异常集合exset
),以及一个整数nfds
,表示需要检查的文件描述符的最大值加1。 - 这些集合实际上是位图数组,用于在内核中高效地标记哪些文件描述符是应用程序关心的。
- 当
select
系统调用被调用时,这些集合(以及nfds
)会从用户态的内存空间拷贝到内核态,以便内核可以安全地访问它们。
- 应用程序在调用
- 内核态的处理:
- 内核接收到
select
的调用后,会开始监视指定的文件描述符集合。 - 如果某个文件描述符(比如一个套接字)的状态发生了变化(例如,有数据可读),内核会在对应的位图中将该位置设为1。
nfds
告诉内核需要检查哪些文件描述符(从0到nfds-1
)。
- 内核接收到
- 就绪集合的生成:
- 当某个硬件事件(如数据到达套接字)触发时,内核会检查对应的文件描述符是否在
select
调用的读、写或异常集合中。 - 如果在,内核会将对应的位图中对应的位置设为1,表示该文件描述符已经就绪。
- 这个过程会遍历所有在
select
调用中指定的文件描述符(从0到nfds-1
)。
- 当某个硬件事件(如数据到达套接字)触发时,内核会检查对应的文件描述符是否在
- 内核态到用户态的返回:
- 当
select
系统调用返回时,内核会将更新后的位图数组(即就绪集合)从内核态拷贝回用户态的rdset
、wrset
和exset
中。 - 这些就绪集合现在包含了所有在调用
select
时指定的、并且现在处于就绪状态的文件描述符。
- 当
- 用户态的fd_isset检查:
- 应用程序可以使用
fd_isset
宏(或其他类似机制)来检查rdset
、wrset
和exset
中的位,以确定哪些文件描述符现在处于就绪状态。 fd_isset
接受一个文件描述符和一个位图数组作为参数,并返回一个值,指示该文件描述符在位图中是否已设置(即是否就绪)。
- 应用程序可以使用
注意:上述描述是简化和概念性的,实际的内核实现可能涉及更多的细节和复杂性。此外,现代的Linux系统通常推荐使用epoll
或io_uring
等更高效的事件通知机制,而不是select
。