Linux-select同时监听读写-ulimit-select的原理-select的调用流程-select设置超时;

mmap 实现的细节与 DMA 的使用是分开的。当通过 mmap 访问映射的文件或设备时,数据可能通过传统的 I/O 路径(涉及 CPU、内存和 I/O 控制器)进行传输,也可能在某些情况下使用 DMA 来优化性能。但是,这种优化是由操作系统和硬件共同决定的,而不是由 mmap 系统调用直接控制的。

DMA(Direct Memory Access),即直接存储器访问,是一种在计算机系统中允许某些硬件子系统与主存储器(RAM)进行直接数据传输的技术,而无需通过中央处理器(CPU)的干预


设置监听的步骤

  1. 定义文件描述符集合:使用fd_set类型的变量来定义需要监视的文件描述符集合。fd_set实际上是一个位数组,用于存储文件描述符的状态。
  2. 初始化文件描述符集合:使用FD_ZERO宏来清空文件描述符集合,以确保集合中不包含任何文件描述符。
  3. 添加文件描述符到集合:使用FD_SET宏将需要监视的文件描述符添加到相应的集合中(readfds、writefds或exceptfds)。
  4. 设置超时时间(可选):如果需要设置select的超时时间,可以定义一个timeval结构体并设置其值,然后将该结构体的地址传递给select函数的timeout参数。
  5. 调用select函数:将文件描述符集合和超时时间(如果有)传递给select函数进行调用。select函数会阻塞等待直到有文件描述符就绪或超时发生。
  6. 检查结果: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)是一种基于位操作的数据结构,主要用于表示一个固定大小的集合或序列中的元素状态(存在或不存在)。

  1. 基本定义
    • 位图数组本质上是一个仅包含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)寻找就绪浪费了大量时间;不管有没有就绪都要轮询监听集合;海量连接,少量就绪(专门用一个就绪集合结构来存)


  1. 用户态到内核态的传递
    • 应用程序在调用select时,会传递一个或多个文件描述符集合(如读集合rdset、写集合wrset和异常集合exset),以及一个整数nfds,表示需要检查的文件描述符的最大值加1。
    • 这些集合实际上是位图数组,用于在内核中高效地标记哪些文件描述符是应用程序关心的。
    • select系统调用被调用时,这些集合(以及nfds)会从用户态的内存空间拷贝到内核态,以便内核可以安全地访问它们。
  2. 内核态的处理
    • 内核接收到select的调用后,会开始监视指定的文件描述符集合。
    • 如果某个文件描述符(比如一个套接字)的状态发生了变化(例如,有数据可读),内核会在对应的位图中将该位置设为1。
    • nfds告诉内核需要检查哪些文件描述符(从0到nfds-1)。
  3. 就绪集合的生成
    • 当某个硬件事件(如数据到达套接字)触发时,内核会检查对应的文件描述符是否在select调用的读、写或异常集合中。
    • 如果在,内核会将对应的位图中对应的位置设为1,表示该文件描述符已经就绪。
    • 这个过程会遍历所有在select调用中指定的文件描述符(从0到nfds-1)。
  4. 内核态到用户态的返回
    • select系统调用返回时,内核会将更新后的位图数组(即就绪集合)从内核态拷贝回用户态的rdsetwrsetexset中。
    • 这些就绪集合现在包含了所有在调用select时指定的、并且现在处于就绪状态的文件描述符。
  5. 用户态的fd_isset检查
    • 应用程序可以使用fd_isset宏(或其他类似机制)来检查rdsetwrsetexset中的位,以确定哪些文件描述符现在处于就绪状态。
    • fd_isset接受一个文件描述符和一个位图数组作为参数,并返回一个值,指示该文件描述符在位图中是否已设置(即是否就绪)。

注意:上述描述是简化和概念性的,实际的内核实现可能涉及更多的细节和复杂性。此外,现代的Linux系统通常推荐使用epollio_uring等更高效的事件通知机制,而不是select

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值