[ Linux IMX RK ] UART用户层与驱动层调用关系解析 (三)|CSDN创作打卡

[ Linux IMX RK ] UART用户层与驱动层调用关系解析 (三)|CSDN创作打卡,第1张

[ Linux IMX RK ] UART用户层与驱动层调用关系解析 (三)|CSDN创作打卡 背景

系列讲解了uart-tty驱动框架以及open、write在驱动里的调用。本章研究下用户层读函数read如何从驱动中获取数据

驱动调用解析

Read和write *** 作就会交给line discipline处理。调用Open/Read/Write则调用驱动tty_open/tty_read/tty_write

// line discipline结构体      
// include/linux/tty_ldisc.h    
struct tty_ldisc {
    struct tty_ldisc_ops *ops;
    struct tty_struct *tty;
};
// drivers/tty/tty_io.c

static ssize_t tty_read(struct file *file, char __user *buf, size_t count, loff_t *ppos)
    -> struct tty_struct *tty = file_tty(file);
    -> struct tty_ldisc *ld;
    -> ld = tty_ldisc_ref_wait(tty);          // 等待ld整理出来
    -> if (ld->ops->read)
        i = iterate_tty_read(ld, tty, file, to); 
        	-> ..... size = ld->ops->read(tty, file, buf, count);    //调用到了ldisc层(线路规程)的read函数 

line discipline结构体 初始化与 *** 作函数

// drivers/tty/n_tty.c
static struct tty_ldisc_ops n_tty_ops = {
    .magic           = TTY_LDISC_MAGIC,
    .name            = "n_tty",
    .open            = n_tty_open,
    .close           = n_tty_close,
    .flush_buffer    = n_tty_flush_buffer,
    .read            = n_tty_read,
    .write           = n_tty_write,
    .ioctl           = n_tty_ioctl,
    .set_termios     = n_tty_set_termios,
    .poll            = n_tty_poll,
    .receive_buf     = n_tty_receive_buf,
    .write_wakeup    = n_tty_write_wakeup,
    .receive_buf2    = n_tty_receive_buf2,
};

void n_tty_inherit_ops(struct tty_ldisc_ops *ops)
{
    *ops = n_tty_ops;
    ops->owner = NULL;
    ops->refcount = ops->flags = 0;
}
EXPORT_SYMBOL_GPL(n_tty_inherit_ops);

调用到了ldisc层(线路规程)的read函数为n_tty_read。n_tty_read函数和n_tty_write函数直接调用下一层uart_write不一样。n_tty_read()函数是从缓冲区中读取数据的。
注:
tty_read()读不到东西会阻塞
tty_read() ----> ld->ops->read() ----> n_tty_read()
n_tty_read()中add_wait_queue(&tty->read_wait, &wait)没有数据的时候上层的read进程阻塞在此
而在串口有数据来的时候n_tty_receive_buf()—>wake_up_interruptible(&tty->read_wait),唤醒上面的read进程n_tty_read()中会继续运行,将数据拷到用户空间

n_tty_read
    ->  if (file->f_flags & O_NONBLOCK) {                   // 用于一些数据的初始化和校验。把数据拷贝到用户空间分两种情况处理:一种是标准模式,另一种是非标准模式。在标准模式下,如果没有设置O_NONBLOCK,读 *** 作只有在遇到文件结束符或者各行的字符都已编辑完毕后才返回。
            if (!mutex_trylock(&ldata->atomic_read_lock))
                return -EAGAIN;
        } else {
            if (mutex_lock_interruptible(&ldata->atomic_read_lock))
                return -ERESTARTSYS;
        }
        down_read(&tty->termios_rwsem);
    ->  minimum = time = 0;                            
        timeout = MAX_SCHEDULE_TIMEOUT;         
        if (!ldata->icanon) {
            minimum = MIN_CHAR(tty);                    // 获取termios.c_cc[VMIN]数组的值,作为本次读取 *** 作能够读取到的最大数据量;
            if (minimum) {                              // #define MIN_CHAr(tty) ((tty)->termios.c_cc[VMIN])
                time = (HZ / 10) * TIME_CHAR(tty);
            } else {
                timeout = (HZ / 10) * TIME_CHAR(tty);
                minimum = 1;                                // 设置minimum的值是1,即缓冲区中的数据量超过1个,就要唤醒读取进程;
            }
        }
        packet = tty->packet;
        tail = ldata->read_tail;
        add_wait_queue(&tty->read_wait, &wait);         // 将tty->read_wait放入队列中,有数据就唤醒函数

缓冲区里有数据就会唤醒n_tty_read函数继续运行,将缓冲区里的数据拷贝到用户空间

unsigned char __user *b = buf;                // 指针b定义时指向的是用户空间的buf缓存,用来保存读取到的数据,随着字符的读出而向后递增;(b-buf)是已经读出的字符数;
while (nr) {
    
    if (packet && tty->link->ctrl_status) {
        unsigned char cs;
        if (b != buf)
            break;
        spin_lock_irq(&tty->link->ctrl_lock);
        cs = tty->link->ctrl_status;
        tty->link->ctrl_status = 0;
        spin_unlock_irq(&tty->link->ctrl_lock);
        if (put_user(cs, b)) {
            retval = -EFAULT;
            break;
        }
        b++;
        nr--;
        break;
    }
    if (!input_available_p(tty, 0)) {                    // 检查输入缓冲区中是否有数据,通过检查原始字符的数量
        up_read(&tty->termios_rwsem);
        tty_buffer_flush_work(tty->port);
        down_read(&tty->termios_rwsem);
        if (!input_available_p(tty, 0)) {
            if (test_bit(TTY_OTHER_CLOSED, &tty->flags)) {
                retval = -EIO;
                break;
            }
            if (tty_hung_up_p(file))
                break;
            
            if (test_bit(TTY_HUPPING, &tty->flags))
                break;
            if (!timeout)
                break;
            if (tty_io_nonblock(tty, file)) {
                retval = -EAGAIN;
                break;
            }
            if (signal_pending(current)) {
                retval = -ERESTARTSYS;
                break;
            }
            up_read(&tty->termios_rwsem);
            timeout = wait_woken(&wait, TASK_INTERRUPTIBLE,
                    timeout);
            down_read(&tty->termios_rwsem);
            continue;                                                     
        }
    }
    // 如果缓冲区中没有数据可以读取,当前进程要休眠等待,直到缓冲区有数据可以读取时才会被唤醒;
    // 假定此时缓冲区中没有数据,当前进程进入休眠;之后缓冲区有数据时,当前进程被唤醒并调度运行;
    if (ldata->icanon && !L_EXTPROC(tty)) {
        // 当前进程被唤醒时,此时缓冲区中应该有可以读取的数据;
        // 在规范模式下,缓冲区中的字符是经过加工了的,要累积到一个缓冲行才会唤醒等待读出的进程(缓冲行,即碰到’n’字符);
        // 此时的读取 *** 作在canon_copy_from_read_buf()函数中完成
        retval = canon_copy_from_read_buf(tty, &b, &nr);            
        if (retval)
            break;
    } else {
        int uncopied;
        
        if (packet && b == buf) {
            if (put_user(TIOCPKT_DATA, b)) {
                retval = -EFAULT;
                break;
            }
            b++;
            nr--;
        }
        // 在非规范模式下,缓冲区中的字符是未经加工的,不存在缓冲行的概念,在原始模式可以把字符’’复制到用户空间,这里使用copy_from_read_buf()函数进行成片的拷贝;
        // 由于缓冲区是环形的,缓冲的字符可能跨越环形缓冲区的结尾,被分割成两部分,所以要使用copy_from_read_buf()函数两次;
        uncopied = copy_from_read_buf(tty, &b, &nr);
        uncopied += copy_from_read_buf(tty, &b, &nr);
        if (uncopied) {
            retval = -EFAULT;
            break;
        }
    }
    n_tty_check_unthrottle(tty);                     // 缓冲区的阀门,后面简介
    if (b - buf >= minimum)                        // 指针buf指向用户空间的缓冲区,指针b指向该缓冲区中的下一个空闲位置,(b-buf)是已经读入buf缓冲区中的字符数量;
        break;                                      // 如果(b - buf >= minimum),则本次读取结束;
    if (time)
        timeout = time;
}

在控制台的线路规程中,使用struct n_tty_data结构体表示该设备的数据;其中包含的read_buf成员作为读取的缓冲区使用;

struct n_tty_data {
    size_t read_head;
    char read_buf[N_TTY_BUF_SIZE];                    // #define N_TTY_BUF_SIZE 4096
    size_t read_tail;
}

定义的read_buf缓冲区是线性数组,但是却是作为环形缓冲区使用的;read_head成员是环形缓冲区空闲位置的开始,产生数据的进程从read_head位置开始往缓冲区写入数据;read_tail成员是环形缓冲区保存数据位置的开始,读取数据的进程从read_tail位置开始从缓冲区读取数据;

tty->read_buf[]	// 环形缓冲区;
tty->read_tail	// 指向缓冲区当前可以读取的第一个字符;
tty->read_head	// 指向缓冲区当前可以写入的第一个地址;

缓冲区的阀门

n_tty_check_unthrottle(tty);

// 缓冲区是环形的,空间也是有限的;如果缓冲区数据来的太快,应用程序来不及从缓冲区读取数据;
// 为了防止环形缓冲区中的数据被覆盖,底层的驱动程序可能因为缓冲区已满而暂时关闭了“阀门”,禁止数据继续进入缓冲区;
static void n_tty_check_unthrottle(struct tty_struct *tty)
{
    if (tty->driver->type == TTY_DRIVER_TYPE_PTY) {
        if (chars_in_buffer(tty) > TTY_THRESHOLD_UNTHROTTLE)
            return;
        n_tty_kick_worker(tty);
        tty_wakeup(tty->link);
        return;
    }
    
    while (1) {
        int unthrottled;
        tty_set_flow_change(tty, TTY_UNTHROTTLE_SAFE);
        if (chars_in_buffer(tty) > TTY_THRESHOLD_UNTHROTTLE)    // 检查缓冲区,如果缓冲区中剩余的字符数量减少到了关闭阀门的要求以下
            break;
        n_tty_kick_worker(tty);
        unthrottled = tty_unthrottle_safe(tty);                // 调用tty_unthrottle_safe()函数重新打开“阀门”,数据就可以重新进入缓冲区
        if (!unthrottled)
            break;
    }
    __tty_set_flow_change(tty, 0);
}

回到n_tty_read函数的最后一部分

if (tail != ldata->read_tail)
    n_tty_kick_worker(tty);
up_read(&tty->termios_rwsem);
remove_wait_queue(&tty->read_wait, &wait);            // 当前进程已经读取到了所要求的输入,需要放在临界区的 *** 作已完成,读取 *** 作已经完成,将当前进程从等待read_wait中移除
mutex_unlock(&ldata->atomic_read_lock);
if (b - buf)
    retval = b - buf;
return retval;                            // n_tty_read()函数以读取到的字符数量为返回值;

上述讲到通过canon_copy_from_read_buf和copy_from_read_buf将数值传到用户空间

// canon_copy_from_read_buf()函数只有在规范模式下会被调用,该函数按缓冲行将数据从tty缓冲区中读取到用户空间;通过copy_to_user函数完成数据拷贝
static int canon_copy_from_read_buf(struct tty_struct *tty, unsigned char __user **b, size_t *nr)
    -> ret = tty_copy_to_user(tty, *b, tail, n);        
    	-> size_t size = N_TTY_BUF_SIZE - tail;
    	-> void *from = read_buf_addr(ldata, tail);
     		-> return &ldata->read_buf[i & (N_TTY_BUF_SIZE - 1)];
    	->  uncopied = copy_to_user(to, from, size);
// copy_from_read_buf()函数在非规范模式下,将数据从tty缓冲区中直接读取到用户空间。
// 该函数会被调用两次,第一次是从tty->disc_data->read_tail指针指向的位置到缓冲区结尾,第二次是从缓冲区开头,到tty->disc_data->read_head指针指向的位置;
// 该函数的读取 *** 作需要在ldata->atomic_read_lock信号锁的保护下进行;
static int copy_from_read_buf(struct tty_struct *tty,
                      unsigned char __user **b,
                      size_t *nr)
{
    struct n_tty_data *ldata = tty->disc_data;
    int retval;
    size_t n;
    bool is_eof;
    size_t head = smp_load_acquire(&ldata->commit_head);
    size_t tail = ldata->read_tail & (N_TTY_BUF_SIZE - 1);
    retval = 0;
    n = min(head - ldata->read_tail, N_TTY_BUF_SIZE - tail);
    n = min(*nr, n);
    if (n) {
        unsigned char *from = read_buf_addr(ldata, tail);
        retval = copy_to_user(*b, from, n);                            // 通过copy_to_user函数完成数据拷贝
        n -= retval;
        is_eof = n == 1 && *from == EOF_CHAR(tty);
        tty_audit_add_data(tty, from, n);
        zero_buffer(tty, from, n);
        smp_store_release(&ldata->read_tail, ldata->read_tail + n);
        
        if (L_EXTPROC(tty) && ldata->icanon && is_eof &&
            (head == ldata->read_tail))
            n = 0;
        *b += n;
        *nr -= n;
    }
    return retval;
}

无论是规范还是不规范的模式下,都是从ldata->read_buf[xxx]里获取数据传到用户空间的,那么是什么时候或是什么函数将数据填入这个数组中呢?

// 调用结构体struct tty_ldisc_ops n_tty_ops 中的    .receive_buf     = n_tty_receive_buf,
n_tty_receive_buf
    -> n_tty_receive_buf_common
    	-> __receive_buf(tty, cp, fp, n);
     		-> if (!preops && !I_PARMRK(tty))
                            n_tty_receive_buf_fast(tty, cp, fp, count);
                        else
                            n_tty_receive_buf_standard(tty, cp, fp, count);
    	-> wake_up_interruptible_poll(&tty->read_wait, EPOLLIN);                 // 唤醒

以RK3568串口分析,读数据是会调用中断,通过tty_schedule_flip将数据搬至线路规程层

// 函数serial8250_init中将struct uart_8250_port *up->ops = &univ8250_driver_ops;初始化
static const struct uart_8250_ops univ8250_driver_ops = {
    .setup_irq  = univ8250_setup_irq,                    
    .release_irq    = univ8250_release_irq,
};

// 串口读数据时会调用中断
univ8250_setup_irq
    serial_link_irq_chain
        -> serial8250_interrupt                  // drivers/tty/serial/8250/8250_core.c
            -> dw8250_handle_irq             // drivers/tty/serial/8250/8250_dw.c
                -> serial8250_handle_irq    // drivers/tty/serial/8250/8250_port.c
                	-> if (!up->dma || handle_rx_dma(up, iir))   // up->dma->rx_dma(up)
            		status = serial8250_rx_chars(up, status);
    			->     do {
                                        serial8250_read_char(up, lsr);
                                        if (--max_count == 0)
                                            break;
                                        lsr = serial_in(up, UART_LSR);
                                    } while (lsr & (UART_LSR_DR | UART_LSR_BI));
                                    tty_flip_buffer_push(&port->state->port);             // 将数据插入接收数据缓冲区
                                    	-> tty_schedule_flip(port);                 // 将数据搬至线路规程层
                
// 是谁调用了univ8250_setup_irq函数呢?
// 调用struct uart_ops serial8250_pops结构体中的    .startup    = serial8250_startup,
serial8250_startup                      // drivers/tty/serial/8250/8250_port.c
    -> serial8250_do_startup
        -> retval = up->ops->setup_irq(up);

欢迎分享,转载请注明来源:内存溢出

原文地址: https://outofmemory.cn/zaji/5720511.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-12-18
下一篇 2022-12-18

发表评论

登录后才能评论

评论列表(0条)

保存