美文网首页
(printf 从用户代码到硬件)linux write系统调用

(printf 从用户代码到硬件)linux write系统调用

作者: onedam | 来源:发表于2021-04-19 22:35 被阅读0次

    https://blog.csdn.net/sbctsp/article/details/50471278 这篇文章调用流程写的比较好.
    因为c语言用结构体 有时候动态构造 函数指针. 找具体实现 有点困难.

    https://www.cnblogs.com/yikoulinux/p/14507445.html

    image.png

    linux write系统调用如何实现

    在Linux下我们在使用设备的时候,都会用到write这个函数,通过这个函数我们可以象使用文件那样向设备传送数据。可是为什么用户使用write函数就可以把数据写到设备里面去,这个过程到底是怎么实现的呢?

    这个奥秘就在于设备驱动程序的write实现中,这里我结合一些源代码来解释如何使得一个简简单单的write函数能够完成向设备里面写数据的复杂过程。

    这里的源代码主要来自两个地方。第一是oreilly出版的《Linux device driver》中的实例,第二是Linux Kernel 2.2.14核心源代码。我只列出了其中相关部分的内容,如果读者有兴趣,也可以查阅其它源代码。不过我不是在讲解如何编写设备驱动程序,所以不会对每一个细节都进行说明,再说有些地方我觉得自己还没有吃透。

    由于《Linux device driver》一书中的例子对于我们还是复杂了一些,我将其中的一个例程简化了一下。这个驱动程序支持这样一个设备:核心空间中的一个长度为10的数组kbuf[10]。我们可以通过用户程序open它,read它,write它,close它。这个设备的名字我称为short_t。

    现在言归正传。 对于一个设备,它可以在/dev下面存在一个对应的逻辑设备节点,这个节点以文件的形式存在,但它不是普通意义上的文件,它是设备文件,更确切的说,它是设备节点。这个节点是通过mknod命令建立的,其中指定了主设备号和次设备号。主设备号表明了某一类设备,一般对应着确定的驱动程序;次设备号一般是区分是标明不同属性,例如不同的使用方法,不同的位置,不同的操作。这个设备号是从/proc/devices文件中获得的,所以一般是先有驱动程序在内核中,才有设备节点在目录中。这个设备号(特指主设备号)的主要作用,就是声明设备所使用的驱动程序。驱动程序和设备号是一一对应的,当你打开一个设备文件时,操作系统就已经知道这个设备所对应的驱动程序是哪一个了。这个"知道"的过程后面就讲。

    我们再说说驱动程序的基本结构吧。这里我只介绍动态模块型驱动程序(就是我们使用insmod加载到核心中并使用rmmod卸载的那种),因为我只熟悉这种结构。模块化的驱动程序由两个函数是固定的:int init_module(void) ;void cleanup_module(void)。前者在insmod的时候执行,后者在rmmod的时候执行。 init_nodule在执行的时候,进行一些驱动程序初始化的工作,其中最主要的工作有三
    件:注册设备;申请I/O端口地址范围;申请中断IRQ。这里和我们想知道的事情相关的只
    有注册设备。
    下面是一个典型的init_module函数:

     int init_module(void){
    int result = check_region(short_base,1);/* 察看端口地址*/
    ……
    request_region(short_base,1,"short"); /* 申请端口地址*/
    ……
    result = register_chrdev(short_major, "short", &short_fops); /* 注册设备
    */
    ……
    result = request_irq(short_irq, short_interrupt, SA_INTERRUPT, "short",
    NULL); /* 申请IRQ */
    ……
    return 0;
    }/* init_module*/
    

    上面这个函数我只保留了最重要的部分,其中最重要的函数是 result = register_chrdev(short_major, "short", &short_fops);
    这是一个驱动程序的精髓所在!!当你执行indmod命令时,这个函数可以完成三件大事:第一,申请主设备号(short_major),或者指定,或者动态分配;第二,在内核中注册设备的名字("short");第三,指定fops方法(&short_fops)。其中所指定的fops方法就是我们对设备进行操作的方法(例如read,write,seek,dir,open,release等),如何实现这些方法,是编写设备驱动程序大部分工作量所在。 现在我们就要接触关键部分了--如何实现fops方法。 我们都知道,每一个文件都有一个file的结构,在这个结构中有一个file_operations的结构体,这个结构体指明了能够对该文件进行的操作。

    下面是一个典型的file_operations结构:

    struct file_operations {
    loff_t (*llseek) (struct file *, loff_t, int);
    ssize_t (*read) (struct file *, char *, size_t, loff_t *);
    ssize_t (*write) (struct file *, const char *, size_t, loff_t *);
    int (*readdir) (struct file *, void *, filldir_t);
    unsigned int (*poll) (struct file *, struct poll_table_struct *);
    int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);
    int (*mmap) (struct file *, struct vm_area_struct *);
    int (*open) (struct inode *, struct file *);
    int (*flush) (struct file *);
    int (*release) (struct inode *, struct file *);
    int (*fsync) (struct file *, struct dentry *);
    int (*fasync) (int, struct file *, int);
    int (*check_media_change) (kdev_t dev);
    int (*revalidate) (kdev_t dev);
    int (*lock) (struct file *, int, struct file_lock *);
    };
    

    我们可以看到它实际上就是许多文件操作的函数指针,其中就有write,其它的我们就不去管它了。这个write指针在实际的驱动程序中会以程序员所实现的函数名字出现,它指向程序员实现的设备write操作函数。下面就是一个实际的例子,这个write函数可以向核心内存的一个数组里输入一个字符串。

    int short_write (struct inode *inode, struct file *filp, const char *buf,
    int count){
    int retval = count;
    extern unsigned char kbuf[10];
    
    if(count>10)
    count=10;
    copy_from_user(kbuf, buf, count);
    return retval;
    }/* short_write */
    设备short_t对应的fops方法是这样声明的: struct file_operations short_fops = {
    NULL, /* short_lseek */
    short_read,
    short_write,
    NULL, /* short_readdir */
    NULL, /* short_poll */
    NULL, /* short_ioctl */
    NULL, /* short_mmap */
    short_open,
    short_release,
    NULL, /* short_fsync */
    NULL, /* short_fasync */
    /* nothing more, fill with NULLs */
    };
    

    其中NULL的项目就是不提供这个功能。所以我们可以看出short_t设备只提供了read,write,open,release功能。其中write功能我们在上面已经实现了,具体的实现函数起名为short_write。这些函数就是真正对设备进行操作的函数,这就是驱动程序的一大好处:不管你实现的时候是多么的复杂,但对用户来看,就是那些常用的文件操作函数。

    但是我们可以看到,驱动程序里的write函数有四个参数,函数格式如下:

    short_write (struct inode *inode, struct file *filp, const char *buf, int count) 而用户程序中的write函数只有三个参数,函数格式如下:

    write(inf fd, char *buf, int count)

    那他们两个是怎么联系在一起的呢?这就要靠操作系统核心中的函数sys_write了,下面
    是Linux Kernel 2.2.14中sys_write中的源代码:

    asmlinkage ssize_t sys_write(unsigned int fd, const char * buf, size_t count)
    {
    ssize_t ret;
    struct file * file;
    struct inode * inode;
    ssize_t (*write)(struct file *, const char *, size_t, loff_t *); /* 指向
    驱动程序中的wirte函数的指针*/
    
    lock_kernel();
    ret = -EBADF;
    file = fget(fd); /* 通过文件描述符得到文件指针 */
    if (!file)
    goto bad_file;
    if (!(file->f_mode & FMODE_WRITE))
    goto out;
    inode = file->f_dentry->d_inode; /* 得到inode信息 */
    ret = locks_verify_area(FLOCK_VERIFY_WRITE, inode, file, file->f_pos,
    count);
    if (ret)
    goto out;
    ret = -EINVAL;
    if (!file->f_op || !(write = file->f_op->write)) /* 将函数开始时声明的
    write函数指针指向fops方法中对应的write函数 */
    goto out;
    down(&inode->i_sem);
    ret = write(file, buf, count, &file->f_pos); /* 使用驱动程序中的write函数
    将数据输入设备,注意看,这里就是四个参数了 */
    up(&inode->i_sem);
    out:
    fput(file);
    bad_file:
    unlock_kernel();
    return ret;
    }
    

    我写了一个简单的程序来测试这个驱动程序,该程序源代码节选如下(该省的我都省了):

     main(){
    int fd,count=0;
    unsigned char buf[10];
    fd=open("/dev/short_t",O_RDWR);
    printf("input string:");
    scanf("%s",buf);
    count=strlen(buf);
    if(count>10)
    count=10;
    count=write(fd,buf,count);
    close(fd);
    return 1;
    }
    

    现在我们就演示一下用户使用write函数将数据写到设备里面这个过程到底是怎么实现的:
    1,insmod驱动程序。驱动程序申请设备名和主设备号,这些可以在/proc/devieces中获得。

    2,从/proc/devices中获得主设备号,并使用mknod命令建立设备节点文件。这是通过主
    设备号将设备节点文件和设备驱动程序联系在一起。设备节点文件中的file属性中指明了
    驱动程序中fops方法实现的函数指针。

    3,用户程序使用open打开设备节点文件,这时操作系统内核知道该驱动程序工作了,就
    调用fops方法中的open函数进行相应的工作。open方法一般返回的是文件标示符,实际
    上并不是直接对它进行操作的,而是有操作系统的系统调用在背后工作。

    4,当用户使用write函数操作设备文件时,操作系统调用sys_write函数,该函数首先通
    过文件标示符得到设备节点文件对应的inode指针和flip指针。inode指针中有设备号信
    息,能够告诉操作系统应该使用哪一个设备驱动程序,flip指针中有fops信息,可以告诉
    操作系统相应的fops方法函数在那里可以找到。

    5,然后这时sys_write才会调用驱动程序中的write方法来对设备进行写的操作。 其中1-3都是在用户空间进行的,4-5是在核心空间进行的。用户的write函数和操作系统
    的write函数通过系统调用sys_write联系在了一起。 注意:

    总的来说:设备文件通过设备号绑定了设备驱动,fops绑定了应用层的write和驱动层的write。当应用层写一个设备文件的时候,系统找到对应的设备驱动,再通过fops找到对应的驱动write函数。

    历史上,x86 的系统调用实现经历了 int / iret 到 sysenter / sysexit 再到 syscall / sysret 的演变。
    这个直接在 e8* pVideo=(e8*)0xb8000; ; 可以输出到屏幕..
    https://blog.csdn.net/EvilBinary_root/article/details/7028889

    https://437436999.github.io/2020/03/15/%E6%98%BE%E5%8D%A1%E6%96%87%E6%9C%AC%E6%A8%A1%E5%BC%8F/

     #include <unistd.h>
       ssize_t write(int fd ,const void * buf ,size_t count );
    

    在 drivers 文件夹中找到了一个 实现. syscall 调用的是编号. 这里有点断崖..
    还没找到 stdout 的. 反正这里需要一个struct 这个struct 中包含了 write 函数指针...

    static const struct fb_ops sh_mobile_lcdc_overlay_ops = {
        .owner          = THIS_MODULE,
        .fb_read        = fb_sys_read,
        .fb_write       = fb_sys_write,
        .fb_fillrect    = sys_fillrect,
        .fb_copyarea    = sys_copyarea,
        .fb_imageblit   = sys_imageblit,
        .fb_blank   = sh_mobile_lcdc_overlay_blank,
        .fb_pan_display = sh_mobile_lcdc_overlay_pan,
        .fb_ioctl       = sh_mobile_lcdc_overlay_ioctl,
        .fb_check_var   = sh_mobile_lcdc_overlay_check_var,
        .fb_set_par = sh_mobile_lcdc_overlay_set_par,
        .fb_mmap    = sh_mobile_lcdc_overlay_mmap,
    };
    
    

    https://0xax.gitbooks.io/linux-insides/content/SysCall/linux-syscall-1.html

    copy_from_user 重要的函数. write 本质就是 从用户态 数据到内核

    /dev/console 在linux 源码中. /dev/stdout 没有!!!

    https://blog.csdn.net/vertor11/article/details/104785869

    D:\linux-5.6.3内核源码win解压有点小丢失弄到vbox1了\linux-5.6.3\drivers\tty\tty_io.c
    这里定义了 "/dev/console"

    /*
     * Ok, now we can initialize the rest of the tty devices and can count
     * on memory allocations, interrupts etc..
     */
    int __init tty_init(void)
    {
        tty_sysctl_init();
        cdev_init(&tty_cdev, &tty_fops);
        if (cdev_add(&tty_cdev, MKDEV(TTYAUX_MAJOR, 0), 1) ||
            register_chrdev_region(MKDEV(TTYAUX_MAJOR, 0), 1, "/dev/tty") < 0)
            panic("Couldn't register /dev/tty driver\n");
        device_create(tty_class, NULL, MKDEV(TTYAUX_MAJOR, 0), NULL, "tty");
    
        cdev_init(&console_cdev, &console_fops);
        if (cdev_add(&console_cdev, MKDEV(TTYAUX_MAJOR, 1), 1) ||
            register_chrdev_region(MKDEV(TTYAUX_MAJOR, 1), 1, "/dev/console") < 0)
            panic("Couldn't register /dev/console driver\n");
        consdev = device_create_with_groups(tty_class, NULL,
                            MKDEV(TTYAUX_MAJOR, 1), NULL,
                            cons_dev_groups, "console");
        if (IS_ERR(consdev))
            consdev = NULL;
    
    #ifdef CONFIG_VT
        vty_init(&console_fops);
    #endif
        return 0;
    }
    
    

    终于找到 tty 写入的 指针函数...

    /**
     *  n_tty_write     -   write function for tty
     *  @tty: tty device
     *  @file: file object
     *  @buf: userspace buffer pointer
     *  @nr: size of I/O
     *
     *  Write function of the terminal device.  This is serialized with
     *  respect to other write callers but not to termios changes, reads
     *  and other such events.  Since the receive code will echo characters,
     *  thus calling driver write methods, the output_lock is used in
     *  the output processing functions called here as well as in the
     *  echo processing function to protect the column state and space
     *  left in the buffer.
     *
     *  This code must be sure never to sleep through a hangup.
     *
     *  Locking: output_lock to protect column state and space left
     *       (note that the process_output*() functions take this
     *        lock themselves)
     */
    
    static ssize_t n_tty_write(struct tty_struct *tty, struct file *file,
                   const unsigned char *buf, size_t nr)
    {
        const unsigned char *b = buf;
        DEFINE_WAIT_FUNC(wait, woken_wake_function);
        int c;
        ssize_t retval = 0;
    
        /* Job control check -- must be done at start (POSIX.1 7.1.1.4). */
        if (L_TOSTOP(tty) && file->f_op->write_iter != redirected_tty_write) {
            retval = tty_check_change(tty);
            if (retval)
                return retval;
        }
    
        down_read(&tty->termios_rwsem);
    
        /* Write out any echoed characters that are still pending */
        process_echoes(tty);
    
        add_wait_queue(&tty->write_wait, &wait);
        while (1) {
            if (signal_pending(current)) {
                retval = -ERESTARTSYS;
                break;
            }
            if (tty_hung_up_p(file) || (tty->link && !tty->link->count)) {
                retval = -EIO;
                break;
            }
            if (O_OPOST(tty)) {
                while (nr > 0) {
                    ssize_t num = process_output_block(tty, b, nr);
                    if (num < 0) {
                        if (num == -EAGAIN)
                            break;
                        retval = num;
                        goto break_out;
                    }
                    b += num;
                    nr -= num;
                    if (nr == 0)
                        break;
                    c = *b;
                    if (process_output(c, tty) < 0)
                        break;
                    b++; nr--;
                }
                if (tty->ops->flush_chars)
                    tty->ops->flush_chars(tty);
            } else {
                struct n_tty_data *ldata = tty->disc_data;
    
                while (nr > 0) {
                    mutex_lock(&ldata->output_lock);
                    c = tty->ops->write(tty, b, nr);
                    mutex_unlock(&ldata->output_lock);
                    if (c < 0) {
                        retval = c;
                        goto break_out;
                    }
                    if (!c)
                        break;
                    b += c;
                    nr -= c;
                }
            }
            if (!nr)
                break;
            if (tty_io_nonblock(tty, file)) {
                retval = -EAGAIN;
                break;
            }
            up_read(&tty->termios_rwsem);
    
            wait_woken(&wait, TASK_INTERRUPTIBLE, MAX_SCHEDULE_TIMEOUT);
    
            down_read(&tty->termios_rwsem);
        }
    break_out:
        remove_wait_queue(&tty->write_wait, &wait);
        if (nr && tty->fasync)
            set_bit(TTY_DO_WRITE_WAKEUP, &tty->flags);
        up_read(&tty->termios_rwsem);
        return (b - buf) ? b - buf : retval;
    }
    
    

    接着上一节讲。在用户程序中调用printf,会输出数据,我们知道最好肯定会进入到内核里运行,因为数据是由硬件通过串口等进行输出的,必定需要调用硬件的驱动程序。

    示例程序如下:
    test.c

    include

    int main()
    {
    int i = 1;
    printf("number is : %d !\n ,i");

    return 0;
    

    }

    我们通过 gcc -E test.i test.c 进行预编译,可以看到test.i有:extern int printf (const char *__restrict __format, ...);
    这里我们知道printf是一个外部函数,那么是谁定义的呢?
    当然是glibc。

    那么怎么知道printf属于哪个库呢?
    首先,gcc -g -o test test.c 生成test;
    然后,输入: ldd test,可以看到有下面的打印:
    linux-gate.so.1 => (0x00e5d000)
    libc.so.6 => /lib/tls/i686/cmov/libc.so.6 (0x00a01000)
    /lib/ld-linux.so.2 (0x0049d000)
    从这里可以看出需要三个库;
    接着,查看这三个库,看一下里面是否包含我们要找的函数,如: nm libc.so.6 > nm.txt

    printf在glibc的源码是:
    int
    __printf (const char *format, ...)
    {
    va_list arg;
    int done;

    va_start (arg, format);
    done = vfprintf (stdout, format, arg);
    va_end (arg);

    return done;
    }
    起作用的主要是这条语句:done = vfprintf (stdout, format, arg);
    它的源码没跟踪到,主要原理是格式化字符串,最后将字符串输出到文件中,也就是stdout中,怎么产生输出的呢?
    后来调用了系统调用write,向stdout写(即当前所在的终端),最后产生swi异常,从而陷入内核,执行sys_write。
    我们在上一篇说了一个现象:如果是在串口终端调用printf,会打印在串口终端上;在telnet终端调用printf,会打印在telnet终端上。我们在glibc库里看到的是向stdout写数据。

    这里还要先说一个概念,控制终端(/dev/tty),这是个在应用程序中的一个概念,其实就是当前终端设备的一个链接。我们可以在当前终端下输入 tty 命令查看,例如在telnet终端下输入 tty ,会输出:/dev/pts/0,它代表当前终端设备。猜想在glibc库里有一个重定位过程,把stdout对到/dev/tty,然后进行sys_write,所以每次printf的输出都在当前的控制终端上。

    我们知道在linux中sys_write其实就是:

    SYSCALL_DEFINE3(write, unsigned int, fd, const char __user *, buf,
    size_t, count)
    ret = vfs_write(file, buf, count, &pos);

    至于为什么,请参见下面的博文,里面会讲系统调用的原理和swi异常处理。

    好接着上面的vfs_write函数:
    vfs_write
    ret = file->f_op->write(file, buf, count, pos);

    那么上面的这个write是谁?

    我们去看一下tty的初始化函数:

    tty_init
        cdev_init(&console_cdev, &console_fops);
    
    
    static const struct file_operations console_fops = {
        .write = redirected_tty_write
    
    

    所以上面的那个write函数实际是 redirected_tty_write

    redirected_tty_write
        tty_write(file, buf, count, ppos);
            //看到这里的tty,它就代表我们现在运行的控制终端,从glibc库里传进来的
            struct tty_struct *tty = ((struct tty_file_private *)file->private_data)->tty;  
            do_tty_write(ld->ops->write, tty, file, buf, count);
            // 这里其实就是
            n_tty_write    //struct tty_ldisc_ops tty_ldisc_N_TTY
                ssize_t num = process_output_block(tty, b, nr);
                    i = tty->ops->write(tty, buf, i);
                    // 看到uart_register_driver函数有tty_set_operations(normal, &uart_ops);
                    // 它是设置struct tty_driver *normal;的tty_operations,所以这里的write函数就是
                    uart_write
                        uart_start(tty);
                            __uart_start(tty);
                                // 由定义看,下面的port是uart_port;我们在serial8250_isa_init_ports函数里照到它的初始化
                                // up->port.ops = &serial8250_pops;
                                struct uart_port *port = state->uart_port;
                                port->ops->start_tx(port);
                                // 所以上面的start_tx 其实就是 serial8250_pops.start_tx = 
                                serial8250_start_tx
                                    serial_out(up, UART_IER, up->ier);
    

    下面就不分析了,驱动硬件输出。我们看到printf的最后动作和printk的最后动作是一样的,都是驱动硬件输出。之所以printk只输出到串口,是因为printk的打印对象被直接定位到了控制台(这里是串口);而printf是先经过glibc处理后才调用sys_write函数,传进来的参数会告诉内核应该打印在哪里(当前控制终端)。

    这里只是分析了一下流程,要想更好的理解,请查阅“tty,控制台,虚拟终端,串口,console(控制台终端)”的概念

    使用 putchar 对端口进行write

    struct uart_port {
        spinlock_t      lock;           /* port lock */
        unsigned long       iobase;         /* in/out[bwl] */
        unsigned char __iomem   *membase;       /* read/write[bwl] */
        unsigned int        (*serial_in)(struct uart_port *, int);
        void            (*serial_out)(struct uart_port *, int, int);
        void            (*set_termios)(struct uart_port *,
                                   struct ktermios *new,
                                   struct ktermios *old);
        void            (*set_ldisc)(struct uart_port *,
                             struct ktermios *);
        unsigned int        (*get_mctrl)(struct uart_port *);
        void            (*set_mctrl)(struct uart_port *, unsigned int);
        unsigned int        (*get_divisor)(struct uart_port *,
                               unsigned int baud,
                               unsigned int *frac);
        void            (*set_divisor)(struct uart_port *,
                               unsigned int baud,
                               unsigned int quot,
                               unsigned int quot_frac);
        int         (*startup)(struct uart_port *port);
        void            (*shutdown)(struct uart_port *port);
        void            (*throttle)(struct uart_port *port);
        void            (*unthrottle)(struct uart_port *port);
        int         (*handle_irq)(struct uart_port *);
        void            (*pm)(struct uart_port *, unsigned int state,
                          unsigned int old);
        void            (*handle_break)(struct uart_port *);
        int         (*rs485_config)(struct uart_port *,
                            struct serial_rs485 *rs485);
        int         (*iso7816_config)(struct uart_port *,
                              struct serial_iso7816 *iso7816);
        unsigned int        irq;            /* irq number */
        unsigned long       irqflags;       /* irq flags  */
        unsigned int        uartclk;        /* base uart clock */
        unsigned int        fifosize;       /* tx fifo size */
        unsigned char       x_char;         /* xon/xoff char */
        unsigned char       regshift;       /* reg offset shift */
        unsigned char       iotype;         /* io access style */
        unsigned char       quirks;         /* internal quirks */
    
    #define UPIO_PORT       (SERIAL_IO_PORT)    /* 8b I/O port access */
    #define UPIO_HUB6       (SERIAL_IO_HUB6)    /* Hub6 ISA card */
    #define UPIO_MEM        (SERIAL_IO_MEM)     /* driver-specific */
    #define UPIO_MEM32      (SERIAL_IO_MEM32)   /* 32b little endian */
    #define UPIO_AU         (SERIAL_IO_AU)      /* Au1x00 and RT288x type IO */
    #define UPIO_TSI        (SERIAL_IO_TSI)     /* Tsi108/109 type IO */
    #define UPIO_MEM32BE        (SERIAL_IO_MEM32BE) /* 32b big endian */
    #define UPIO_MEM16      (SERIAL_IO_MEM16)   /* 16b little endian */
    
        /* quirks must be updated while holding port mutex */
    #define UPQ_NO_TXEN_TEST    BIT(0)
    
        unsigned int        read_status_mask;   /* driver specific */
        unsigned int        ignore_status_mask; /* driver specific */
        struct uart_state   *state;         /* pointer to parent state */
        struct uart_icount  icount;         /* statistics */
    
        struct console      *cons;          /* struct console, if any */
        /* flags must be updated while holding port mutex */
        upf_t           flags;
    
        /*
         * These flags must be equivalent to the flags defined in
         * include/uapi/linux/tty_flags.h which are the userspace definitions
         * assigned from the serial_struct flags in uart_set_info()
         * [for bit definitions in the UPF_CHANGE_MASK]
         *
         * Bits [0..UPF_LAST_USER] are userspace defined/visible/changeable
         * The remaining bits are serial-core specific and not modifiable by
         * userspace.
         */
    #define UPF_FOURPORT        ((__force upf_t) ASYNC_FOURPORT       /* 1  */ )
    #define UPF_SAK         ((__force upf_t) ASYNC_SAK            /* 2  */ )
    #define UPF_SPD_HI      ((__force upf_t) ASYNC_SPD_HI         /* 4  */ )
    #define UPF_SPD_VHI     ((__force upf_t) ASYNC_SPD_VHI        /* 5  */ )
    #define UPF_SPD_CUST        ((__force upf_t) ASYNC_SPD_CUST   /* 0x0030 */ )
    #define UPF_SPD_WARP        ((__force upf_t) ASYNC_SPD_WARP   /* 0x1010 */ )
    #define UPF_SPD_MASK        ((__force upf_t) ASYNC_SPD_MASK   /* 0x1030 */ )
    #define UPF_SKIP_TEST       ((__force upf_t) ASYNC_SKIP_TEST      /* 6  */ )
    #define UPF_AUTO_IRQ        ((__force upf_t) ASYNC_AUTO_IRQ       /* 7  */ )
    #define UPF_HARDPPS_CD      ((__force upf_t) ASYNC_HARDPPS_CD     /* 11 */ )
    #define UPF_SPD_SHI     ((__force upf_t) ASYNC_SPD_SHI        /* 12 */ )
    #define UPF_LOW_LATENCY     ((__force upf_t) ASYNC_LOW_LATENCY    /* 13 */ )
    #define UPF_BUGGY_UART      ((__force upf_t) ASYNC_BUGGY_UART     /* 14 */ )
    #define UPF_MAGIC_MULTIPLIER    ((__force upf_t) ASYNC_MAGIC_MULTIPLIER /* 16 */ )
    
    #define UPF_NO_THRE_TEST    ((__force upf_t) (1 << 19))
    /* Port has hardware-assisted h/w flow control */
    #define UPF_AUTO_CTS        ((__force upf_t) (1 << 20))
    #define UPF_AUTO_RTS        ((__force upf_t) (1 << 21))
    #define UPF_HARD_FLOW       ((__force upf_t) (UPF_AUTO_CTS | UPF_AUTO_RTS))
    /* Port has hardware-assisted s/w flow control */
    #define UPF_SOFT_FLOW       ((__force upf_t) (1 << 22))
    #define UPF_CONS_FLOW       ((__force upf_t) (1 << 23))
    #define UPF_SHARE_IRQ       ((__force upf_t) (1 << 24))
    #define UPF_EXAR_EFR        ((__force upf_t) (1 << 25))
    #define UPF_BUG_THRE        ((__force upf_t) (1 << 26))
    /* The exact UART type is known and should not be probed.  */
    #define UPF_FIXED_TYPE      ((__force upf_t) (1 << 27))
    #define UPF_BOOT_AUTOCONF   ((__force upf_t) (1 << 28))
    #define UPF_FIXED_PORT      ((__force upf_t) (1 << 29))
    #define UPF_DEAD        ((__force upf_t) (1 << 30))
    #define UPF_IOREMAP     ((__force upf_t) (1 << 31))
    
    #define __UPF_CHANGE_MASK   0x17fff
    #define UPF_CHANGE_MASK     ((__force upf_t) __UPF_CHANGE_MASK)
    #define UPF_USR_MASK        ((__force upf_t) (UPF_SPD_MASK|UPF_LOW_LATENCY))
    
    #if __UPF_CHANGE_MASK > ASYNC_FLAGS
    #error Change mask not equivalent to userspace-visible bit defines
    #endif
    
        /*
         * Must hold termios_rwsem, port mutex and port lock to change;
         * can hold any one lock to read.
         */
        upstat_t        status;
    
    #define UPSTAT_CTS_ENABLE   ((__force upstat_t) (1 << 0))
    #define UPSTAT_DCD_ENABLE   ((__force upstat_t) (1 << 1))
    #define UPSTAT_AUTORTS      ((__force upstat_t) (1 << 2))
    #define UPSTAT_AUTOCTS      ((__force upstat_t) (1 << 3))
    #define UPSTAT_AUTOXOFF     ((__force upstat_t) (1 << 4))
    #define UPSTAT_SYNC_FIFO    ((__force upstat_t) (1 << 5))
    
        int         hw_stopped;     /* sw-assisted CTS flow state */
        unsigned int        mctrl;          /* current modem ctrl settings */
        unsigned int        timeout;        /* character-based timeout */
        unsigned int        type;           /* port type */
        const struct uart_ops   *ops;
        unsigned int        custom_divisor;
        unsigned int        line;           /* port index */
        unsigned int        minor;
        resource_size_t     mapbase;        /* for ioremap */
        resource_size_t     mapsize;
        struct device       *dev;           /* parent device */
    
        unsigned long       sysrq;          /* sysrq timeout */
        unsigned int        sysrq_ch;       /* char for sysrq */
        unsigned char       has_sysrq;
    
        unsigned char       hub6;           /* this should be in the 8250 driver */
        unsigned char       suspended;
        const char      *name;          /* port name */
        struct attribute_group  *attr_group;        /* port specific attributes */
        const struct attribute_group **tty_groups;  /* all attributes (serial core use only) */
        struct serial_rs485     rs485;
        struct serial_iso7816   iso7816;
        void            *private_data;      /* generic platform data pointer */
    }
    /**
     *  uart_console_write - write a console message to a serial port
     *  @port: the port to write the message
     *  @s: array of characters
     *  @count: number of characters in string to write
     *  @putchar: function to write character to port
     */
    void uart_console_write(struct uart_port *port, const char *s,
                unsigned int count,
                void (*putchar)(struct uart_port *, int))
    {
        unsigned int i;
    
        for (i = 0; i < count; i++, s++) {
            if (*s == '\n')
                putchar(port, '\r');
            putchar(port, *s);
        }
    }
    
    
    static void arc_serial_console_putchar(struct uart_port *port, int ch)
    {
        while (!(UART_GET_STATUS(port) & TXEMPTY))
            cpu_relax();
    
        UART_SET_DATA(port, (unsigned char)ch);
    }
    
    #define __raw_writeb __raw_writeb
    static inline void __raw_writeb(u8 b, volatile void __iomem *addr)
    {
        __asm__ __volatile__(
        "   stb%U1 %0, %1   \n"
        :
        : "r" (b), "m" (*(volatile u8 __force *)addr)
        : "memory");
    }
    
    #define __raw_writew __raw_writew
    static inline void __raw_writew(u16 s, volatile void __iomem *addr)
    {
        __asm__ __volatile__(
        "   stw%U1 %0, %1   \n"
        :
        : "r" (s), "m" (*(volatile u16 __force *)addr)
        : "memory");
    
    }
    

    上面是 powerpc的 指令 写内存的. 下面是x86 使用的是mov
    CPU通过硬件设备的寄存器读写设备IO。
    对X86平台,这些寄存器位于专门的IO空间中,称为IO端口;而对于其他大多是CPU,IO寄存器是映射到普通内存中,没有划出特别的空间,因此称作IO内存。

    linux-5.6.3\arch\x86\include\asm\io.h

    #define build_mmio_read(name, size, type, reg, barrier) \
    static inline type name(const volatile void __iomem *addr) \
    { type ret; asm volatile("mov" size " %1,%0":reg (ret) \
    :"m" (*(volatile type __force *)addr) barrier); return ret; }
    
    #define build_mmio_write(name, size, type, reg, barrier) \
    static inline void name(type val, volatile void __iomem *addr) \
    { asm volatile("mov" size " %0,%1": :reg (val), \
    "m" (*(volatile type __force *)addr) barrier); }
    
    build_mmio_read(readb, "b", unsigned char, "=q", :"memory")
    build_mmio_read(readw, "w", unsigned short, "=r", :"memory")
    build_mmio_read(readl, "l", unsigned int, "=r", :"memory")
    
    build_mmio_read(__readb, "b", unsigned char, "=q", )
    build_mmio_read(__readw, "w", unsigned short, "=r", )
    build_mmio_read(__readl, "l", unsigned int, "=r", )
    
    build_mmio_write(writeb, "b", unsigned char, "q", :"memory")
    build_mmio_write(writew, "w", unsigned short, "r", :"memory")
    build_mmio_write(writel, "l", unsigned int, "r", :"memory")
    
    build_mmio_write(__writeb, "b", unsigned char, "q", )
    build_mmio_write(__writew, "w", unsigned short, "r", )
    build_mmio_write(__writel, "l", unsigned int, "r", )
    

    PowerPC 处理器 .为什么会没落? linux 也支持该处理器
    Intel通过酷睿,拉大优势,使得苹果抛弃PowerPC

    define __iormb() do { } while (0) 这是内存屏障(Memory Barrier).防止乱序出现问题

    内存屏障、编译屏障:
    现代 CPU中指令的执行次序不一定按顺序执行,没有相关性的指令可以打乱次序执行,以充分利用 CPU的指令流水线,提高执行速度。同时,编译器也会对指令进行优化,例如,调整指令顺序来利用CPU的指令流水线。这些优化方式,大部分时候都工作良好,但是在一些比较复杂的情况可能会出现错误,例如,执行同步代码时就有可能因为优化导致同步原语之后的指令在同步原语前执行。

    内存屏障和编译屏障就是用来告诉CPU和编译器停止优化的手段。编译屏障是指使用伪指令“memory”告诉编译器不能把“memory”执行前后的代码混淆在一起,这时“memory”起到了一种优化屏障的作用。内存屏障是在代码中使用一些特殊指令,如ARM中的dmb、dsb和isb指令,x86中的sfence、lfence和mfence指令。CPU遇到这些特殊指令后,要等待前面的指令执行完成才执行后面的指令。这些指令的作用就好像一道屏障把前后指令隔离开了,防止CPU把前后两段指令颠倒执行。
    假设我们重新编写代码,在e=d[4095]与b=a、c=a之间加上编译屏障:

    define barrier() asm volatile("": : :"memory")

    比如下面一段代码,写端申请一个新 的struct foo结构体并初始化其中的a、b、c,之后把结构体地址赋值给全局gp指针:
    struct foo {
    int a;
    int b;
    int c;
    };
    struct foo gp = NULL;
    /
    . . . */

    p = kmalloc(sizeof(*p), GFP_KERNEL);
    p->a = 1;
    p->b = 2;
    p->c = 3;
    gp = p;
    而读端如果简单做如下处理,则程序的运行可能是不符合预期的
    p = gp;
    if (p != NULL) {
    do_something_with(p->a, p->b, p->c);
    }
    有两种可能的原因会造成程序出错,一种可能性是编译乱序,另外一种可能性是执行乱序
    一、编译乱序
    关于编译方面,C语言顺序的“p->a=1;p->b=2;p->c=3;gp=p; ”的编译结果的指令顺序可能是gp的赋值指令发生在a、b、c的赋值之前。现代的高性能编译器在目标码优化上都具备对指令进行乱序优化的能力。编译器可以对访存的指令进行乱序,减少逻辑上不必要的访存,以及尽量提高Cache命中率和CPU 的Load/Store单元的工作效率。因此在打开编译器优化以后,看到生成的汇编码并没有严格按照代码的逻辑顺序,这是正常的
    使用barrier(屏障)解决编译乱序
    解决编译乱序问题,需要通过barrier()编译屏障进行。我们可以在代码中设置barrier()屏障,这个屏障可以阻挡编译器的优化。对于编译器来说,设置编译屏障可以保证屏障前的语句和屏障后的语句不乱“串门”
    演示案例:

    比如,下面的一段代码在e=d[4095]与b=a、c=a之间没有编译屏障:
    int main(int argc, char *argv[])
    {
    int a = 0, b, c, d[4096], e;
    e = d[4095];
    b = a;
    c = a;
    printf("a:%d b:%d c:%d e:%d\n", a, b, c, e);
    return 0;
    }
    用“arm-linux-gnueabihf-gcc-O2”优化编译,反汇编结果是:
    显然,尽管源代码级别b=a、c=a发生在e=d[4095]之后,但是目标代码的b=a、c=a指令发生在 e=d[4095]之前
    int main(int argc, char *argv[])
    {
    831c: b530 push {r4, r5, lr}
    831e: f5ad 4d80 sub.w sp, sp, #16384 ; 0x4000
    8322: b083 sub sp, #12
    8324: 2100 movs r1, #0
    8326: f50d 4580 add.w r5, sp, #16384 ; 0x4000
    832a: f248 4018 movw r0, #33816 ; 0x8418
    832e: 3504 adds r5, #4
    8330: 460a mov r2, r1 -> b= a;
    8332: 460b mov r3, r1 -> c= a;
    8334: f2c0 0000 movt r0, #0
    8338: 682c ldr r4, [r5, #0]
    833a: 9400 str r4, [sp, #0] -> e = d[4095];
    833c: f7ff efd4 blx 82e8 <_init+0x20>
    }
    假设我们重新编写代码,在e=d[4095]与b=a、c=a之间加上编译屏障:

    define barrier() asm volatile("": : :"memory")

    int main(int argc, char *argv[])
    {
    int a = 0, b, c, d[4096], e;
    e = d[4095];
    barrier();
    b = a;
    c = a;
    printf("a:%d b:%d c:%d e:%d\n", a, b, c, e);
    return 0;
    }
    再次用“arm-linux-gnueabihf-gcc-O2”优化编译,反汇编结果是:
    因为“asm____volatile("" ::: "memory")”这个编译屏障的存在,原来的3条指令的顺序“拨乱 反正”了。
    int main(int argc, char *argv[])
    {
    831c: b510 push {r4, lr}
    831e: f5ad 4d80 sub.w sp, sp, #16384 ; 0x4000
    8322: b082 sub sp, #8
    8324: f50d 4380 add.w r3, sp, #16384 ; 0x4000
    8328: 3304 adds r3, #4
    832a: 681c ldr r4, [r3, #0]
    832c: 2100 movs r1, #0
    832e: f248 4018 movw r0, #33816 ; 0x8418
    8332: f2c0 0000 movt r0, #0
    8336: 9400 str r4, [sp, #0] -> e = d[4095];
    8338: 460a mov r2, r1 -> b= a;
    833a: 460b mov r3, r1 -> c= a;
    833c: f7ff efd4 blx 82e8 <_init+0x20>
    }
    volatile关键字解决编译乱序的问题
    关于解决编译乱序的问题,C语言volatile关键字的作用较弱,它更多的只是避免内存访问行为的合 并,对C编译器而言,volatile是暗示除了当前的执行线索以外,其他的执行线索也可能改变某内存,所以它的含义是“易变的”
    例如:如果线程A读取var这个内存中的变量两次而没有修改var,编译器 可能觉得读一次就行了,第2次直接取第1次的结果。但是如果加了volatile关键字来形容var,则就是告诉编译器线程B、线程C或者其他执行实体可能把var改掉了,因此编译器就不会再把线程A代码的第2次内存 读取优化掉了
    总之,Linux内核明显不太喜欢volatile,这可参考内核源代码下的文档Documentation/volatile-considered-harmful.txt
    二、执行乱序
    编译乱序是编译器的行为,而执行乱序则是处理器运行时的行为
    执行乱序是指即便编译的二进制指令的顺序按照“p->a=1;p->b=2;p->c=3;gp=p; ”排放,在处理器上执行时,后发射的指令还是可能先执行完,这是处理器的“乱序执行(Out-of-Order Execution)”策略
    高级的CPU可以根据自己缓存的组织特性,将访存指令重新排序执行。连续地址的访问可能会先执行,因为这样缓存命中率高。有的还允许访存 的非阻塞,即如果前面一条访存指令因为缓存不命中,造成长延时的存储访问时,后面的访存指令可以先 执行,以便从缓存中取数。因此,即使是从汇编上看顺序正确的指令,其执行的顺序也是不可预知的
    演示案例
    举个例子,ARM v6/v7的处理器会对以下指令顺序进行优化
    LDR r0, [r1] ;
    STR r2, [r3] ;
    假设第一条LDR指令导致缓存未命中,这样缓存就会填充行,并需要较多的时钟周期才能完成。老的ARM处理器,比如ARM926EJ-S会等待这个动作完成,再执行下一条STR指令。而ARM v6/v7处理器会识 别出下一条指令(STR)且不需要等待第一条指令(LDR)完成(并不依赖于r0的值),即会先执行STR 指令,而不是等待LDR指令完成
    SMP处理器的的执行乱序
    对于大多数体系结构而言,尽管每个CPU都是乱序执行,但是这一乱序对于单核的程序执行是不可见的,因为单个CPU在碰到依赖点(后面的指令依赖于前面指令的执行结果)的时候会等待,所以程序员可能感觉不到这个乱序过程。但是这个依赖点等待的过程,在SMP处理器里面对于其他核是不可见的
    演示案例:

    比如,若在CPU0上执行:
    while (f == 0);
    print x;
    CPU1上执行:
    x = 42;
    f = 1;
    我们不能武断地认为CPU0上打印的x一定等于42,因为CPU1上即便“f=1”编译在“x=42”后面,执行时 仍然可能先于“x=42”完成,所以这个时候CPU0上打印的x不一定就是42
    内存屏障的指令
    处理器为了解决多核间一个核的内存行为对另外一个核可见的问题,引入了一些内存屏障的指令
    譬如,ARM处理器的屏障指令包括:
    DMB(数据内存屏障):在DMB之后的显式内存访问执行前,保证所有在DMB指令之前的内存访问 完成
    DSB(数据同步屏障):等待所有在DSB指令之前的指令完成(位于此指令前的所有显式内存访问均 完成,位于此指令前的所有缓存、跳转预测和TLB维护操作全部完成)
    ISB(指令同步屏障):Flush流水线,使得所有ISB之后执行的指令都是从缓存或内存中获得的
    Linux内核的自旋锁、互斥体等互斥逻辑,需要用到上述指令:在请求获得锁时,调用屏障指令;在 解锁时,也需要调用屏障指令。下面代码清单的汇编代码描绘了一个简单的互斥逻辑,留意其中的第14行 和22行

    前面提到每个CPU都是乱序执行,但是单个CPU在碰到依赖点的时候会等待,所以执行乱序对单核不 一定可见。但是,当程序在访问外设的寄存器时,这些寄存器的访问顺序在CPU的逻辑上构不成依赖关系,但是从外设的逻辑角度来讲,可能需要固定的寄存器读写顺序,这个时候,也需要使用CPU的内存屏障指令。内核文档Documentation/memory-barriers.txt和Documentation/io_ordering.txt对此进行了描述
    屏障API
    在Linux内核中,定义了:
    读写屏障mb()、读屏障rmb()、写屏障wmb()以及作用于寄存器读写的__iormb()、__iowmb()这样的屏障API
    读写寄存器的readl_relaxed()和readl()、 writel_relaxed()和writel()API的区别就体现在有无屏障方面

    define readb(c) ({ u8 __v = readb_relaxed(c); __iormb(); __v; })

    define readw(c) ({ u16 __v = readw_relaxed(c); __iormb(); __v; })

    define readl(c) ({ u32 __v = readl_relaxed(c); __iormb(); __v; })

    define writeb(v,c) ({ __iowmb(); writeb_relaxed(v,c); })

    define writew(v,c) ({ __iowmb(); writew_relaxed(v,c); })

    define writel(v,c) ({ __iowmb(); writel_relaxed(v,c); })

    比如我们通过writel_relaxed()写完DMA的开始地址、结束地址、大小之后,我们一定要调用 writel()来启动DMA
    writel_relaxed(DMA_SRC_REG, src_addr);
    writel_relaxed(DMA_DST_REG, dst_addr);
    writel_relaxed(DMA_SIZE_REG, size);

    相关文章

      网友评论

          本文标题:(printf 从用户代码到硬件)linux write系统调用

          本文链接:https://www.haomeiwen.com/subject/lwpilltx.html