当前位置: 首页 > news >正文

Linux驱动学习(六)一些函数

Linux 系统将可访问的内存空间分为了两个部分, 一部分是内核空间, 一部分是用户空间。操作系统和驱动程序运行在内核空间(内核态) , 应用程序运行在用户空间(用户态)

(1) 内核空间中的代码控制了硬件资源, 用户空间中的代码只能通过内核暴露的系统调用接口来使用系统中的硬件资源, 这样的设计可以保证操作系统自身的安全性和稳定性。
(2) 从另一方面来说, 内核空间的代码更偏向于系统管理, 而用户空间中的代码更偏重业务逻辑实现, 俩者的分工不同。

用户空间和内核空间数据交换

内核空间和用户空间的内存是不能互相访问的。 但是很多应用程序都需要和内核进行数据的交换, 例如应用程序使用 read 函数从驱动中读取数据, 使用 write 函数向驱动中写数据。

上述功能就需要使用 copy_from_user 和 copy_to_user 俩个函数来完成。
copy_from_user 函数是将用户空间的数据拷贝到内核空间。
copy_to_user 函数是将内核空间的数据拷贝到用户空间。

代码

#include <linux/init.h>
#include <linux/module.h>
#include <linux/kdev_t.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>static dev_t dev_num;  //设备号
static int major = 0;  //主设备号
static int minor = 0;  //次设备号
struct cdev cdev_test; // cdevstruct class *class;       //类
struct device *device;    //设备/*打开设备函数*/
static int cdev_test_open(struct inode *inode, struct file *file)
{printk("This is cdev_test_open\r\n");return 0;
}/*向设备写入数据函数*/
static ssize_t cdev_test_write(struct file *file, const char __user *buf, size_t size, loff_t *off)
{/*本章实验重点******/char kbuf[32] = {0};   //定义写入缓存区kbufif (copy_from_user(kbuf, buf, size) != 0) // copy_from_user:用户空间向内核空间传数据{printk("copy_from_user error\r\n");//打印copy_from_user函数执行失败return -1;}printk("This is cdev_test_write\r\n");printk("kbuf is %s\r\n", kbuf);return 0;
}/*从设备读取数据*/
static ssize_t cdev_test_read(struct file *file, char __user *buf, size_t size, loff_t *off)
{/*本章实验重点******/
char kbuf[32] = "This is cdev_test_read!";//定义内核空间数据
// copy_to_user:内核空间向用户空间传数据if (copy_to_user(buf, kbuf, strlen(kbuf)) != 0)     {printk("copy_to_user error\r\n"); //打印copy_to_user函数执行失败return -1;}printk("This is cdev_test_read\r\n");return 0;
}static int cdev_test_release(struct inode *inode, struct file *file)
{printk("This is cdev_test_release\r\n");return 0;
}/*设备操作函数,定义file_operations结构体类型的变量cdev_test_fops*/
struct file_operations cdev_test_fops = {.owner = THIS_MODULE, //将owner字段指向本模块,可以避免在模块的操作正在被使用时卸载该模块.open = cdev_test_open, //将open字段指向chrdev_open(...)函数.read = cdev_test_read,  //将open字段指向chrdev_read(...)函数.write = cdev_test_write, //将open字段指向chrdev_write(...)函数.release = cdev_test_release, //将open字段指向chrdev_release(...)函数
};static int __init chr_fops_init(void) //驱动入口函数
{/*注册字符设备驱动*/int ret;
/*1 创建设备号*/
//动态分配设备号ret = alloc_chrdev_region(&dev_num, 0, 1, "alloc_name");     if (ret < 0){printk("alloc_chrdev_region is error\n");//打印动态分配设备号失败}printk("alloc_chrdev_region is ok\n");major = MAJOR(dev_num); //获取主设备号minor = MINOR(dev_num); //获取次设备号printk("major is %d \r\n", major); //打印主设备号printk("minor is %d \r\n", minor); //打印次设备号/*2 初始化cdev*/cdev_test.owner = THIS_MODULE;cdev_init(&cdev_test, &cdev_test_fops);/*3 添加一个cdev,完成字符设备注册到内核*/cdev_add(&cdev_test, dev_num, 1);/*4 创建类*/class = class_create(THIS_MODULE, "test");/*5  创建设备*/device = device_create(class, NULL, dev_num, NULL, "test");return 0;
}static void __exit chr_fops_exit(void) //驱动出口函数
{/*注销字符设备*/unregister_chrdev_region(dev_num, 1); //注销设备号cdev_del(&cdev_test);               //删除cdevdevice_destroy(class, dev_num);       //删除设备class_destroy(class);                 //删除类
}
module_init(chr_fops_init);   //注册入口函数
module_exit(chr_fops_exit);  //注册出口函数
MODULE_LICENSE("GPL v2");
MODULE_AUTHOR("topeet");
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>int main(int argc, char *argv[])  //主函数
{int fd;   //定义int类型的文件描述符char buf1[32] = {0}; //定义读取缓存区buf1char buf2[32] = "nihao"; //定义写入缓存区buf2fd = open("/dev/test", O_RDWR);  //打开字符设备驱动if (fd < 0){perror("open error \n");return fd;}read(fd, buf1, sizeof(buf1));//从/dev/test文件读取数据printf("buf1 is %s \r\n", buf1); //打印读取的数据write(fd,buf2,sizeof(buf2));//向/dev/test文件写入数据close(fd);return 0;
}

文件私有

在之前章节的驱动程序中,将生成字符设备的一些硬件属性(设备号、类、设备名称等)全都写成了变量的形式,虽然这样编写驱动代码不会产生报错,但是会显得有点不专业。通常在驱动开发中会为设备定义相关的设备结构体,将硬件属性的描述信息全部放在该结构体中。

Linux中并没有明确规定要使用文件私有数据

struct file 结构体中专门为用户留了一个域用于定义私有数据

驱动代码:

#include <linux/init.h>
#include <linux/module.h>
#include <linux/kdev_t.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>struct device_test{dev_t dev_num;              //设备号int major ;                  //主设备号int minor ;                   //次设备号struct cdev cdev_test;          // cdevstruct class *class;             //类struct device *device;          //设备char kbuf[32];                //缓存区buf
};struct  device_test dev1;  //定义一个device_test结构体变量/*打开设备函数*/
static int cdev_test_open(struct inode *inode, struct file *file)
{file->private_data=&dev1;  //设置私有数据printk("This is cdev_test_open\r\n");return 0;
}/*向设备写入数据函数*/
static ssize_t cdev_test_write(struct file *file, const char __user *buf, size_t size, loff_t *off)
{struct device_test *test_dev=(struct device_test *)file->private_data; //在write函数中读取private_dataif (copy_from_user(test_dev->kbuf, buf, size) != 0) // copy_from_user:用户空间向内核空间传数据{printk("copy_from_user error\r\n");return -1;}printk("This is cdev_test_write\r\n");printk("kbuf is %s\r\n", test_dev->kbuf); //打印kbuf的值return 0;
}/**从设备读取数据*/
static ssize_t cdev_test_read(struct file *file, char __user *buf, size_t size, loff_t *off)
{struct device_test *test_dev=(struct device_test *)file->private_data;    if (copy_to_user(buf, test_dev->kbuf, strlen( test_dev->kbuf)) != 0) // copy_to_user:内核空间向用户空间传数据
{printk("copy_to_user error\r\n");return -1;}printk("This is cdev_test_read\r\n");return 0;
}static int cdev_test_release(struct inode *inode, struct file *file)
{printk("This is cdev_test_release\r\n");return 0;
}/*设备操作函数*/
struct file_operations cdev_test_fops = {.owner = THIS_MODULE,         //将owner字段指向本模块,可以避免在模块的操作正在被使用时卸载该模块.open = cdev_test_open,         //将open字段指向chrdev_open(...)函数.read = cdev_test_read,          //将open字段指向chrdev_read(...)函数.write = cdev_test_write,         //将open字段指向chrdev_write(...)函数.release = cdev_test_release,//将open字段指向chrdev_release(...)函数
};static int __init chr_fops_init(void) //驱动入口函数
{/*注册字符设备驱动*/int ret;/*1 创建设备号*/ret = alloc_chrdev_region(&dev1.dev_num, 0, 1, "alloc_name"); //动态分配设备号if (ret < 0){printk("alloc_chrdev_region is error\n");}printk("alloc_chrdev_region is ok\n");dev1.major = MAJOR(dev1.dev_num); //获取主设备号dev1.minor = MINOR(dev1.dev_num); //获取次设备号printk("major is %d \r\n", dev1.major); //打印主设备号printk("minor is %d \r\n", dev1.minor); //打印次设备号/*2 初始化cdev*/dev1.cdev_test.owner = THIS_MODULE;cdev_init(&dev1.cdev_test, &cdev_test_fops);/*3 添加一个cdev,完成字符设备注册到内核*/cdev_add(&dev1.cdev_test, dev1.dev_num, 1);/*4 创建类*/dev1. class = class_create(THIS_MODULE, "test");/*创建设备*/dev1.device = device_create(dev1.class, NULL, dev1.dev_num, NULL, "test");return 0;
}static void __exit chr_fops_exit(void) //驱动出口函数
{/*注销字符设备*/unregister_chrdev_region(dev1.dev_num, 1); //注销设备号cdev_del(&dev1.cdev_test);                 //删除cdevdevice_destroy(dev1.class, dev1.dev_num);       //删除设备class_destroy(dev1.class);                 //删除类
}
module_init(chr_fops_init);
module_exit(chr_fops_exit);
MODULE_LICENSE("GPL v2");
MODULE_AUTHOR("topeet");

app.c

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>int main(int argc, char *argv[]) //主函数
{int fd;char buf1[32] = "nihao";  //定义写入缓存区buf1fd = open("/dev/test", O_RDWR); //打开/dev/test设备if (fd < 0){perror("open error \n");return fd;}write(fd,buf1,sizeof(buf1)); //向/dev/test设备写入数据close(fd);return 0;
}

一个驱动兼容不同设备

在Linux中,使用主设备号来表示对应某一类驱动,使用次设备号来表示这类驱动下的各个设备。假如现在驱动要支持的主设备号相同,但是次设备号不同的设备。

container_of在Linux内核中是一个常用的宏,用于从包含在某个结构中的指针获得结构本身的指针,通俗地讲就是通过结构体变量中某个成员的首地址进而获得整个结构体变量的首地址。可以使用这个函数获取不同设备的地址,来对不同的设备进行操作,从而一个驱动可以兼容不同的设备。

container_of在Linux内核中是一个常用的宏,用于从包含在某个结构中的指针获得结构本身的指针,通俗地讲就是通过结构体变量中某个成员的首地址进而获得整个结构体变量的首地址。那么可以使用这个函数获取不同设备的地址,来对不同的设备进行操作,从而一个驱动可以兼容不同的设备。

//函数作用:通过结构体变量中某个成员的首地址获取到整个结构体变量的首地址
//参数:ptr是结构体变量中某个成员的地址。type是结构体的类型,member是该结构体变量的具体名字
container_of(ptr,type,member)

驱动代码

#include <linux/init.h>
#include <linux/module.h>
#include <linux/kdev_t.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>struct device_test
{dev_t dev_num;             //设备号int major;                  //主设备号int minor;                  //次设备号struct cdev cdev_test;        // cdevstruct class *class;           //类struct device *device;        //设备char kbuf[32];              //定义缓冲区kbuf
};struct device_test dev1;   //定义一个device_test结构体变量dev1
struct device_test dev2;  //定义一个device_test结构体变量dev2/*打开设备函数*/
static int cdev_test_open(struct inode *inode, struct file *file)
{dev1.minor = 0;    //设置dev1的次设备号为0dev2.minor = 1;   //设置dev2的次设备号为1//inode->i_rdev 为该 inode 的设备号,使用container_of函数找到结构体变量dev1 dev2的地址
//然后设置私有数据file->private_data = container_of(inode->i_cdev, struct device_test, cdev_test);printk("This is cdev_test_open\r\n");return 0;
}/*向设备写入数据函数*/
static ssize_t cdev_test_write(struct file *file, const char __user *buf, size_t size, loff_t *off)
{struct device_test *test_dev = (struct device_test *)file->private_data;//如果次设备号是0,则为dev1if (test_dev->minor == 0){if (copy_from_user(test_dev->kbuf, buf, size) != 0) // copy_from_user:用户空间向内核空间传数据{printk("copy_from_user error\r\n");return -1;}printk(" test_dev->kbuf is %s\r\n", test_dev->kbuf);}//如果次设备号是1,则为dev2else if(test_dev->minor == 1){if (copy_from_user(test_dev->kbuf, buf, size) != 0) // copy_from_user:用户空间向内核空间传数据{printk("copy_from_user error\r\n");return -1;}printk(" test_dev->kbuf is %s\r\n", test_dev->kbuf);}return 0;
}/**从设备读取数据*/
static ssize_t cdev_test_read(struct file *file, char __user *buf, size_t size, loff_t *off)
{struct device_test *test_dev = (struct device_test *)file->private_data;if (copy_to_user(buf, test_dev->kbuf, strlen(test_dev->kbuf)) != 0) // copy_to_user:内核空间向用户空间传数据{printk("copy_to_user error\r\n");return -1;}printk("This is cdev_test_read\r\n");return 0;
}static int cdev_test_release(struct inode *inode, struct file *file)
{printk("This is cdev_test_release\r\n");return 0;
}/*设备操作函数,定义file_operations结构体类型的变量cdev_test_fops*/
struct file_operations cdev_test_fops = {.owner = THIS_MODULE, //将owner字段指向本模块,可以避免在模块的操作正在被使用时卸载该模块.open = cdev_test_open, //将open字段指向chrdev_open(...)函数.read = cdev_test_read, //将open字段指向chrdev_read(...)函数.write = cdev_test_write, //将open字段指向chrdev_write(...)函数.release = cdev_test_release, //将open字段指向chrdev_release(...)函数
};static int __init chr_fops_init(void) //驱动入口函数
{/*注册字符设备驱动*/int ret;/*1 创建设备号,,这里注册2个设备号*/ret = alloc_chrdev_region(&dev1.dev_num, 0, 2, "alloc_name"); //动态分配设备号if (ret < 0){printk("alloc_chrdev_region is error\n");}printk("alloc_chrdev_region is ok\n");dev1.major = MAJOR(dev1.dev_num); //获取主设备号dev1.minor = MINOR(dev1.dev_num); //获取次设备号printk("major is %d \r\n", dev1.major); //打印主设备号printk("minor is %d \r\n", dev1.minor); //打印次设备号//对设备1进行操作/*2 初始化cdev*/dev1.cdev_test.owner = THIS_MODULE;cdev_init(&dev1.cdev_test, &cdev_test_fops);/*3 添加一个cdev,完成字符设备注册到内核*/cdev_add(&dev1.cdev_test, dev1.dev_num, 1);/*4 创建类*/dev1.class = class_create(THIS_MODULE, "test1");/*5 创建设备*/dev1.device = device_create(dev1.class, NULL, dev1.dev_num, NULL, "test1");dev2.major = MAJOR(dev1.dev_num + 1); //获取主设备号dev2.minor = MINOR(dev1.dev_num + 1); //获取次设备号printk("major is %d \r\n", dev2.major); //打印主设备号printk("minor is %d \r\n", dev2.minor); //打印次设备号//对设备2进行操作/*2 初始化cdev*/dev2.cdev_test.owner = THIS_MODULE;cdev_init(&dev2.cdev_test, &cdev_test_fops);/*3 添加一个cdev,完成字符设备注册到内核*/cdev_add(&dev2.cdev_test, dev1.dev_num + 1, 1);/*4 创建类*/dev2.class = class_create(THIS_MODULE, "test2");/*5  创建设备*/dev2.device = device_create(dev2.class, NULL, dev1.dev_num + 1, NULL, "test2");return 0;
}static void __exit chr_fops_exit(void) //驱动出口函数
{/*注销字符设备*/unregister_chrdev_region(dev1.dev_num, 1);    //注销设备号unregister_chrdev_region(dev1.dev_num + 1, 1);  //注销设备号cdev_del(&dev1.cdev_test);                  //删除cdevcdev_del(&dev2.cdev_test);                  //删除cdevdevice_destroy(dev1.class, dev1.dev_num);     //删除设备device_destroy(dev2.class, dev1.dev_num + 1);  //删除设备class_destroy(dev1.class);                 //删除类class_destroy(dev2.class);                 //删除类}
module_init(chr_fops_init);
module_exit(chr_fops_exit);
MODULE_LICENSE("GPL v2");
MODULE_AUTHOR("topeet");

app.c

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>int main(int argc, char *argv[])
{int fd1;  //定义设备1的文件描述符int fd2;  //定义设备2的文件描述符char buf1[32] = "nihao /dev/test1";   //定义写入缓存区buf1char buf2[32] = "nihao /dev/test2";   //定义写入缓存区buf2fd1 = open("/dev/test1", O_RDWR);  //打开设备1:test1if (fd1 < 0){perror("open error \n");return fd1;}write(fd1,buf1,sizeof(buf1));  //向设备1写入数据close(fd1); //取消文件描述符到文件的映射fd2= open("/dev/test2", O_RDWR); //打开设备2:test2if (fd2 < 0){perror("open error \n");return fd2;}write(fd2,buf2,sizeof(buf2));  //向设备2写入数据close(fd2);   //取消文件描述符到文件的映射return 0;
}

Linux错误处理实验

即使是最简单的注册字符设备,也存在注册失败的可能性,因此在之前编写的驱动代码中采用检查函数返回值的方式,确认函数是否成功执行,采用goto语句对Linux错误处理进行更进一步的处理

goto语句

在编写驱动程序时,驱动程序应该提供函数执行失败后处理的能力。如果驱动程序中函数执行失败了,必须取消掉所有失败前的注册,否则内核会处于一个不稳定的状态,因为它包含了不存在代码的内部指针。在处理Linux错误时,最好使用goto语句,goto语句的使用示例如下所示:

int   init my_init_function(void)
{
int err;
err = register_this(ptr1, "skull"); 
if (err)goto fail_this;err = register_that(ptr2, "skull");
if (err)goto fail_that;err = register_those(ptr3, "skull");
if (err)goto fail_those;return 0;        fail_those:unregister_that(ptr2, "skull"); 
fail_that:unregister_this(ptr1, "skull");
fail_this:return err;      
}

驱动代码:

#include <linux/init.h>
#include <linux/module.h>
#include <linux/kdev_t.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>struct device_test{dev_t dev_num;  //设备号int major ;  //主设备号int minor ;  //次设备号struct cdev cdev_test; // cdevstruct class *class;   //类struct device *device; //设备char kbuf[32];  //定义缓存区kbuf
};struct  device_test dev1;   //定义一个device_test结构体变量/*打开设备函数*/
static int cdev_test_open(struct inode *inode, struct file *file)
{file->private_data=&dev1;  //设置私有数据printk("This is cdev_test_open\r\n");return 0;
}/*向设备写入数据函数*/
static ssize_t cdev_test_write(struct file *file, const char __user *buf, size_t size, loff_t *off)
{struct device_test *test_dev=(struct device_test *)file->private_data;if (copy_from_user(test_dev->kbuf, buf, size) != 0) // copy_from_user:用户空间向内核空间传数据{printk("copy_from_user error\r\n");return -1;}printk("This is cdev_test_write\r\n");printk("kbuf is %s\r\n", test_dev->kbuf);return 0;
}/**从设备读取数据*/
static ssize_t cdev_test_read(struct file *file, char __user *buf, size_t size, loff_t *off)
{    struct device_test *test_dev=(struct device_test *)file->private_data;   
if (copy_to_user(buf, test_dev->kbuf, strlen( test_dev->kbuf)) != 0) // copy_to_user:内核空间向用户空间传数据{printk("copy_to_user error\r\n");return -1;}printk("This is cdev_test_read\r\n");return 0;
}static int cdev_test_release(struct inode *inode, struct file *file)
{printk("This is cdev_test_release\r\n");return 0;
}/*设备操作函数*/
struct file_operations cdev_test_fops = {.owner = THIS_MODULE, //将owner字段指向本模块,可以避免在模块的操作正在被使用时卸载该模块.open = cdev_test_open, //将open字段指向chrdev_open(...)函数.read = cdev_test_read, //将open字段指向chrdev_read(...)函数.write = cdev_test_write, //将open字段指向chrdev_write(...)函数.release = cdev_test_release, //将open字段指向chrdev_release(...)函数
};static int __init chr_fops_init(void) //驱动入口函数
{/*注册字符设备驱动*/int ret;/*1 创建设备号*/ret = alloc_chrdev_region(&dev1.dev_num, 0, 1, "alloc_name"); //动态分配设备号if (ret < 0){goto err_chrdev;}printk("alloc_chrdev_region is ok\n");dev1.major = MAJOR(dev1.dev_num); //获取主设备号dev1.minor = MINOR(dev1.dev_num); //获取次设备号printk("major is %d \r\n", dev1.major); //打印主设备号printk("minor is %d \r\n", dev1.minor); //打印次设备号/*2 初始化cdev*/dev1.cdev_test.owner = THIS_MODULE;cdev_init(&dev1.cdev_test, &cdev_test_fops);/*3 添加一个cdev,完成字符设备注册到内核*/ret =  cdev_add(&dev1.cdev_test, dev1.dev_num, 1);if(ret<0){goto  err_chr_add;}/*4 创建类*/dev1. class = class_create(THIS_MODULE, "test");if(IS_ERR(dev1.class)){ret=PTR_ERR(dev1.class);goto err_class_create;}/*5创建设备*/dev1.device = device_create(dev1.class, NULL, dev1.dev_num, NULL, "test");if(IS_ERR(dev1.device)){ret=PTR_ERR(dev1.device);goto err_device_create;}return 0;err_device_create:class_destroy(dev1.class);                 //删除类err_class_create:cdev_del(&dev1.cdev_test);                 //删除cdeverr_chr_add:unregister_chrdev_region(dev1.dev_num, 1); //注销设备号err_chrdev:return ret;
}static void __exit chr_fops_exit(void) //驱动出口函数
{/*注销字符设备*/unregister_chrdev_region(dev1.dev_num, 1); //注销设备号cdev_del(&dev1.cdev_test);                 //删除cdevdevice_destroy(dev1.class, dev1.dev_num);       //删除设备class_destroy(dev1.class);                 //删除类
}
module_init(chr_fops_init);
module_exit(chr_fops_exit);
MODULE_LICENSE("GPL v2");
MODULE_AUTHOR("topeet");
http://www.dtcms.com/a/313506.html

相关文章:

  • 【canvas】
  • 从WebShell 与 ShellCode 免杀技术 打造适合自己的免杀技术链
  • 设计模式 - 组合模式:用树形结构处理对象之间的复杂关系
  • 攻防世界-web-csaw-mfw
  • 【C++】封装,this指针
  • C++高阶笔记第四篇:STL-函数对象
  • 【Leetcode】2106. 摘水果
  • Yakit热加载魔术方法模版插件语法JSRpc进阶调用接口联动
  • 【Bluedroid】btif_av_handle_event 流程源码解析
  • 更换KR100门禁读头&主机
  • UART串口常用库函数(STC8系列)
  • LLM大模型开发-SpringAI:ChatClient、Ollama、Advisor
  • greenfoot主要api
  • 广东省省考备考(第六十五天8.3)——判断推理:图形推理(数量规律题目总结)
  • 使用C++实现日志(3)
  • sqli-labs:Less-23关卡详细解析
  • C的数据类型与变量
  • 2025 Java开发真实试题-阿里面试题分析
  • C语言与数据结构:从基础到实战
  • 机器学习——过采样(OverSampling),解决类别不平衡问题,案例:逻辑回归 信用卡欺诈检测
  • 前端工程化:Vue3(一)
  • 2025年EAAI SCI1区TOP,森林救援调度与路径规划:一种新型蚁群优化算法应用,深度解析+性能实测
  • 智能化门禁常见问题处理思路
  • Linux mount挂载选项详解(重点关注nosuid)
  • 使用Perl和库WWW::Curl的爬虫程序!
  • [spring-cloud: 服务注册]-源码解析
  • Spring Boot AOP 优雅实现异常重试机制
  • 多线程异步日志系统与实现及 TCP/IP C/S 模型
  • IO流-字节流-FileOutputStream
  • day50预训练模型 CBAM注意力