嵌入式Linux C应用编程——输入设备

输入设备

常见的输入设备有鼠标、键盘、触摸屏、遥控器、电脑画图板等,用户通过输入设备与系统进行交互。

input 子系统

Linux 系统为了统一管理这些输入设备,实现了一套能够兼容所有输入设备的框架,那么这个框架就是 input 子系统。驱动开发人员基于 input 子系统开发输入设备的驱动程序,input 子系统可以屏蔽硬件的差异,向应用层提供一套统一的接口。

基于 input 子系统注册成功的输入设备,都会在 /dev/input 目录下生成对应的设备节点(设备文件),设备节点名称通常为 eventX(X 表示一个数字编号 0、1、2、3 等),譬如 /dev/input/event0/dev/input/event1/dev/input/event2 等,通过读取这些设备节点可以获取输入设备上报的数据。

读取数据的流程

如果我们要读取触摸屏的数据,假设触摸屏设备对应的设备节点为 /dev/input/event0,那么数据读取流程如下:

  1. 应用程序打开 /dev/input/event0 设备文件;
  2. 应用程序发起读操作(譬如调用 read),如果没有数据可读则会进入休眠(阻塞 I/O 情况下);
  3. 当有数据可读时,应用程序会被唤醒,读操作获取到数据返回;
  4. 应用程序对读取到的数据进行解析。

当无数据可读时,程序会进入休眠状态(也就是阻塞),譬如应用程序读触摸屏数据,如果当前并没有去触碰触摸屏,自然是无数据可读;当我们用手指触摸触摸屏或者在屏上滑动时,此时就会产生触摸数据,应用程序就有数据可读了,应用程序会被唤醒,成功读取到数据。

读取的数据类型

每一次 read 操作获取的都是一个 struct input_event 结构体类型数据,该结构体定义在 <linux/input.h> 头文件中,它的定义如下:

c 复制代码
struct input_event {
    struct timeval time;    /* 时间戳 */
    __u16 type;             /* 事件类型 */
    __u16 code;             /* 具体事件编码 */
    __s32 value;            /* 事件数值 */
};
  • time :时间戳,记录了事件发生的时间,类型为 struct timeval(包含 tv_sec 秒和 tv_usec 微秒)。
  • type:用于描述发生了哪一种类型的事件(对事件的分类),Linux 系统所支持的常见的输入事件类型如下所示:
c 复制代码
#define EV_SYN     0x00    // 同步类事件
#define EV_KEY     0x01    // 按键类事件
#define EV_REL     0x02    // 相对位移类事件(譬如鼠标)
#define EV_ABS     0x03    // 绝对位移类事件(譬如触摸屏)
#define EV_MSC     0x04    // 其它杂类事件

一种输入设备通常可以产生多种不同类型的事件,譬如点击鼠标按键(左键、右键,或鼠标上的其它按键)时会上报按键类事件,移动鼠标时则会上报相对位移类事件。

  • code :表示该类事件中的哪一个具体事件。以上列举的每一种事件类型中都包含了一系列具体事件,譬如一个键盘上通常有很多按键,譬如字母 A、B、C、D 或者数字 1、2、3、4 等,而 code 变量则告知应用程序是哪一个按键发生了输入事件。每一种事件类型都包含多种不同的事件,例如按键类事件:
c 复制代码
#define KEY_RESERVED    0
#define KEY_ESC         1   // ESC 键
#define KEY_1           2   // 数字 1 键
#define KEY_2           3   // 数字 2 键

相对位移事件:

c 复制代码
#define REL_X       0x00    // X 轴
#define REL_Y       0x01    // Y 轴
#define REL_Z       0x02    // Z 轴

绝对位移事件(触摸屏设备是一种绝对位移设备):

c 复制代码
#define ABS_X       0x00    // X 轴
#define ABS_Y       0x01    // Y 轴
#define ABS_Z       0x02    // Z 轴
  • value :内核每次上报事件都会向应用层发送一个数据 value,对 value 值的解释随着 code 的变化而变化。譬如对于按键事件(type = 1)来说,如果 code = 2(键盘上的数字键 1,也就是 KEY_1),那么如果 value 等于 1,则表示 KEY_1按下value 等于 0 表示 KEY_1松开 ;如果 value 等于 2,则表示 KEY_1长按(或自动重复)。

数据同步

上面我们提到了同步事件类型 EV_SYN,同步事件用于实现同步操作,告知接收者本轮上报的数据已经完整。应用程序读取输入设备上报的数据时,一次 read 操作只能读取一个 struct input_event 类型数据,譬如对于触摸屏来说,一个触摸点的信息包含了 X 坐标、Y 坐标以及其它信息,对于这种情况,应用程序需要执行多次 read 操作才能把一个触摸点的信息全部读取出来,这样才能得到触摸点的完整信息。

通过同步事件,内核将本轮需要上报、发送给接收者的数据全部上报完毕后,接着会上报一个同步事件,以告知应用程序本轮数据已经完整、可以进行同步了。

同步类事件中也包含了多种不同的事件,如下所示:

c 复制代码
#define SYN_REPORT      0
#define SYN_CONFIG      1
#define SYN_MT_REPORT   2
#define SYN_DROPPED     3
#define SYN_MAX         0xf
#define SYN_CNT         (SYN_MAX + 1)

所有的输入设备都需要上报同步事件,上报的同步事件通常是 SYN_REPORT,而 value 值通常为 0。

读取数据示例

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <linux/input.h>

int main(int argc, char *argv[])
{
    struct input_event in_ev = {0};
    int fd = -1;

    /* 校验传参 */
    if (2 != argc) {
        fprintf(stderr, "usage: %s <input-dev>\n", argv[0]);
        exit(-1);
    }

    /* 打开文件 */
    if (0 > (fd = open(argv[1], O_RDONLY))) {
        perror("open error");
        exit(-1);
    }

    for ( ; ; ) {
        /* 循环读取数据 */
        if (sizeof(struct input_event) !=
            read(fd, &in_ev, sizeof(struct input_event))) {
            perror("read error");
            exit(-1);
        }
        printf("type:%d code:%d value:%d\n",
               in_ev.type, in_ev.code, in_ev.value);
    }
}

执行程序时需要传入参数,这个参数就是对应的输入设备的设备节点(设备文件的路径),程序中会对传参进行校验。程序中首先调用 open() 函数打开设备文件,之后在 for 循环中调用 read() 函数读取文件,将读取到的数据存放在 struct input_event 结构体对象中,之后将结构体对象中的各个成员变量打印出来。注意,程序中使用了阻塞式 I/O 方式读取设备文件,所以当无数据可读时 read 调用会被阻塞,直到有数据可读时才会被唤醒。

与 GPIO 的操作不同(GPIO 操作的是普通的文件),而此处操作的是设备文件,读写设备文件之前无需设置读写位置偏移量。

按键实机测试

先在 /proc/bus/input/devices 中使用 cat devices 命令查看输入设备对应的设备节点(也就是在 /dev/input 目录下与真实设备的对应关系):

第二个设备是触摸屏,第一个设备是 SNVS 的 Power Key。

通过输入命令 ./read_input /dev/input/event2 就可以不断读取板载 KEY0 的值了:

type = 1 表示上报的是事件 EV_KEY,也就是按键事件。value = 1 表示按键按下,0 表示松开,2 表示长按。在两次按键事件的中间,还穿插了同步事件。

一开始产生的两行 type = 20 的返回(对应 EV_REP,即重复事件,内核在设备初始化时会上报按键重复参数),有兴趣的朋友可以查阅内核头文件了解更多,这里我不太清楚具体的作用。code = 114 对应的是键盘上的 KEY_VOLUMEDOWN 按键,也就是 KEY0,这个按键是在系统已经配置好的。

采用 read() 函数读取 input_event 变量之后,读取的内容会存入 input_event 类型相应的变量中。可以通过读取 input_event 结构体变量中的 type 然后自定义编程:

c 复制代码
if (EV_KEY == in_ev.type) {   // 按键事件
    switch (in_ev.value) {
        case 0:
            printf("code<%d>: 松开\n", in_ev.code);
            break;
        case 1:
            printf("code<%d>: 按下\n", in_ev.code);
            break;
        case 2:
            printf("code<%d>: 长按\n", in_ev.code);
            break;
    }
}

同时还可以插入 USB 设备来测试,比如鼠标和键盘,这里我使用了鼠标:

code = 272 代表的是鼠标左键的 code(对应 BTN_LEFT)。

触摸屏

触摸屏属于绝对位移设备,可以上报绝对位移事件。

单点触摸与多点触摸

触摸屏分为多点触摸设备和单点触摸设备。单点触摸设备只支持单点触摸,一次同步事件内的完整数据只包含一个触摸点信息,具体的上报信息包括了 XY 这种一定有的绝对位移事件,至于其它绝对位移事件根据具体的设备以及驱动的实现而定。

多点触摸设备可支持多点触摸,正点原子的这个开发板是支持多点触摸的。对于多点触摸设备,一轮完整的数据可能包含多个触摸点信息。同样地,具体的上报信息包括了 XY 这种一定有的绝对位移事件,至于其它绝对位移事件根据具体的设备以及驱动的实现而定。

触摸屏设备除了上报绝对位移事件之外,还可以上报按键类事件和同步类事件。同步事件很好理解,因为几乎每一个输入设备都会上报同步事件,告知应用层本轮数据是否完整;当手指点击触摸屏或手指从触摸屏离开时,此时就会上报按键类事件,用于描述按下触摸屏和松开触摸屏;具体的按键事件为 BTN_TOUCHcode = 0x14a,也就是 330),当然,手指在触摸屏上滑动不会上报 BTN_TOUCH 事件。

对于 code = BTN_TOUCH 的事件,也就是屏幕的按键事件不支持长按状态,即 value 不会等于 2。对于多点触摸设备来说,只有第一个点按下时上报 BTN_TOUCH 事件 value = 1,当最后一个点离开触摸屏时上报 BTN_TOUCH 事件 value = 0。

单点触摸设备事件上报的顺序

点击触摸屏时:

  1. BTN_TOUCHvalue = 1)
  2. ABS_X
  3. ABS_Y
  4. SYN_REPORT

滑动:

  1. ABS_X
  2. ABS_Y
  3. SYN_REPORT

松开:

  1. BTN_TOUCHvalue = 0)
  2. SYN_REPORT

以上列举的只是一个大致流程,实际上对于不同的触摸屏设备,能够获取到的信息量大小是不相同的,譬如某设备只能读取到触摸点的 X 和 Y 坐标,而另一设备却能读取 X、Y 坐标以及按压力大小、触点面积等信息,总之这些数据都会在 SYN_REPORT 同步事件之前上报给应用层。

当手指在触摸屏上滑动时,并不会上报 BTN_TOUCH 事件,因为滑动过程并未发生按下、松开这种动作。

多点触摸设备事件上报的顺序

多点触摸设备上报的一轮完整数据中可能包含多个触摸点的信息,譬如 5 点触摸设备,如果 5 个手指同时在触摸屏上滑动,那么硬件就会更新 5 个触摸点的信息,内核需要把这 5 个触摸点的信息上报给应用层。

在 Linux 内核中,多点触摸设备使用多点触摸(MT)协议 上报各个触摸点的数据,MT 协议分为两种类型:Type AType BType A 协议适用于硬件无法区分触摸点编号的设备,需要内核进行跟踪匹配,实际使用中比较少,几乎处于淘汰的边缘;Type B 协议适用于硬件支持为每个触摸点分配唯一 ID 的设备(如大多数现代触摸屏),我们主要用的是 Type B

Type B 协议

Type B 协议适用于能够追踪并区分触摸点的设备,开发板配套使用的触摸屏属于这类设备。Type B 协议的重点是通过 ABS_MT_SLOT 事件上报各个触摸点信息的更新。

硬件能够为每一个识别到的触摸点与一个 slot 进行关联,slot 实际上是一个编号。ABS_MT_SLOT 事件会在多点触摸的事件中上报,这个事件的 value 存放的就是 slot 编号,以告知应用层当前正在更新该 slot 关联的触摸点对应的信息。

除此之外,Type B 协议还会使用到 ABS_MT_TRACKING_ID 事件,用于触摸点的创建、替换和销毁工作。它携带的数据 value 表示一个 ID,一个非负的 ID(ID >= 0)表示一个有效的触摸点,如果 ID 等于 -1 表示该触摸点已经不存在、被移除了;一个以前不存在的 ID 表示这是一个新的触摸点。

这个 IDslot 并不是始终对应的,假设第一次按下,slot 为 0,ID 可能为 80;此时抬起然后再按下,slot 还是为 0,但 ID 可能就变为了 90。

Type B 协议可以减少发送到用户空间的数据,只有发生了变更的数据才会上报。譬如某个触摸点发生了移动,但仅仅只改变了 X 轴坐标、而未改变 Y 轴坐标,那么内核只会将改变后的 X 坐标值通过 ABS_MT_POSITION_X 事件发送给应用层。

获取触摸屏信息

在编写多点触摸应用程序时,通常需要先知道触摸屏支持的最大触摸点数、坐标范围等信息。这些信息可以通过 ioctl 系统调用向输入设备驱动查询得到,用于后续动态分配内存和正确解析触摸数据。

使用 ioctl 查询轴信息

对输入设备调用 ioctl,配合 EVIOCGABS 请求码,可以获取某个绝对轴(ABS_XXX)的详细信息,包括最小值、最大值、当前值等。

EVIOCGABS 的定义在 <linux/input.h> 中:

c 复制代码
#define EVIOCGABS(abs)  _IOR('E', 0x40 + (abs), struct input_absinfo)

使用时需要传入一个 abs 参数(如 ABS_MT_SLOTABS_XABS_Y 等),第三个参数是 struct input_absinfo * 指针。

struct input_absinfo 结构体定义如下:

c 复制代码
struct input_absinfo {
    __s32 value;       // 当前值
    __s32 minimum;     // 最小值
    __s32 maximum;     // 最大值
    __s32 fuzz;        // 噪声容差
    __s32 flat;        // 平坦区
    __s32 resolution;  // 分辨率
};
获取最大触摸点数

要查询触摸屏支持的最大触摸点数,需要获取 ABS_MT_SLOT 轴的信息:

c 复制代码
struct input_absinfo info;
int max_slots;

if (0 > ioctl(fd, EVIOCGABS(ABS_MT_SLOT), &info)) {
    perror("ioctl error");
    // 处理错误
}

max_slots = info.maximum + 1 - info.minimum;

因为 minimum 通常为 0,所以 max_slots = info.maximum + 1。比如一个 5 点触摸屏,minimum = 0,maximum = 4,计算结果就是 5。

ABS_MT_SLOTminimummaximum 是由驱动根据硬件能力设置的,如果触摸屏不支持多点触摸(即单点触摸设备),这个 ioctl 调用通常会失败。

完整的示例代码

获取并打印触摸屏支持的最大触摸点数:

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <linux/input.h>

int main(int argc, char *argv[])
{
    struct input_absinfo info;
    int fd = -1;
    int max_slots;

    if (2 != argc) {
        fprintf(stderr, "usage: %s <input-dev>\n", argv[0]);
        exit(EXIT_FAILURE);
    }

    fd = open(argv[1], O_RDONLY);
    if (0 > fd) {
        perror("open error");
        exit(EXIT_FAILURE);
    }

    if (0 > ioctl(fd, EVIOCGABS(ABS_MT_SLOT), &info)) {
        perror("ioctl error");
        close(fd);
        exit(EXIT_FAILURE);
    }

    max_slots = info.maximum + 1 - info.minimum;
    printf("max_slots: %d\n", max_slots);

    close(fd);
    exit(EXIT_SUCCESS);
}

触摸屏实机测试

单点触摸

单点触摸的代码比较简单,主要是读取 input_event 数据,从中提取出触摸点的坐标和状态信息进行解析。

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <linux/input.h>

int main(int argc, char *argv[])
{
    struct input_event in_ev;
    int x, y;       // 触摸点 X 和 Y 坐标
    int down;       // 用于记录触摸状态:1=按下,0=松开,-1=移动
    int valid;      // 数据有效标志:1=有效,0=无效
    int fd = -1;

    /* 校验传参 */
    if (2 != argc) {
        fprintf(stderr, "usage: %s <input-dev>\n", argv[0]);
        exit(EXIT_FAILURE);
    }

    /* 打开文件 */
    if (0 > (fd = open(argv[1], O_RDONLY))) {
        perror("open error");
        exit(EXIT_FAILURE);
    }

    x = y = 0;          // 初始化坐标
    down = -1;          // 初始状态为"移动"
    valid = 0;          // 初始数据无效

    for ( ; ; ) {
        /* 循环读取数据 */
        if (sizeof(struct input_event) !=
            read(fd, &in_ev, sizeof(struct input_event))) {
            perror("read error");
            exit(EXIT_FAILURE);
        }

        switch (in_ev.type) {
        case EV_ABS:                    // 绝对位移事件
            switch (in_ev.code) {
            case ABS_MT_TRACKING_ID:    // 触摸点 ID(用于判断按下/松开)
                if (0 == in_ev.value) {         // value=0 表示按下
                    down = 1;
                    valid = 1;
                    break;
                } else if (-1 == in_ev.value) { // value=-1 表示松开
                    down = 0;
                    valid = 1;
                    break;
                }
            case ABS_MT_POSITION_X:     // X 坐标
                x = in_ev.value;
                valid = 1;
                break;
            case ABS_MT_POSITION_Y:     // Y 坐标
                y = in_ev.value;
                valid = 1;
                break;
            }
            break;

        case EV_SYN:                    // 同步事件
            if (SYN_REPORT == in_ev.code) {
                if (valid) {            // 数据有效时才输出
                    switch (down) {
                    case 1:
                        printf("按下(%d, %d)\n", x, y);
                        break;
                    case 0:
                        printf("松开\n");
                        break;
                    case -1:
                        printf("移动(%d, %d)\n", x, y);
                        break;
                    }
                    valid = 0;          // 重置标志
                    down = -1;
                }
            }
            break;
        }
    }
}
多点触摸
c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include <string.h>
#include <linux/input.h>

/* 用于描述每一个触摸点的信息 */
struct ts_mt {
    int x;          // X 坐标
    int y;          // Y 坐标
    int id;         // 对应 ABS_MT_TRACKING_ID
    int valid;      // 数据有效标志(=1 表示该触摸点信息发生更新)
};

/* 用于暂存每个 slot 的 X/Y 坐标 */
struct tp_xy {
    int x;
    int y;
};

static int ts_read(const int fd, const int max_slots, struct ts_mt *mt)
{
    struct input_event in_ev;
    static int slot = 0;                        // 当前操作的 slot 编号
    static struct tp_xy xy[12] = {0};           // 暂存每个 slot 的上一次坐标
    int i;

    /* 初始化缓冲区 */
    memset(mt, 0x0, max_slots * sizeof(struct ts_mt));
    for (i = 0; i < max_slots; i++)
        mt[i].id = -2;  // -2 表示"未知/移动";-1 表示松开;>=0 表示按下

    for ( ; ; ) {
        if (sizeof(struct input_event) !=
            read(fd, &in_ev, sizeof(struct input_event))) {
            perror("read error");
            return -1;
        }

        switch (in_ev.type) {
        case EV_ABS:
            switch (in_ev.code) {
            case ABS_MT_SLOT:           // 告知当前更新哪个 slot
                slot = in_ev.value;
                break;
            case ABS_MT_POSITION_X:     // X 坐标
                xy[slot].x = in_ev.value;
                mt[slot].valid = 1;
                break;
            case ABS_MT_POSITION_Y:     // Y 坐标
                xy[slot].y = in_ev.value;
                mt[slot].valid = 1;
                break;
            case ABS_MT_TRACKING_ID:    // 触摸点 ID
                mt[slot].id = in_ev.value;
                mt[slot].valid = 1;
                break;
            }
            break;

        case EV_SYN:
            if (SYN_REPORT == in_ev.code) {
                /* 同步时,将暂存的坐标拷贝到 mt 中供上层使用 */
                for (i = 0; i < max_slots; i++) {
                    mt[i].x = xy[i].x;
                    mt[i].y = xy[i].y;
                }
                return 0;   // 返回 0 表示一轮完整数据读取完毕
            }
            break;
        }
    }
}

int main(int argc, char *argv[])
{
    struct input_absinfo slot;
    struct ts_mt *mt = NULL;
    int max_slots;
    int fd;
    int i;

    /* 校验传参 */
    if (2 != argc) {
        fprintf(stderr, "usage: %s <input_dev>\n", argv[0]);
        exit(EXIT_FAILURE);
    }

    /* 打开文件 */
    fd = open(argv[1], O_RDONLY);
    if (0 > fd) {
        perror("open error");
        exit(EXIT_FAILURE);
    }

    /* 获取触摸屏支持的最大触摸点数 */
    if (0 > ioctl(fd, EVIOCGABS(ABS_MT_SLOT), &slot)) {
        perror("ioctl error");
        close(fd);
        exit(EXIT_FAILURE);
    }
    max_slots = slot.maximum + 1 - slot.minimum;
    printf("max_slots: %d\n", max_slots);

    /* 动态分配内存 */
    mt = calloc(max_slots, sizeof(struct ts_mt));

    /* 循环读取数据 */
    for ( ; ; ) {
        if (0 > ts_read(fd, max_slots, mt))
            break;

        for (i = 0; i < max_slots; i++) {
            if (mt[i].valid) {
                if (0 <= mt[i].id)
                    printf("slot<%d>, 按下(%d, %d)\n", i, mt[i].x, mt[i].y);
                else if (-1 == mt[i].id)
                    printf("slot<%d>, 松开\n", i);
                else
                    printf("slot<%d>, 移动(%d, %d)\n", i, mt[i].x, mt[i].y);
            }
        }
    }

    /* 清理退出 */
    close(fd);
    free(mt);
    exit(EXIT_SUCCESS);
}

以上程序实现了基于 Type B 协议的多点触摸数据读取:

  1. 数据的结构

    • struct ts_mt 描述单个触摸点的信息(坐标、ID、有效标志)。
    • struct tp_xy 用于暂存每个 slot 的坐标,因为 Type B 协议中坐标更新可能先于 TRACKING_ID 到来,需要暂存后统一处理。
  2. ts_read() 函数

    • 内部维护 static int slot 记录当前正在更新的 slot 编号。
    • 每当收到 ABS_MT_SLOT 事件时更新 slot,后续的 ABS_MT_POSITION_X/YABS_MT_TRACKING_ID 都属于该 slot
    • ABS_MT_TRACKING_IDvalue
      • >= 0:有效触摸点(按下/新触摸点)
      • -1:触摸点已移除(松开)
    • 当收到 SYN_REPORT 同步事件时,将暂存的坐标数据批量拷贝到 mt 数组中,返回 0 表示一轮数据就绪。
  3. main() 函数

    • 先通过 ioctl + EVIOCGABS(ABS_MT_SLOT) 获取触摸屏支持的最大触摸点数 max_slots
    • calloc 动态分配 max_slotsstruct ts_mt 结构体。
    • 循环调用 ts_read(),每次返回后遍历所有 slot,根据 id 值判断每个触摸点的状态:
      • id >= 0 按下
      • id == -1 松开
      • id == -2 移动(坐标更新但 ID 未变)
  • id 初始化为 -2,区别于 -1(松开)和 >=0(按下),从而区分"移动"状态。
  • 使用 valid 标志位避免输出未发生更新的 slot 数据。
  • tp_xy 数组暂存坐标,解决 ABS_MT_POSITION_X/YABS_MT_TRACKING_ID 到达顺序可能交错的问题。
相关推荐
爆浇牛肉面1 小时前
Linux三剑客与管道使用全解析:从入门到实战
linux
lightqjx2 小时前
【Linux系统】程序地址空间(初识)
linux·页表·地址空间
wljy12 小时前
二、线程操作
linux·线程属性·线程操作
mounter6252 小时前
探索未来 AI 算力网络的基石:从传统 RoCE 走向 SRv6 驱动的弹性弹性网络(解析 Netdev 0x1A 创新实践)
linux·网络·人工智能·linux kernel·kernel·rdma·rocev2
x_x-F3 小时前
深入浅出 Linux 网络核心:sk_buff 的内存与指针流转
linux·运维·网络
潘正翔13 小时前
docker核心概念
linux·运维·服务器·docker·容器·centos·运维开发
weigangwin13 小时前
agent-workspace-linux 不是远程桌面:Linux AI 工作区的隔离边界验收
linux·xvfb·mcp·桌面隔离·权限边界·bubblewrap·agent-workspace
Dovis(誓平步青云)15 小时前
远程办公软件文件传输实测:6 款工具的速度、稳定性和办公体验对比
linux·运维·服务器·后端·生成对抗网络
Java小白笔记15 小时前
Docker 安装配置完全指南:MacOS 、Windows、Linux环境下的安装、配置与验证
linux·macos·docker