Linux设备模型之tty驱动架构分析 (转载) (2012-03-08)

简介: Linux设备模型之tty驱动架构分析 (转载) 转自:http://ericxiao.cublog.cn/一:前言TTY这个名称源于电传打字节的简称,在Linux系统表示各种终端。
Linux设备模型之tty驱动架构分析 (转载)  转自:http://ericxiao.cublog.cn/

一:前言
TTY这个名称源于电传打字节的简称,在Linux系统表示各种终端。终端通常都跟硬件相对应。比如对应于输入设备的键盘鼠标,或输出设备的显示器和串口终端,也有对应于不存在设备的pty驱动。在如此众多的终端模型之中,Linux系统是怎么将它们统一建模的呢? 这就是我们今天要讨论的问题。
二:tty驱动概貌
Tty架构如下所示:
Linux设备模型之tty驱动架构分析 - 长风 - im_rain的个人主页
如上图所示,用户空间主要是通过设备文件同tty_core交互。tty_core根据用空间操作的类型再选择跟line discipline和tty_driver交互。例如设置硬件的ioctl指令就直接交给tty_driver处理。Read和write操作就会交给 line discipline处理。
Line discipline是线路规程的意思。正如它的名字一样,它表示的是这条终端”线程”的输入与输出规范设置.主要用来进行输入/输出数据的预处理。处理之后。就会将数据交给tty_driver
Tty_driver就是终端对应的驱动了。它将字符转换成终端可以理解的字串,再将其传给终端设备。
值得注意的是,这个架构没有为tty_driver提供read操作。也就是说tty_core 和line discipline都没有办法从tty_driver里直接读终端信息。这是因为tty_driver对应的hardware并不一定是输入数据和输出 数据的共同负载者。例如控制终端,输出设备是显示器。输入设备是键盘。基于这样的原理。在line discipline中有一个输入缓存区。并提供了一个名叫receive_buf()的接口函数。对应的终端设备只要调用line discipine的receiver_buf函数,将数据写入到输入缓存区就可以了。
如果一个设备同时是输入设备又是输出设备。那在设备的中断处理中调用receive_buf()将数据写入即可.

三:tty驱动接口分析
具体的tty驱动设计可以参考LDD3。这里只对它的接口实现做一个分析.tty driver的所有操作都包含在tty_driver中。内核即供了一个名叫alloc_tty_driver()来分配这个tty_driver。当然 我们也可以在自己的驱动中将它定义成一个静态的结构。对tty_driver进行一些必要的初始化之后,调用tty_register_driver() 将其注册.
alloc_tty_driver()接口代码如下所示:

点击(此处)折叠或打开

  1. struct tty_driver *alloc_tty_driver(int lines)
  2. {
  3.          struct tty_driver *driver;

  4.          driver = kzalloc(sizeof(struct tty_driver), GFP_KERNEL);
  5.          if (driver) {
  6.                    driver->magic = TTY_DRIVER_MAGIC;
  7.                    driver->num = lines;
  8.                    /* later we'll move allocation of tables here */
  9.          }
  10.          return driver;
  11. }

这个函数只有一个参数。这个参数的含义为line的个数。也即次设备号的个数。注意每个设备文件都会对应一个line.
在这个接口里为tty_driver分配内存,然后将driver->mage.driver->num初始化之后就返回了.

tty_register_driver()用来注册一个tty_driver。代码如下:

点击(此处)折叠或打开

  1. int tty_register_driver(struct tty_driver *driver)
  2. {
  3.          int error;
  4.          int i;
  5.          dev_t dev;
  6.          void **p = NULL;

  7.          //TTY_DRIVER_INSTALLED:已安装的
  8.          if (driver->flags & TTY_DRIVER_INSTALLED)
  9.                    return 0;

  10.          //TTY_DRIVER_DEVPTS_MEM:使用devpts进行动态内存映射
  11.          if (!(driver->flags & TTY_DRIVER_DEVPTS_MEM) && driver->num) {
  12.                    p = kzalloc(driver->num * 3 * sizeof(void *), GFP_KERNEL);
  13.                    if (!p)
  14.                             return -ENOMEM;
  15.          }

  16.          //注册字符设备号
  17.          //如果没有指定driver->major
  18.          if (!driver->major) {
  19.                    error = alloc_chrdev_region(&dev, driver->minor_start,
  20.                                                         driver->num, driver->name);
  21.                    if (!error) {
  22.                             driver->major = MAJOR(dev);
  23.                             driver->minor_start = MINOR(dev);
  24.                    }
  25.          } else {
  26.                    dev = MKDEV(driver->major, driver->minor_start);
  27.                    error = register_chrdev_region(dev, driver->num, driver->name);
  28.          }
  29.          if (error
  30.                    kfree(p);
  31.                   return error;
  32.          }

  33.          if (p) {
  34.                driver->ttys = (struct tty_struct **)p;
  35.                driver->termios = (struct ktermios **)(p + driver->num);
  36.                driver->termios_locked = (struct ktermios **)(p + driver->num * 2);
  37.          } else {
  38.                driver->ttys = NULL;
  39.                driver->termios = NULL;
  40.                driver->termios_locked = NULL;
  41.          }

  42.          //注册字符设备
  43.          cdev_init(&driver->cdev, &tty_fops);
  44.          driver->cdev.owner = driver->owner;
  45.          error = cdev_add(&driver->cdev, dev, driver->num);
  46.          if (error) {
  47.                    unregister_chrdev_region(dev, driver->num);
  48.                    driver->ttys = NULL;
  49.                    driver->termios = driver->termios_locked = NULL;
  50.                    kfree(p);
  51.                    return error;
  52.          }

  53.          //指定默认的put_char
  54.          if (!driver->put_char)
  55.                    driver->put_char = tty_default_put_char;

  56.          mutex_lock(&tty_mutex);
  57.          list_add(&driver->tty_drivers, &tty_drivers);
  58.          mutex_unlock(&tty_mutex);

  59.          //如果没有指定TTY_DRIVER_DYNAMIC_DEV.即动态设备管理
  60.          if (!(driver->flags & TTY_DRIVER_DYNAMIC_DEV)) {
  61.                    for (i = 0; i num; i++)
  62.                        tty_register_device(driver, i, NULL);
  63.          }
  64.          proc_tty_register_driver(driver);
  65.          return 0;
  66. }

这个函数操作比较简单。就是为tty_driver创建字符设备。然后将字符设备的操作集指定为tty_fops.并且将tty_driver挂载到 tty_drivers链表中.其实这个链表的作用跟我们之前分析的input子系统中的input_dev[ ]数组类似。都是以设备号为关键字找到对应的driver.
特别的。如果没有定义TTY_DRIVER_DYNAMIC_DEV.还会在sysfs中创建一个类设备.这样主要是为了udev管理设备.
以流程图的方式将上述操作表示如下:

Linux设备模型之tty驱动架构分析 - 长风 - im_rain的个人主页

四:设备文件的操作
设备文件的操作是本节分析的重点。它的主要操作是将各项操作对应到ldsic或者是tty_driver.

4.1:打开tty设备的操作
从注册的过程可以看到,所有的操作都会对应到tty_fops中。Open操作对应的操作接口是tty_open()。代码如下:

点击(此处)折叠或打开

  1. static int tty_open(struct inode *inode, struct file *filp)
  2. {
  3.          struct tty_struct *tty;
  4.          int noctty, retval;
  5.          struct tty_driver *driver;
  6.          int index;
  7.          dev_t device = inode->i_rdev;
  8.          unsigned short saved_flags = filp->f_flags;

  9.          nonseekable_open(inode, filp);

  10. retry_open:
  11.          //O_NOCTTY 如果路径名指向终端设备,不要把这个设备用作控制终端

  12.          //noctty:需不需要更改当前进程的控制终端
  13.          noctty = filp->f_flags & O_NOCTTY;
  14.          index = -1;
  15.          retval = 0;

  16.          mutex_lock(&tty_mutex);

  17.          //设备号(5,0)/dev/tty.表示当前进程的控制终端
  18.          if (device == MKDEV(TTYAUX_MAJOR, 0)) {
  19.                    tty = get_current_tty();
  20.                    //如果当前进程的控制终端不存在,退出
  21.                    if (!tty) {
  22.                             mutex_unlock(&tty_mutex);
  23.                             return -ENXIO;
  24.                    }

  25.                    //取得当前进程的tty_driver
  26.                    driver = tty->driver;
  27.                    index = tty->index;
  28.                    filp->f_flags |= O_NONBLOCK; /* Don't let /dev/tty block */
  29.                    /* noctty = 1; */
  30.                    goto got_driver;
  31.          }
  32. #ifdef CONFIG_VT
  33.          //设备号(4,0)./dev/tty0:表示当前的控制台
  34.          if (device == MKDEV(TTY_MAJOR, 0)) {
  35.                    extern struct tty_driver *console_driver;
  36.                    driver = console_driver;
  37.                    //fg_console: 表示当前的控制台
  38.                    index = fg_console;
  39.                    noctty = 1;
  40.                    goto got_driver;
  41.          }
  42. #endif
  43.          //设备号(5,1)./dev/console.表示外接的控制台. 通过regesit_console()
  44.          if (device == MKDEV(TTYAUX_MAJOR, 1)) {
  45.                    driver = console_device(&index);
  46.                    if (driver) {
  47.                             /* Don't let /dev/console block */
  48.                             filp->f_flags |= O_NONBLOCK;
  49.                             noctty = 1;
  50.                             goto got_driver;
  51.                    }
  52.                    mutex_unlock(&tty_mutex);
  53.                    return -ENODEV;
  54.          }

  55.          //以文件的设备号为关键字,到tty_drivers中搜索所注册的driver
  56.          driver = get_tty_driver(device, &index);
  57.          if (!driver) {
  58.                    mutex_unlock(&tty_mutex);
  59.                    return -ENODEV;
  60.          }
  61. got_driver:
  62.          //index表示它的次设备号
  63.          retval = init_dev(driver, index, &tty);
  64.          mutex_unlock(&tty_mutex);
  65.          if (retval)
  66.                    return retval;

  67.          filp->private_data = tty;
  68.          file_move(filp, &tty->tty_files);
  69.          check_tty_count(tty, "tty_open");
  70.          if (tty->driver->type == TTY_DRIVER_TYPE_PTY &&
  71.              tty->driver->subtype == PTY_TYPE_MASTER)
  72.          noctty = 1;
  73. #ifdef TTY_DEBUG_HANGUP
  74.          printk(KERN_DEBUG "opening %s...", tty->name);
  75. #endif
  76.          if (!retval) {
  77.                    if (tty->driver->open)
  78.                             retval = tty->driver->open(tty, filp);
  79.                    else
  80.                             retval = -ENODEV;
  81.          }
  82.          filp->f_flags = saved_flags;

  83.          if (!retval && test_bit(TTY_EXCLUSIVE, &tty->flags) &&
  84.                                                         !capable(CAP_SYS_ADMIN))
  85.                    retval = -EBUSY;

  86.          if (retval) {
  87. #ifdef TTY_DEBUG_HANGUP
  88.                    printk(KERN_DEBUG "error %d in opening %s...", retval,
  89.                           tty->name);
  90. #endif
  91.                    release_dev(filp);
  92.                    if (retval != -ERESTARTSYS)
  93.                             return retval;
  94.                    if (signal_pending(current))
  95.                             return retval;
  96.                    schedule();
  97.                    /*
  98.                     * Need to reset f_op in case a hangup happened.
  99.                     */
  100.                    if (filp->f_op == &hung_up_tty_fops)
  101.                             filp->f_op = &tty_fops;
  102.                    goto retry_open;
  103.          }

  104.          mutex_lock(&tty_mutex);
  105.          spin_lock_irq(&current->sighand->siglock);

  106.          //设置当前进程的终端
  107.          if (!noctty &&
  108.              current->signal->leader &&
  109.              !current->signal->tty &&
  110.              tty->session == NULL)
  111.                    __proc_set_tty(current, tty);
  112.          spin_unlock_irq(&current->sighand->siglock);
  113.          mutex_unlock(&tty_mutex);
  114.          tty_audit_opening();
  115.          return 0;
  116. }

注意在这里有个容易忽略的操作:init_dev()。
Init_dev() -à initialize_tty_struct() à tty_ldisc_assign(tty, tty_ldisc_get(N_TTY));
看一下tty_ldisc_assign(tty, tty_ldisc_get(N_TTY))的操作:
Tty_ldisc_get():

点击(此处)折叠或打开

  1. struct tty_ldisc *tty_ldisc_get(int disc)
  2. {
  3.          unsigned long flags;
  4.          struct tty_ldisc *ld;

  5.          if (disc = NR_LDISCS)
  6.                    return NULL;

  7.          spin_lock_irqsave(&tty_ldisc_lock, flags);

  8.          ld = &tty_ldiscs[disc];
  9.          /* Check the entry is defined */
  10.          if (ld->flags & LDISC_FLAG_DEFINED) {
  11.                    /* If the module is being unloaded we can't use it */
  12.                    if (!try_module_get(ld->owner))
  13.                             ld = NULL;
  14.                    else /* lock it */
  15.                             ld->refcount++;
  16.          } else
  17.                    ld = NULL;
  18.          spin_unlock_irqrestore(&tty_ldisc_lock, flags);
  19.          return ld;
  20. }

这个函数的操作为到tty_ldiscs[ ]找到对应项.这个数组中的成员是调用tty_register_ldisc()将其设置进去的.

tty_ldisc_assign操作如下:

点击(此处)折叠或打开

  1. static void tty_ldisc_assign(struct tty_struct *tty, struct tty_ldisc *ld)
  2. {
  3.          tty->ldisc = *ld;
  4.          tty->ldisc.refcount = 0;
  5. }

即将取出来的idisc作为tty->ldisc字段.
在这段代码中涉及到了tty_driver,tty_struct, struct tty_ldisc.这三者之间的关系用下图表示如下:
Linux设备模型之tty驱动架构分析 - 长风 - im_rain的个人主页

在这里,为tty_struct的ldisc是默认指定为tty_ldiscs[N_TTY].该ldisc对应的是控制终端的线路规范。可以在用空间用 带TIOCSETD的ioctl调用进行更改.
将上述open用流程图的方式表示如下:
Linux设备模型之tty驱动架构分析 - 长风 - im_rain的个人主页

4.2:设备文件的write操作
设备文件的write操作对应tty_fops->write即tty_write().代码如下:

点击(此处)折叠或打开

  1. static ssize_t tty_write(struct file *file, const char __user *buf,
  2.                                                         size_t count, loff_t *ppos)
  3. {
  4.          struct tty_struct *tty;
  5.          struct inode *inode = file->f_path.dentry->d_inode;
  6.          ssize_t ret;
  7.          struct tty_ldisc *ld;

  8.          tty = (struct tty_struct *)file->private_data;
  9.          if (tty_paranoia_check(tty, inode, "tty_write"))
  10.                    return -EIO;
  11.          if (!tty || !tty->driver->write ||
  12.                    (test_bit(TTY_IO_ERROR, &tty->flags)))
  13.                             return -EIO;

  14.          ld = tty_ldisc_ref_wait(tty);
  15.          if (!ld->write)
  16.                    ret = -EIO;
  17.          else
  18.                    ret = do_tty_write(ld->write, tty, file, buf, count);
  19.          tty_ldisc_deref(ld);
  20.          return ret;
  21. }

在open的过程中,将tty_struct存放在file的私有区。在write中,从file的私有区中就可以取到要操作的tty_struct.
如果tty_driver中没有write.如果tty有错误都会有效性判断失败返回。如果一切正常,递增ldsic的引用计数。将用 do_tty_wirte()再行写操作。写完之后,再递减ldsic的引用计数.
Do_tty_write代码分段分析如下:

点击(此处)折叠或打开

  1. static inline ssize_t do_tty_write(
  2.          ssize_t (*write)(struct tty_struct *, struct file *, const unsigned char *, size_t),
  3.          struct tty_struct *tty,
  4.          struct file *file,
  5.          const char __user *buf,
  6.          size_t count)
  7. {
  8.          ssize_t ret, written = 0;
  9.          unsigned int chunk;

  10.          ret = tty_write_lock(tty, file->f_flags & O_NDELAY);
  11.          if (ret
  12.                    return ret;

  13.          /*
  14.           * We chunk up writes into a temporary buffer. This
  15.           * simplifies low-level drivers immensely, since they
  16.           * don't have locking issues and user mode accesses.
  17.           *
  18.           * But if TTY_NO_WRITE_SPLIT is set, we should use a
  19.           * big chunk-size..
  20.           *
  21.           * The default chunk-size is 2kB, because the NTTY
  22.           * layer has problems with bigger chunks. It will
  23.           * claim to be able to handle more characters than
  24.           * it actually does.
  25.           *
  26.           * FIXME: This can probably go away now except that 64K chunks
  27.           * are too likely to fail unless switched to vmalloc...
  28.           */
  29.          chunk = 2048;
  30.          if (test_bit(TTY_NO_WRITE_SPLIT, &tty->flags))
  31.                    chunk = 65536;
  32.          if (count
  33.                    chunk = count;

  34.          /* write_buf/write_cnt is protected by the atomic_write_lock mutex */
  35.          if (tty->write_cnt
  36.                    unsigned char *buf;

  37.                    if (chunk
  38.                             chunk = 1024;

  39.                    buf = kmalloc(chunk, GFP_KERNEL);
  40.                    if (!buf) {
  41.                             ret = -ENOMEM;
  42.                             goto out;
  43.                    }
  44.                    kfree(tty->write_buf);
  45.                    tty->write_cnt = chunk;
  46.                    tty->write_buf = buf;
  47.          }
  48. 默认一次写数据的大小为2K.如果设置了TTY_NO_WRITE_SPLIT.则将一次写的数据量扩大为65536.
  49. Tty->write_buf是写操作的临时缓存区。即将用户空的数据暂时存放到这里
  50. Tty->write_cnt是临时缓存区的大小。
  51. 在这里,必须要根据一次写的数据量对这个临时缓存区做调整

  52.          /* Do the write .. */
  53.          for (;;) {
  54.                    size_t size = count;
  55.                    if (size > chunk)
  56.                             size = chunk;
  57.                    ret = -EFAULT;
  58.                    if (copy_from_user(tty->write_buf, buf, size))
  59.                             break;
  60.                    lock_kernel();
  61.                    ret = write(tty, file, tty->write_buf, size);
  62.                    unlock_kernel();
  63.                    if (ret
  64.                             break;
  65.                    written += ret;
  66.                    buf += ret;
  67.                    count -= ret;
  68.                    if (!count)
  69.                             break;
  70.                    ret = -ERESTARTSYS;
  71.                    if (signal_pending(current))
  72.                             break;
  73.                    cond_resched();
  74.          }
  75.          if (written) {
  76.                    struct inode *inode = file->f_path.dentry->d_inode;
  77.                    inode->i_mtime = current_fs_time(inode->i_sb);
  78.                    ret = written;
  79.          }
  80. out:
  81.          tty_write_unlock(tty);
  82.          return ret;
  83. }

后面的操作就比较简单了。先将用户空间的数据copy到临时缓存区,然后再调用ldisc->write()完成这次写操作.最后再更新设备结点的 时间戳.
Write操作的流程图如下示:
Linux设备模型之tty驱动架构分析 - 长风 - im_rain的个人主页

在这里,我们只看到将数据写放到了ldisc->write().没有看到与tty_driver相关的部份。实际上在ldisc中对写入的数据做 预处理过后,还是会调用tty_driver->write()将其写入硬件.

4.3:设备文件的read操作

点击(此处)折叠或打开

  1. static ssize_t tty_read(struct file *file, char __user *buf, size_t count,
  2.                             loff_t *ppos)
  3. {
  4.          int i;
  5.          struct tty_struct *tty;
  6.          struct inode *inode;
  7.          struct tty_ldisc *ld;

  8.          tty = (struct tty_struct *)file->private_data;
  9.          inode = file->f_path.dentry->d_inode;
  10.          if (tty_paranoia_check(tty, inode, "tty_read"))
  11.                    return -EIO;
  12.          if (!tty || (test_bit(TTY_IO_ERROR, &tty->flags)))
  13.                    return -EIO;

  14.          /* We want to wait for the line discipline to sort out in this
  15.             situation */
  16.          ld = tty_ldisc_ref_wait(tty);
  17.          lock_kernel();
  18.          if (ld->read)
  19.                    i = (ld->read)(tty, file, buf, count);
  20.          else
  21.                    i = -EIO;
  22.          tty_ldisc_deref(ld);
  23.          unlock_kernel();
  24.          if (i > 0)
  25.                    inode->i_atime = current_fs_time(inode->i_sb);
  26.          return i;
  27. }

这个read操作就更简单。直接调用ldsic->read()完成工作
流程图如下:
Linux设备模型之tty驱动架构分析 - 长风 - im_rain的个人主页

五:小结
在tty设备文件的操作中。Open操作会进行一系统初始化。然后调用ldsic->open tty_driver->open。在write和read调用中只tty_core只会用到 ldisc->wirte/ldisc->read。除了上面分析的几个操作之外,还有一个ioctl操作,以及它封装的几个 termios。这些ioctl类的操作会直接和tty_driver相关联。
相关文章
|
2月前
|
存储 分布式计算 API
大数据-107 Flink 基本概述 适用场景 框架特点 核心组成 生态发展 处理模型 组件架构
大数据-107 Flink 基本概述 适用场景 框架特点 核心组成 生态发展 处理模型 组件架构
95 0
|
4月前
|
Ubuntu Linux
查看Linux系统架构的命令,查看linux系统是哪种架构:AMD、ARM、x86、x86_64、pcc 或 查看Ubuntu的版本号
查看Linux系统架构的命令,查看linux系统是哪种架构:AMD、ARM、x86、x86_64、pcc 或 查看Ubuntu的版本号
948 3
|
4月前
|
缓存 安全 Linux
Linux 五种IO模型
Linux 五种IO模型
|
29天前
|
机器学习/深度学习 自然语言处理 C++
TSMamba:基于Mamba架构的高效时间序列预测基础模型
TSMamba通过其创新的架构设计和训练策略,成功解决了传统时间序列预测模型面临的多个关键问题。
84 4
TSMamba:基于Mamba架构的高效时间序列预测基础模型
|
25天前
|
缓存 运维 网络协议
深入Linux内核架构:操作系统的核心奥秘
深入Linux内核架构:操作系统的核心奥秘
44 2
|
2月前
|
机器学习/深度学习 网络架构 计算机视觉
目标检测笔记(一):不同模型的网络架构介绍和代码
这篇文章介绍了ShuffleNetV2网络架构及其代码实现,包括模型结构、代码细节和不同版本的模型。ShuffleNetV2是一个高效的卷积神经网络,适用于深度学习中的目标检测任务。
87 1
目标检测笔记(一):不同模型的网络架构介绍和代码
|
3月前
|
机器学习/深度学习
ACM MM24:复旦提出首个基于扩散模型的视频非限制性对抗攻击框架,主流CNN和ViT架构都防不住它
【9月更文挑战第23天】复旦大学研究团队提出了ReToMe-VA,一种基于扩散模型的视频非限制性对抗攻击框架,通过时间步长对抗性潜在优化(TALO)与递归令牌合并(ReToMe)策略,实现了高转移性且难以察觉的对抗性视频生成。TALO优化去噪步骤扰动,提升空间难以察觉性及计算效率;ReToMe则确保时间一致性,增强帧间交互。实验表明,ReToMe-VA在攻击转移性上超越现有方法,但面临计算成本高、实时应用受限及隐私安全等挑战。[论文链接](http://arxiv.org/abs/2408.05479)
82 3
|
2月前
|
机器学习/深度学习 人工智能 自然语言处理
【AI大模型】BERT模型:揭秘LLM主要类别架构(上)
【AI大模型】BERT模型:揭秘LLM主要类别架构(上)
|
3月前
|
机器学习/深度学习 测试技术 数据处理
KAN专家混合模型在高性能时间序列预测中的应用:RMoK模型架构探析与Python代码实验
Kolmogorov-Arnold网络(KAN)作为一种多层感知器(MLP)的替代方案,为深度学习领域带来新可能。尽管初期测试显示KAN在时间序列预测中的表现不佳,近期提出的可逆KAN混合模型(RMoK)显著提升了其性能。RMoK结合了Wav-KAN、JacobiKAN和TaylorKAN等多种专家层,通过门控网络动态选择最适合的专家层,从而灵活应对各种时间序列模式。实验结果显示,RMoK在多个数据集上表现出色,尤其是在长期预测任务中。未来研究将进一步探索RMoK在不同领域的应用潜力及其与其他先进技术的结合。
103 4
|
3月前
|
分布式计算 负载均衡 监控
p2p网络架构模型
P2P(Peer-to-Peer)模式是一种网络架构模型,在这种模型中,每个节点(peer)既是服务的提供者也是服务的消费者。这意味着每个参与的节点都可以直接与其他节点通信,并且可以相互提供资源和服务,例如文件共享、流媒体传输等。
91 6