如何在Linux上找到所有串行设备(ttyS、ttyUSB等)而不打开它们?

133

在Linux系统上,获取所有可用串口/设备列表的正确方法是什么?

换句话说,当我遍历/dev/中的所有设备时,如何判断哪些是经典意义上的串口,即通常支持波特率和 RTS / CTS 流控制的串口?

解决方案将使用C进行编码。

我提问是因为我正在使用一个明显错误的第三方库:它似乎只迭代/dev/ttyS * 。问题在于,例如通过USB提供的串口(由USB-RS232适配器提供),这些串口列在/dev/ttyUSB * 下。阅读Linux.org上的Serial-HOWTO,我得到了这样一个想法,即随着时间的推移,还会有其他名称空间。

因此,我需要找到检测串行设备的官方方法。问题在于没有正式记录,或者我找不到它。

我想象一种方式是打开来自 /dev/tty * 的所有文件,并在其中调用特定的 ioctl(),该函数仅在串行设备上可用。但是,那是一个好的解决方案吗?

更新

hrickards 建议查看“setserial”的源代码。 它的代码正是我想要的:

首先,它使用以下命令打开设备:

fd = open (path, O_RDWR | O_NONBLOCK)

然后它调用:

ioctl (fd, TIOCGSERIAL, &serinfo)
如果这个调用没有返回错误,那么显然它是一个串行设备。
我在 串口编程/termios 中找到了类似的代码,建议同时添加 O_NOCTTY 选项。
然而,这种方法存在一个问题:
当我在 BSD Unix(即 Mac OS X)上测试此代码时,它也能正常工作。但是,通过蓝牙提供的串行设备会导致系统(驱动程序)尝试连接到蓝牙设备,在超时错误返回之前需要一段时间。这是由于只打开设备引起的。我可以想象在Linux上也可能发生类似的情况-理想情况下,我不应该需要打开设备来确定其类型。我想知道是否有一种方法可以在不打开设备的情况下调用ioctl函数,或者以不会导致连接被建立的方式打开设备?
我该怎么办?

1
有人匿名建议了这个编辑,但被拒绝了,所以我把它留在这里作为评论:如果您在ioctl调用中使用TIOCGSERIAL标志,而不是TIOCMGET,则调用不会返回错误,即使某些错误路径不引用COM(串行)端口。使用TIOCMGET标志,ioctl仅适用于可以在TTY和TTYUSB可能路径中访问的COM端口。 - Thomas Tempelmann
15个回答

100

/sys 文件系统应该包含您所需的大量信息。我的系统(2.6.32-40-generic#87-Ubuntu)建议:

/sys/class/tty

这将为您提供系统已知的所有TTY设备的描述。以下是一个简化的示例:

# ll /sys/class/tty/ttyUSB*
lrwxrwxrwx 1 root root 0 2012-03-28 20:43 /sys/class/tty/ttyUSB0 -> ../../devices/pci0000:00/0000:00:1d.0/usb2/2-1/2-1.4/2-1.4:1.0/ttyUSB0/tty/ttyUSB0/
lrwxrwxrwx 1 root root 0 2012-03-28 20:44 /sys/class/tty/ttyUSB1 -> ../../devices/pci0000:00/0000:00:1d.0/usb2/2-1/2-1.3/2-1.3:1.0/ttyUSB1/tty/ttyUSB1/

通过以下链接之一:

# ll /sys/class/tty/ttyUSB0/
insgesamt 0
drwxr-xr-x 3 root root    0 2012-03-28 20:43 ./
drwxr-xr-x 3 root root    0 2012-03-28 20:43 ../
-r--r--r-- 1 root root 4096 2012-03-28 20:49 dev
lrwxrwxrwx 1 root root    0 2012-03-28 20:43 device -> ../../../ttyUSB0/
drwxr-xr-x 2 root root    0 2012-03-28 20:49 power/
lrwxrwxrwx 1 root root    0 2012-03-28 20:43 subsystem -> ../../../../../../../../../../class/tty/
-rw-r--r-- 1 root root 4096 2012-03-28 20:43 uevent

这里dev文件包含以下信息:

# cat /sys/class/tty/ttyUSB0/dev
188:0

这是主/次节点。可以在/dev目录中搜索它们,以获取用户友好的名称:

# ll -R /dev |grep "188, *0"
crw-rw----   1 root dialout 188,   0 2012-03-28 20:44 ttyUSB0

/sys/class/tty 目录包含所有 TTY 设备,但您可能希望排除那些讨厌的虚拟终端和伪终端。我建议您仅检查那些具有 device/driver 条目的设备:

# ll /sys/class/tty/*/device/driver
lrwxrwxrwx 1 root root 0 2012-03-28 19:07 /sys/class/tty/ttyS0/device/driver -> ../../../bus/pnp/drivers/serial/
lrwxrwxrwx 1 root root 0 2012-03-28 19:07 /sys/class/tty/ttyS1/device/driver -> ../../../bus/pnp/drivers/serial/
lrwxrwxrwx 1 root root 0 2012-03-28 19:07 /sys/class/tty/ttyS2/device/driver -> ../../../bus/platform/drivers/serial8250/
lrwxrwxrwx 1 root root 0 2012-03-28 19:07 /sys/class/tty/ttyS3/device/driver -> ../../../bus/platform/drivers/serial8250/
lrwxrwxrwx 1 root root 0 2012-03-28 20:43 /sys/class/tty/ttyUSB0/device/driver -> ../../../../../../../../bus/usb-serial/drivers/ftdi_sio/
lrwxrwxrwx 1 root root 0 2012-03-28 21:15 /sys/class/tty/ttyUSB1/device/driver -> ../../../../../../../../bus/usb-serial/drivers/ftdi_sio/

@entalpi 你会发现 /dev/zero。你真的认为这是一个串行设备吗? - A.H.
1
在/dev中搜索是无用的,因为默认情况下udev会创建/dev/DEVNAME节点,因此您已经拥有/sys/class/tty中的名称。您感兴趣的是任何指向该设备的“符号”链接。这要难得多。 - xryl669

37

最近的内核(不确定从哪个版本开始)可以列出 /dev/serial 的内容以获取系统上串口的列表。它们实际上是符号链接,指向正确的/dev/节点:

flu0@laptop:~$ ls /dev/serial/
total 0
drwxr-xr-x 2 root root 60 2011-07-20 17:12 by-id/
drwxr-xr-x 2 root root 60 2011-07-20 17:12 by-path/
flu0@laptop:~$ ls /dev/serial/by-id/
total 0
lrwxrwxrwx 1 root root 13 2011-07-20 17:12 usb-Prolific_Technology_Inc._USB-Serial_Controller-if00-port0 -> ../../ttyUSB0
flu0@laptop:~$ ls /dev/serial/by-path/
total 0
lrwxrwxrwx 1 root root 13 2011-07-20 17:12 pci-0000:00:0b.0-usb-0:3:1.0-port0 -> ../../ttyUSB0

这是一个USB串口适配器,正如你所见。请注意,当系统上不存在串口时,/dev/serial/目录不存在。希望这能帮到你 :)


5
这是udev的一个功能(具体来说,是位于/lib/udev/rules.d/??-persistent-serial.rules的配置),它在2.5中被引入。 - ergosys
4
好的建议!不幸的是,我认为这不能显示内置串口,仅适用于USB串口(在连接时由udev看到)。在Ubuntu 14中,在VMware虚拟机中(使用VM提供的ttyS0/COM1),我没有看到任何与/dev/serial有关的内容,并且udev规则(60-persistent-serial.rules)仅查看udev设备--我认为udev无法发现“内置”的ttyS*串口,因此它们将需要使用ioctl或类似方法进行测试,就像其他答案中所述。 - Reed Hedges
ls /dev/serial/ ls:无法访问'/dev/serial/':没有那个文件或目录 Slackware 14.2 current x64 - jpka
2
@jpka:如果没有串行设备可用,就会出现这种情况。我按照上面的方法做了,它起作用了。然后我从 USB 上拔掉了我的 (FTDI) 串行设备,之后它产生了你描述的错误。 - Warpspace

24

我发现

dmesg | grep tty

完成工作。


这只显示实际的串行端口。不包括USB-RS232等,正如OP所要求的。 - undefined

18

我正在编写类似下面代码的功能。它对USB设备和我们所有拥有30个且只有少数几个真正起作用的stupid serial8250-devices正常工作。

基本上,我使用了之前答案中的概念。首先枚举/sys/class/tty/中的所有tty设备。不包含/device子目录的设备将被过滤掉。/sys/class/tty/console就是这样的一个设备。然后根据驱动符号链接的目标来接受实际包含设备的设备作为有效的串口。

$ ls -al /sys/class/tty/ttyUSB0//device/driver
lrwxrwxrwx 1 root root 0 sep  6 21:28 /sys/class/tty/ttyUSB0//device/driver -> ../../../bus/platform/drivers/usbserial

并且对于 ttyS0

$ ls -al /sys/class/tty/ttyS0//device/driver
lrwxrwxrwx 1 root root 0 sep  6 21:28 /sys/class/tty/ttyS0//device/driver -> ../../../bus/platform/drivers/serial8250

所有由serial8250驱动的驱动程序必须使用先前提到的ioctl进行探测。

        if (ioctl(fd, TIOCGSERIAL, &serinfo)==0) {
            // If device type is no PORT_UNKNOWN we accept the port
            if (serinfo.type != PORT_UNKNOWN)
                the_port_is_valid

只有报告有效设备类型的端口才是有效的。

枚举串行端口的完整源代码如下。欢迎添加。

#include <stdlib.h>
#include <dirent.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
#include <termios.h>
#include <sys/ioctl.h>
#include <linux/serial.h>

#include <iostream>
#include <list>

using namespace std;

static string get_driver(const string& tty) {
    struct stat st;
    string devicedir = tty;

    // Append '/device' to the tty-path
    devicedir += "/device";

    // Stat the devicedir and handle it if it is a symlink
    if (lstat(devicedir.c_str(), &st)==0 && S_ISLNK(st.st_mode)) {
        char buffer[1024];
        memset(buffer, 0, sizeof(buffer));

        // Append '/driver' and return basename of the target
        devicedir += "/driver";

        if (readlink(devicedir.c_str(), buffer, sizeof(buffer)) > 0)
            return basename(buffer);
    }
    return "";
}

static void register_comport( list<string>& comList, list<string>& comList8250, const string& dir) {
    // Get the driver the device is using
    string driver = get_driver(dir);

    // Skip devices without a driver
    if (driver.size() > 0) {
        string devfile = string("/dev/") + basename(dir.c_str());

        // Put serial8250-devices in a seperate list
        if (driver == "serial8250") {
            comList8250.push_back(devfile);
        } else
            comList.push_back(devfile); 
    }
}

static void probe_serial8250_comports(list<string>& comList, list<string> comList8250) {
    struct serial_struct serinfo;
    list<string>::iterator it = comList8250.begin();

    // Iterate over all serial8250-devices
    while (it != comList8250.end()) {

        // Try to open the device
        int fd = open((*it).c_str(), O_RDWR | O_NONBLOCK | O_NOCTTY);

        if (fd >= 0) {
            // Get serial_info
            if (ioctl(fd, TIOCGSERIAL, &serinfo)==0) {
                // If device type is no PORT_UNKNOWN we accept the port
                if (serinfo.type != PORT_UNKNOWN)
                    comList.push_back(*it);
            }
            close(fd);
        }
        it ++;
    }
}

list<string> getComList() {
    int n;
    struct dirent **namelist;
    list<string> comList;
    list<string> comList8250;
    const char* sysdir = "/sys/class/tty/";

    // Scan through /sys/class/tty - it contains all tty-devices in the system
    n = scandir(sysdir, &namelist, NULL, NULL);
    if (n < 0)
        perror("scandir");
    else {
        while (n--) {
            if (strcmp(namelist[n]->d_name,"..") && strcmp(namelist[n]->d_name,".")) {

                // Construct full absolute file path
                string devicedir = sysdir;
                devicedir += namelist[n]->d_name;

                // Register the device
                register_comport(comList, comList8250, devicedir);
            }
            free(namelist[n]);
        }
        free(namelist);
    }

    // Only non-serial8250 has been added to comList without any further testing
    // serial8250-devices must be probe to check for validity
    probe_serial8250_comports(comList, comList8250);

    // Return the lsit of detected comports
    return comList;
}


int main() {
    list<string> l = getComList();

    list<string>::iterator it = l.begin();
    while (it != l.end()) {
        cout << *it << endl;
        it++;
    }

    return 0;   
}

孤立的链接被认为是一个差劲的答案,因为它本身是没有意义的,并且目标资源未来也不能保证存在。请尽量包含您所链接信息的摘要。 - j0k
感谢Soren为我们做了这件事,尽管我们对API有一些了解,但是你做得真的很好,再次感谢。 - ind79ra

15

我认为我在我的内核源文件文档中找到了答案: /usr/src/linux-2.6.37-rc3/Documentation/filesystems/proc.txt

1.7 TTY info in /proc/tty
-------------------------

Information about  the  available  and actually used tty's can be found in the
directory /proc/tty.You'll  find  entries  for drivers and line disciplines in
this directory, as shown in Table 1-11.


Table 1-11: Files in /proc/tty
..............................................................................
 File          Content                                        
 drivers       list of drivers and their usage                
 ldiscs        registered line disciplines                    
 driver/serial usage statistic and status of single tty lines 
..............................................................................

To see  which  tty's  are  currently in use, you can simply look into the file
/proc/tty/drivers:

  > cat /proc/tty/drivers 
  pty_slave            /dev/pts      136   0-255 pty:slave 
  pty_master           /dev/ptm      128   0-255 pty:master 
  pty_slave            /dev/ttyp       3   0-255 pty:slave 
  pty_master           /dev/pty        2   0-255 pty:master 
  serial               /dev/cua        5   64-67 serial:callout 
  serial               /dev/ttyS       4   64-67 serial 
  /dev/tty0            /dev/tty0       4       0 system:vtmaster 
  /dev/ptmx            /dev/ptmx       5       2 system 
  /dev/console         /dev/console    5       1 system:console 
  /dev/tty             /dev/tty        5       0 system:/dev/tty 
  unknown              /dev/tty        4    1-63 console 

这是一个指向该文件的链接:http://git.kernel.org/?p=linux/kernel/git/next/linux-next.git;a=blob_plain;f=Documentation/filesystems/proc.txt;hb=e8883f8057c0f7c9950fa9f20568f37bfa62f34a


是的,那似乎有效。然而,这个解决方案需要我读取一个文本文件并解析它。我想知道是否有更好的方法,即一个API可以让我以结构化二进制格式获取这些内容。 - Thomas Tempelmann

6

我看了一下代码,发现它存在一个缺陷,就是必须打开设备,这可能会导致连接尝试,这样做并不好。但也许 Linux 驱动程序在蓝牙支持方面比当前的 OSX 驱动程序更聪明,因为它们不会立即打开连接?谁知道呢? 也许我应该开始一个新问题,特别澄清这一点。如果结果证明没问题,那么我也可以接受你在这里的答案。嗯... - Thomas Tempelmann

5

使用/proc/tty/drivers只能显示已加载的tty驱动程序。如果您正在寻找串行端口列表,请查看/dev/serial,它将有两个子目录:by-id和by-path。

例如:

# find . -type l
./by-path/usb-0:1.1:1.0-port0
./by-id/usb-Prolific_Technology_Inc._USB-Serial_Controller-if00-port0

感谢这篇文章:https://superuser.com/questions/131044/how-do-i-know-which-dev-ttys-is-my-serial-port

本文介绍了如何确定Linux系统中哪个/dev/ttyS端口是串行端口。首先,使用dmesg命令查看串口信息,然后使用ls -l /sys/class/tty/命令找到相应的tty设备。最后,使用udevadm info命令来确定该设备的详细信息,包括其udev属性和驱动程序。

1
显然这取决于发行版。我在我的机器上(运行Debian)找不到/dev/serial。 - SimonC
适用于Ubuntu。 - Rick Papo
1
根据2021年5月7日发布的最新版本,raspios 32位buster(debian)在基于ARM的Raspberry Pi上是Debian Buster,但没有/dev/serial。/dev/serial/by-path和/dev/serial/by-id取决于发行版。 - always_learning

4

我这里没有串口设备来测试,但如果你有Python和DBus,你可以自己尝试。

import dbus
bus = dbus.SystemBus()
hwmanager = bus.get_object('org.freedesktop.Hal', '/org/freedesktop/Hal/Manager')
hwmanager_i = dbus.Interface(hwmanager, 'org.freedesktop.Hal.Manager')
print hwmanager_i.FindDeviceByCapability("serial")

如果失败了,您可以在hwmanager_i.GetAllDevicesWithProperties()内搜索,看看我刚才猜测的能力名称“serial”是否有不同的名称。
祝您好运!

4

通过群组 dialout 来获取每个用户为 "dialout" 的 tty:

ls -l /dev/tty* | grep 'dialout'

只获取它的文件夹:

ls -l /dev/tty* | grep 'dialout' | rev | cut -d " " -f1 | rev

轻松监听 tty 输出,例如 Arduino 串口输出:

head --lines 1 < /dev/ttyUSB0

仅监听每个 tty 的一行输出:

for i in $(ls -l /dev/tty* | grep 'dialout' | rev | cut -d " " -f1 | rev); do head --lines 1 < $i; done

我真的很喜欢通过查找驱动程序的方法:

ll /sys/class/tty/*/device/driver

您现在可以选择 tty-名称:

ls /sys/class/tty/*/device/driver | grep 'driver' | cut -d "/" -f 5

3

我的解决方案基于udev库,下面的代码是基于示例2

#include <string.h>
#include <libudev.h>

bool enumerate_serial_ports(void)
{
    struct udev* udev;
    struct udev_enumerate* enumerate;
    struct udev_list_entry* devices, *dev_list_entry;

    /* create udev object */
    udev = udev_new();
    if (!udev) 
    {
        SPDLOG_ERROR("Cannot create udev context.");
        return false;
    }

    /* create enumerate object */
    enumerate = udev_enumerate_new(udev);
    if (!enumerate) 
    {
        SPDLOG_ERROR("Cannot create enumerate context.");

        udev_unref(udev);
        return false;
    }

    udev_enumerate_add_match_subsystem(enumerate, "tty");
    udev_enumerate_scan_devices(enumerate);

    /* fillup device list */
    devices = udev_enumerate_get_list_entry(enumerate);
    if (!devices) 
    {
        SPDLOG_ERROR("Failed to get device list.");

        udev_enumerate_unref(enumerate);
        udev_unref(udev);
        return false;
    }

    udev_list_entry_foreach(dev_list_entry, devices) 
    {
        struct udev_device* dev = udev_device_new_from_syspath(udev, udev_list_entry_get_name(dev_list_entry));

        // filter out virtual ports
        if((udev_device_get_sysnum(dev) != NULL) && (strstr(udev_device_get_devpath(dev), "/devices/virtual/") == NULL))
        {
            SPDLOG_DEBUG("subsystem={}", udev_device_get_subsystem(dev));
            SPDLOG_DEBUG("syspath={}", udev_device_get_syspath(dev));
            SPDLOG_DEBUG("sysname={}", udev_device_get_sysname(dev));
            SPDLOG_DEBUG("sysnum={}", udev_device_get_sysnum(dev));
            SPDLOG_DEBUG("devnode={}", udev_device_get_devnode(dev));
            SPDLOG_DEBUG("-----------------------------------------");
        }    
        
        /* free dev */
        udev_device_unref(dev);
    }

    /* free enumerate */
    udev_enumerate_unref(enumerate);
    /* free udev */
    udev_unref(udev);

    return true;
}

在使用USB串口转接器的树莓派4上的输出如下:
[ debug ][11:50:47.645] - subsystem=tty
[ debug ][11:50:47.645] - syspath=/sys/devices/platform/scb/fd500000.pcie/pci0000:00/0000:00:00.0/0000:01:00.0/usb1/1-1/1-1.3/1-1.3:1.0/ttyUSB0/tty/ttyUSB0
[ debug ][11:50:47.645] - sysname=ttyUSB0
[ debug ][11:50:47.645] - sysnum=0
[ debug ][11:50:47.645] - devnode=/dev/ttyUSB0
[ debug ][11:50:47.645] - -----------------------------------------
                           
[ debug ][11:50:47.645] - subsystem=tty
[ debug ][11:50:47.645] - syspath=/sys/devices/platform/soc/fe201000.serial/tty/ttyAMA0
[ debug ][11:50:47.645] - sysname=ttyAMA0
[ debug ][11:50:47.645] - sysnum=0
[ debug ][11:50:47.645] - devnode=/dev/ttyAMA0
[ debug ][11:50:47.645] - -----------------------------------------
                           
[ debug ][11:50:47.646] - subsystem=tty
[ debug ][11:50:47.646] - syspath=/sys/devices/platform/soc/fe215040.serial/tty/ttyS0
[ debug ][11:50:47.646] - sysname=ttyS0
[ debug ][11:50:47.646] - sysnum=0
[ debug ][11:50:47.646] - devnode=/dev/ttyS0
[ debug ][11:50:47.646] - -----------------------------------------

已测试。枚举了所有在/dev/ttyS*中的串口,包括所有不需要的串口。 - Gunther
什么是未经请求的? - Mehmet Fide
抱歉我需要解释一下:我只想获取可用的物理串口列表。 在我的电脑上,它们是: /dev/ttyS0 /dev/ttyUSB0 我的解决方法是:使用termios getattr对它们进行过滤。如果失败了,我会忽略该端口。 - Gunther
1
在我的示例代码中,我已经过滤掉了所有虚拟端口。你是否包含了这部分?它应该只列出物理端口,这对我来说是成立的。 - Mehmet Fide
1
另一种过滤掉不需要的设备的方法可能是查找具有“TIOCGSERIAL”属性(例如“type”)的设备;请参阅https://www.kernel.org/doc/Documentation/ABI/testing/sysfs-tty以获取所有定义的内容。 - Sam Morris

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接