Linux——设备模型

news2024/11/23 11:28:07

目录

本章目标

一、设备模型基础

二、总线、设备和驱动


---------------------------------------------------------------------------------------------------------------------------------

工作两周了,真吃不消啊,虽然年轻但是早8.30晚10点还是不太行,第一种嗷嗷叫中午他们午休我在看内核源码,这周遭不住了以前接触的SOC都是M核或者A核的,真没想到还有ARM核和FPGA结合的SOC,查了资料发现ARM+FPGA+DSP三合一的SOC都有,真是世界之大我就是个渣。由于是国产化的芯片测试我们测试设备的主控包括FPGA都是国产的。之前还在学习阶段老是听一堆人在那说国产芯片手册不行。我用了之后发现和三星的没什么区别,就是把多写了点高大上但是对开发没用的东西。剩下都一样也没有不详细什么的。国产芯片总得来说还是很不错的。最后我想吐槽一下,研究生究竟比本科生多了什么。刚来什么都干不了还得学习。然后每天到点就下班不用加班工作还比本科生高。多学了三年除了浪费青春感觉没多学什么啊。除了这种也有离谱的怪胎,加拿大留学生高二暑假回国兼职能直接上手干活。13岁的初中生报培训班课余时间学嵌入式。真离谱哇卷死我啦。好了就到这里后面介绍嵌入式linux的设备模型。

---------------------------------------------------------------------------------------------------------------------------------

本章目标


        之前编写的驱动程序虽然都能正常工作,但是还是存在着一些弊端。本章首先罗列出了这些弊端,然后引出了 Linux的设备模型,并对核心的底层技术进行了讨论。接下来详述平台设备和驱动的实现方法,在这个过程中将会看到所提到的弊端是如何一个个被解决的。最后讨论了 Linux 内核引入的设备树,并实现了对应的驱动。本章的内容较抽象,需要具备一些面向对象的编程思想。
 

        在前面的基础上,我们已经能够开发一个功能较完备的字符设备驱动了,但是仔细考虑这个驱动,还是会发现一些不足,主要存在下面的一些问题。

(1)设备和驱动没有分离,也就是说,设备的信息是硬编码在驱动代码中的,这给驱动程序造成了极大的限制。如果硬件有所改动,那么必然要修改照动代码(比知对于第前面的 LED 硬件,如果改变了驱动 LED 的管脚,那么就必然要修改 LED 的驱动代码)这样驱动的通用性将会非常差。这是最突出的一个问题,必须要很好地解决。

(2)没有类似于 Windows 系统中的设备管理器,不可以方便查看设备和驱动的信息。
(3)不能自动创建设备节点。
(4)驱动不能自动加载。
(5)U 盘 SD 卡等不能自动挂载。
(6) 没有电源管理。
        其实这些问题在 Linux下都是有解决方法的,这些间题的解决主要依托于 Linux 的设备驱动模型,本章后面的内容都会围绕怎样解决这些问题来展开。


一、设备模型基础


        首先我们来看上面提到的第二个问题,就是关于设备和驱动信息的展示。在 Linux系统中有一个sysfs伪文件系统,挂于/sys 目录下,该目录详细罗列了所有与设备、驱动和硬件相关的信息。例如,在FS4412 的终端上,可以使用下面的命令来查看。

bus下面是一些总线接口,我最近pcie用的挺多的,咱们三星这块板子里面没有这个接口 

 

 devices下面是一些具体的设备

 这个是我们的网卡设备

 这个是dm9000的驱动


        在/sys目录下有很多子目录,例如 block 目录下是块设备、bus 目录下是系统中的有总线(如12C、SPI和USB 等)、class 目录下是一些设备类 (如 input 输入设备类、t终端设备类)、devices 目录下是系统中所有的设备。再仔细查看sys/bus/platfom/devices/5000000.ethemet/目录,它是一个挂接在一个叫 platform 总线下的以太网设备,其目录下的 driver 是一个软链接,指向了../../../bus/platform/drivers/dm9000,也就是说,该设备是被注册在 platfor总线下的一个名叫dm9000的驱动程序所驱动再看对应的驱动目录/sys/bus/platform/drivers/dm9000/,会发现该驱动程序驱动了../../.J../devices/5000000.srom-cs1/5000000.ethermet 设备,即驱动了 devices 目录下的以太网设备,而/sys/busplatform/devices/5000000.ethemnet 又是指向.J.././devices/5000000.srom-csl/5000000.ethemet的软链接。所以也可以说前面的驱动程序驱动了/sys/bus/platform/drivers/dm9000/设备。

        上面的内容看起来有点乱,但思路是清晰的,即在总线 bus 目录下有很多具体的总线,而具体的总线目录下有注册的驱动和挂接的设备,注册的驱动程序驱动对应总线目录下的某些具体设备,总线目录下的某些设备被对应总线下的某个驱动程序所驱动。
        那么上面这些信息是怎么来的呢。我们知道,伪文件系统在系统运行时才会有内容也就是说,伪文件系统的目录、文件以及软链接都是动态生成的,这些内容都是反映核的相关信息,回顾我们之前学习的 proc 接口,不难猜想得出这些信息的生成可以在驱动中来实现。接下来我们就来讨论要生成这些信息的一个重要内核数据结构一一struct kobject。
        了解 MFC 或者 QT 的人都知道那些窗口部件都是一层一层继承下来的,而在最上层有一个最基础的类,MFC 的根类是 CObject,而QT的根类是QObject。这里我们将结构看成类。那么kobject就是linux设备驱动模型的中的根类。作为驱动开发者,我们没有必要了解kobject的详细信息,就像作为一个QT应用程序开发者不需要了解QObject的详细信息一样。在这里,我们只需要知道他和/sys目录下的目录和文件的关系。

        当向内核成功添加一个 kobject 对象后,底层的代码会自动在/sys 目录下生成一个子目录。另外,kobject 可以附加一些属性,并绑定操作这些属性的方法,当向内核成功添加一个 kobject 对象后,其附加的属性会被底层的代码自动实现为对象对应目录下的文件,用户访问这些文件最终就变成了调用操作属性的方法来访问其属性。最后,通过 sys 的API接口可以将两个 kobject 对象关联起来,形成软链接。
        除了 struct kobject,还有一个叫 struct kset 的类,它是多个 kobject 对象的集合,也就是多个 kobiect 对象可以通过一个 kset 集合在一起。kset 本身也内了一个kobject,它可以作为集合中的 kobiect 对象的父对象,从而在 kobject 之间形成父子关系,这种父子关系在/sys目录下体现为父目录和子目录的关系。而属于同一集合的 kobject 对象形成兄弟关系,在/sys目录下体现为同级目录。kset 也可以附加属性,从而在对应的目录下产生文件。
        为了能更好地了解这部分内容,而又不过分深入细节,特别编写了一个非常简单的模块,为了突出主线,省略了出错处理

#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>

#include <linux/slab.h>
#include <linux/kobject.h>

static struct kset *kset;
static struct kobject *kobj1;
static struct kobject *kobj2;
static unsigned int val = 0;

static ssize_t val_show(struct kobject *kobj, struct kobj_attribute *attr, char *buf)
{
	return snprintf(buf, PAGE_SIZE, "%d\n", val);
}

static ssize_t val_store(struct kobject *kobj, struct kobj_attribute *attr, const char *buf, size_t count)
{
	char *endp;

	printk("size = %d\n", count);
	val = simple_strtoul(buf, &endp, 10);

	return count;
}

static struct kobj_attribute kobj1_val_attr = __ATTR(val, 0666, val_show, val_store);
static struct attribute *kobj1_attrs[] = {
	&kobj1_val_attr.attr,
	NULL,
};

static struct attribute_group kobj1_attr_group = { 
	        .attrs = kobj1_attrs,
};

static int __init model_init(void)
{
	int ret;

	kset = kset_create_and_add("kset", NULL, NULL);
	kobj1 = kobject_create_and_add("kobj1", &kset->kobj);
	kobj2 = kobject_create_and_add("kobj2", &kset->kobj);

	ret = sysfs_create_group(kobj1, &kobj1_attr_group);
	ret = sysfs_create_link(kobj2, kobj1, "kobj1");

	return 0;
}

static void __exit model_exit(void)
{
	sysfs_remove_link(kobj2, "kobj1");
	sysfs_remove_group(kobj1, &kobj1_attr_group);
	kobject_del(kobj2);
	kobject_del(kobj1);
	kset_unregister(kset);
}

module_init(model_init);
module_exit(model_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("name <e-mail>");
MODULE_DESCRIPTION("A simple module for device model");

        代码第 42行使用 kset_create_and_add 创建并向内核添加了一个名叫 kset 的 kset 对象.代码第 43 行和第 44 行用 kobject_creae_and _add 分别创建并向内添加两个名叫 kobj1和kobj2 的 kobject 对象。代码第 46 行为kobj1添加了一组属性 kobj1_attr_group.这组属性中只有一个属性叫 kobj1_attr_group,属性的名字叫val,所绑定的读和写的方法分别是val show 和 val_store。对应的文件访间权限是 0666。代码第47 行使用 sysfs_create_link在 kobj2 下创建了一个kobj1 的软链接,名叫 kobj1。

        代码第 54 行至第 58 行是初始化操作的反操作,用于删除软链接、属性和对象。

        属性 val 的读方法将 val 的值以格式%d 打印在 buf 中,那么读相应的属性文件则会得到 val 的十进制字符串。属性 val 的写方法是将用户写入文件的内容,即 buf中的字符串通过simple_strtoul 将字符申转换成十进制的数值再赋值给 val。

没有安装tree回到ubuntu环境

 

 哪天有时间我重新规划一下内核用chroot来给开发板加点命令


        在创建 kset 对象时,由于没有指定其父对象,所以 kset 位于/sys 目录下,在创建 kobjl和 kobj2 时,指定其父对象为 kset 中内嵌的 kobject,所以 kobj1和 kobj2位于 kset 目录之下。kobi1 附加了一个属性叫 val,所以在 kobj1 目录下有一个val 的文件,对该文件可以进行读写,其实就是对属性 val 进行读写。在 kobj2 下创建了一个软链接 kobj1,所以在kobj2目录下有 kobj1的软链接。对象的关系如图所示。其中,虚线表示 kobj1、kobj2属于集合 kset,kobjl和 kobj2 实线指向 kset 内的 kobject 表示它们的父对象是 kset内嵌的 kobject。

 
二、总线、设备和驱动


        如图所示,在一台拥有 USB 总线的计算机系统上,USB,总线会在外部流出很多USB 接口,挂接很多 USB 设备。为了让这些设备能正常工作,系统上也会安装其对应的驱动。虽然这些驱动在硬件上和 USB 总线没有直接的连接,但是从软件层面来看,它们是注册在 USB 总线下面的。一个便于理解的简化后的情况是这样的: 当接入一个 USB设备时,USB 总线会立即感知到这件事,并去追历所有注册在 USB 总线上的驱动(在这个过程中可能会自动加载一个匹配的 USB 驱动),然后调用驱动中的一段代码来探测是否能够驱动刚插入的 USB 设备,如果可以,那么总线完成驱动和设备之间的绑定。

 


        为了刻画上面的三种对象,Linux 设备模型为这三种对象各自定义了对应的类:struct bus_type 代表总线、struct device 代表设备、struct device_driver 代表驱动。这三者都内嵌了struct kobject或struct kset,于是就会生成对应的总线、设备和驱动的目录。另外,Linux内核还为这些kobject和 kset 对象附加了很多属性,于是也产生了很多对应目录下的文件可以这样认为,总线、设备和驱动都继承自同一个基类 struct kobject,使用面向对象的思想来理解它们之间的关系会非常容易。将这三者分开来刻画,不仅和现实生活中的情景相符合,更重要的是解决了本章开始提出的第一个问题,那就是实现了设备和驱动的分离。设备专门用来描述设备所占有的资源信息,而驱动和设备绑定成功后,驱动负责从设备中动态获取这些资源信息,当设备的资源改变后,只是设备改变而已,驱动的代码可以不做任何修改,这就大大提高了驱动代码的通用性。另外,总线是联系两者的桥梁是一条重要的纽带。
        为了能够更好地理解这个设备模型对驱动编程带来的影响,又不用过多地深入细节,我们以一个最简单的例子来进行说明:

#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>

#include <linux/device.h>

static int vbus_match(struct device *dev, struct device_driver *drv)
{
	return 1;
}

static struct bus_type vbus = {
	.name = "vbus",
	.match = vbus_match,
};

EXPORT_SYMBOL(vbus);

static int __init vbus_init(void)
{
	return bus_register(&vbus);
}

static void __exit vbus_exit(void)
{
	bus_unregister(&vbus);
}

module_init(vbus_init);
module_exit(vbus_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("name <e-mail>");
MODULE_DESCRIPTION("A virtual bus");
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>

#include <linux/device.h>

extern struct bus_type vbus;

static struct device_driver vdrv = {
	.name = "vdrv",
	.bus = &vbus,
};

static int __init vdrv_init(void)
{
	return driver_register(&vdrv);
}

static void __exit vdrv_exit(void)
{
	driver_unregister(&vdrv);
}

module_init(vdrv_init);
module_exit(vdrv_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("name <e-mail>");
MODULE_DESCRIPTION("A virtual device driver");
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>

#include <linux/device.h>

extern struct bus_type vbus;

static void vdev_release(struct device *dev)
{
}

static struct device vdev = {
	.init_name = "vdev",
	.bus = &vbus,
	.release = vdev_release,
};

static int __init vdev_init(void)
{
	return device_register(&vdev);
}

static void __exit vdev_exit(void)
{
	device_unregister(&vdev);
}

module_init(vdev_init);
module_exit(vdev_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("name <e-mail>");
MODULE_DESCRIPTION("A virtual device");


        在 vbus.c 文件中,代码第 12 行至第 15 行定义了一个代表总线的 vbus 对象,该总线的名字是 vbus,用于匹配驱动和设备的函数是 vbus_match。代码第21 行向内核注册了该总线。代码第 26 行是总线的注销。为了简单起见,vbus_match 仅仅返回1,表示传入的设备和驱动匹配成功,而更一般的情况是考察它们的 ID 号是否匹配。
        在 vdrv.c 文件中,代码第 9 行至第 12 行定义了一个代表驱动的 vdrv 对象,该驱动的名字是 vdrv,所属的总线是 vbus,这样注册这个驱动时,就会将之注册在 vbus 总线之下代码第 16行和第21行分别是驱动的注册和注销操作。模块中使用了 bus 模块导出的符号 vbus。
        在 vdev.c 文件中,代码第 13 行至第 17 行定义了一个代表设备的 vdev 对象,该设备的名字是 vdev,是完全用代码虚拟出来的一个设备。所属的总线是 vbus,这样注册这个设备时,就会将之挂接到 vbus 总线之下。还有一个用于释放的函数 dev_release,为了简单起见,这个函数什么都没做。代码第 21 行和第 26 行分别是设备的注册和注销。模块中使用了 vbus 模块导出的符号 vbus。
        下面是编译和测试的命令。


        在加载了 vbus 模块后,/sys/bus 目录下自动生成了 bus 目录,并且在 vbus 目录下生成了 devices 和 drivers 两个目录,分别来记录挂接在 vbus 总线上的设备和注册在 vbus 总线上的驱动。当加载了vdrv 模块后,/sys/bus/vbus/drivers 目录下自动生成了 vdrv 目录此时还没有设备与之绑定。当加载了 vdev 模块后,/sys/bus/vbus/devices 目录下自动生成了vdev 目录,并且和../../bus/vbus/drivers/vdrv 的驱动绑定成功,在/sys/devices 目录下也自动生成了 vdev目录,其实/sys/bus/vbus/devices/vdev 是指向/sys/bus/vbus/devices/vdev的软链接。最后 /sys/bus/vbus/drivers/vdrv/中的 vdev 也指定了其绑定的设备为.././..../devicesvdev。
        这和我们在前面看到的 DM9000 网卡非常类似,只是 DM9000 网卡设备是挂接在platform总线下的,而驱动也是注册在 platform 总线下的。
 

        虽然使用 struct bus_type  struct device 和 struct device_driver 能够实现 Linux 设备模型,但是它们的抽象层次还是太高,不能具体地刻画某一种特定的总线。所以一种具体的总线会在它们的基础上派生出来,形成更具体的子类,这些子类对象能够更好地描述相应的对象。比如,针对USB 总线就派生出了 struct usb_bus_type、struct usb_device和struct usb_driver,分别代表具体的USB 总线、USB 设备和USB 驱动。通常情况下,总线已经在内核中实现好,我们只需要写对应总线的驱动即可,有时候还会编写相应的设备注册代码。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/795513.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

JAVA面试总结-Redis篇章(四)——双写一致性

JAVA面试总结-Redis篇章&#xff08;四&#xff09;——双写一致性 问&#xff1a;redis 做为缓存&#xff0c;mysql的数据如何与redis进行同步呢&#xff1f;第一种情况&#xff0c;如果你的项目一致性要求高的话 采用以下逻辑我们应该先删除缓存&#xff0c;再修改数据库&…

【C++】STL——string的模拟实现、常用构造函数、迭代器、运算符重载、扩容函数、增删查改

文章目录 1.模拟实现string1.1构造函数1.2迭代器1.3运算符重载1.4扩容函数1.5增删查改 1.模拟实现string string使用文章 1.1构造函数 这里我们实现常用的第四个string(const char* s)和析构函数 class string { public://初始化列表赋值//string(const char* str "\0…

如何搭建一个成功的书店小程序

随着互联网的快速发展&#xff0c;传统实体书店面临着客流量下降的困境。为了适应市场需求&#xff0c;书店行业转型变得尤为重要。而制作书店小程序商城则成为了一种必备的转型方式。下面将详细介绍如何利用专业的小程序商城制作平台&#xff0c;手把手制作书店小程序商城。 首…

Docker 基础知识解析:容器与虚拟化的区别与优势

&#x1f337;&#x1f341; 博主猫头虎 带您 Go to New World.✨&#x1f341; &#x1f984; 博客首页——猫头虎的博客&#x1f390; &#x1f433;《面试题大全专栏》 文章图文并茂&#x1f995;生动形象&#x1f996;简单易学&#xff01;欢迎大家来踩踩~&#x1f33a; &a…

DTH11 温湿度模块

文章目录 前言一、DTH11 模块介绍二、设备树设置三、驱动程序四、测试程序五、上机测试及效果总结 前言 DHT11 是一款可测量 温度 和 湿度 的传感器。比如市面上一些空气加湿器&#xff0c;会测量空气中湿度&#xff0c;再根据测量结果决定是否继续加湿。 一、DTH11 模块介绍 …

VS下c++解析pcap文件

一、pcap文件格式 https://www.cnblogs.com/Chary/articles/15716063.html 接口协议&#xff08;四&#xff09;&#xff1a;以太网&#xff08;Ethernet&#xff09;学习&#xff08;一&#xff09;&#xff1a;协议_以太网协议_QNee的博客-CSDN博客 二、代码 pcapParser.h #…

向npm注册中心发布包(下)

目录 1、在package.json文件中指定dependencies和devDependencies 1.1 将依赖项添加到 package.json 文件 1.2 从命令行中 将依赖项添加到 package.json 文件 1.3 手动编辑 package.json 文件 2、关于语义版本控制 2.1 在已发布的包中增加语义版本 2.2 使用语义版本控制…

CentOS7系统Nvidia Docker容器基于TensorFlow2.12测试GPU

CentOS7系统Nvidia Docker容器基于TensorFlow1.15测试GPU 参考我的另一篇博客 1. 安装NVIDIA-Docker的Tensorflow2.12.0版本 1. 版本依赖对应关系&#xff1a;从源代码构建 | TensorFlow GPU 版本Python 版本编译器构建工具cuDNNCUDAtensorflow-2.6.03.6-3.9GCC 7.3.1Ba…

Linux设置密码复杂度

在etc目录下pam.d目录下&#xff0c;存在system-auth文件 先将文件备份下&#xff0c;然后在system-auth中插入下面行 password requisite pam_pwquality.so try_first_pass local_users_only retry3 authtok_type minlen8 lcredit-1 ucredit-1 dcredit-1 ocredi…

OpenCV 4.0+Python机器学习与计算机视觉实战

&#x1f482; 个人网站:【办公神器】【游戏大全】【神级源码资源网】&#x1f91f; 前端学习课程&#xff1a;&#x1f449;【28个案例趣学前端】【400个JS面试题】&#x1f485; 寻找学习交流、摸鱼划水的小伙伴&#xff0c;请点击【摸鱼学习交流群】 目录 前言第一部分&…

Shell脚本学习-变量子串

变量子串&#xff1a; man bash&#xff0c;然后搜索&#xff1a;Parameter Expansion。 参数拓展 $字符引进、提出了参数拓展、命令替换和数字替换。变量名或标识被大括号包围才能够被拓展。 我们可以记住一个表&#xff1a; 序号表达式说明1${parameter}返回变量$paramete…

东南大学齿轮箱故障诊断(Python代码,CNN结合LSTM模型)

运行代码要求&#xff1a; 代码运行环境要求&#xff1a;Keras版本>2.4.0&#xff0c;python版本>3.6.0 1.东南大学采集数据平台&#xff1a; 数据 该数据集包含2个子数据集&#xff0c;包括轴承数据和齿轮数据&#xff0c;这两个子数据集都是在传动系动力学模拟器&am…

PVS-Studio Crack,重新编译后的自动分析

PVS-Studio Crack,重新编译后的自动分析 PVS Studio执行静态代码分析并生成报告&#xff0c;帮助程序员查找和修复错误。PVS Studio执行广泛的代码检查&#xff0c;搜索印刷错误和复制粘贴错误也很有用。此类错误的示例&#xff1a;V501、V517、V522、V523、V3001。 静态分析的…

常常会用到的截取字符串substr()、substring()、slice()方法详解

常常会用到的截取字符串substr()、substring()、slice()方法详解 slice() 定义&#xff1a;接受一个或者两个参数&#xff0c;第一个参数指定子字符串的开始位置。第二个参数表示子字符串的结束位置&#xff08;不包括结束位置的那个字符&#xff09;&#xff0c;如果没有传递…

Linux用户权限问题详解

Linux用户权限问题详解 【一】Linux权限的概念&#xff08;1&#xff09;用户类型&#xff08;2&#xff09;如何切换用户&#xff08;3&#xff09;用户相关的一些命令 【二】Linux文件权限管理&#xff08;1&#xff09;文件访问者的分类&#xff08;2&#xff09;文件类型和…

激光点云数据如何在客户端进行管理、查看及分享?

四维轻云是一款地理空间数据在线管理平台&#xff0c;具有地理空间数据的在线管理、查看及分享等功能。在四维轻云平台中&#xff0c;用户可以不受时间地点限制&#xff0c;随时随地上传数字高程模型、激光点云、倾斜摄影模型、正射影像等地理空间数据。 现在&#xff0c;小编…

算法(3)

喝汽水 两瓶即可换一瓶 import java.util.*; public class Main {public static void main(String[] args) {//剩2个空瓶子时&#xff0c;可以先找老板借一瓶汽水&#xff0c;喝掉这瓶满的&#xff0c;喝完以后用3个空瓶子换一瓶满的还给老板。 //也就是说2个空瓶子即可换一瓶汽…

vue如何设置网站标题和logo图标

目录 1、在根目录找到项目index.html文件 2、在index.html 的 title标签中修改名称为自己设计的标题 3、在index.html 的 title标签下的link标签中引入图标 ①格式为&#xff1a; ②注意&#xff1a; 1、在根目录找到项目index.html文件 2、在index.html 的 title标签中修改…

Visual Assist X Crack

Visual Assist X Crack Visual Assist X通过Visual Studio中的关键新功能和对现有功能的改进&#xff0c;大大缩短了应用程序开发时间&#xff0c;使您能够&#xff1a;Visual Assist提高了自动化程度&#xff0c;简化了导航&#xff0c;并在开发过程中显示重要信息。这些功能已…

WebDAV之π-Disk派盘+ WinSCP

WinSCP是一个免费的开源文件传输应用程序&#xff0c;它使用文件传输协议&#xff0c;安全外壳文件传输协议和安全复制协议来进行纯文件或安全文件传输。该应用程序旨在与Windows一起使用&#xff0c;并支持常见的Windows桌面功能&#xff0c;例如拖放文件&#xff0c;跳转列表…