Linux block_device gendisk和hd_struct到底是个啥关系

本文的源码版本是Linux 5.15版本,有图有真相:

1.先从块设备驱动说起

安卓平台有一个非常典型和重要的块设备驱动:zram,我们来看一下zram这个块设备驱动加载初始化和swapon的逻辑,完整梳理完这个逻辑将对Linux块设备驱动模型有深入的理解。

zram驱动加载的时候会调用zram_add函数,源码如下:

1887/*
1888 * Allocate and initialize new zram device. the function returns
1889 * '>= 0' device_id upon success, and negative value otherwise.
1890 */
1891static int zram_add(void)
1892{
1893	struct zram *zram;
1894	int ret, device_id;
1895
1896	zram = kzalloc(sizeof(struct zram), GFP_KERNEL);
1909    ...
1910	/* gendisk structure */
1911	zram->disk = blk_alloc_disk(NUMA_NO_NODE);
1
1918
1919	zram->disk->major = zram_major;
1920	zram->disk->first_minor = device_id;
1921	zram->disk->minors = 1;
1922	zram->disk->fops = &zram_devops;
1923	zram->disk->private_data = zram;
1924	snprintf(zram->disk->disk_name, 16, "zram%d", device_id);
1925     ...
1957	device_add_disk(NULL, zram->disk, zram_disk_attr_groups);
1961	zram_debugfs_register(zram);
1962	pr_info("Added device: %s\n", zram->disk->disk_name);
1963	return device_id;...
1970}

zram_add中有两个非常重要的函数:

  • blk_alloc_disk
  • device_add_disk

上面两个函数描述了块设备驱动的两个步骤:1)创建gendisk对象,代表的是一个“磁盘” 2)注册和激活磁盘,激活磁盘之后就可以正式使用了。

2. gendisk和hd_struct是啥

blk_alloc_disk函数创建了一个gendisk对象,这就出现了本文要讲述的非常重要的对象。怎么理解gendisk呢?Linux用gendisk代表一个“磁盘”,这里的磁盘可以是一个真实的硬盘,也可以是一个虚拟设备。

我们接触windows系统比较多,硬盘都会划分分区,在Linux是不是也有同样的概念呢?确实如此,这就要去struct gendisk数据结构来一探究竟了:

121struct gendisk {
122	/* major, first_minor and minors are input parameters only,
123	 * don't use directly.  Use disk_devt() and disk_max_parts().
124	 */
125	int major;			/* major number of driver */
126	int first_minor;
127	int minors;                     /* maximum number of minors, =1 for
128                                         * disks that can't be partitioned. */
129
130	char disk_name[DISK_NAME_LEN];	/* name of major driver */
131
132	unsigned short events;		/* supported events */
133	unsigned short event_flags;	/* flags related to event processing */
134
135	struct xarray part_tbl;
136	struct block_device *part0;
137
138	const struct block_device_operations *fops;
139	struct request_queue *queue;...}

gendisk中的part_tbl就代表该磁盘的分区表,那么每个分区用什么结构体表示:struct hd_struct

注意gendisk结构体中还有一个重要的fops函数,代表了操作该块设备的操作函数列表,具体本文后面详细讲述。

3. gendisk是怎么创建的

前面知道gendisk是通过blk_alloc_disk函数创建的:

275#define blk_alloc_disk(node_id)						\
276({									\
277	static struct lock_class_key __key;				\
278									\
279	__blk_alloc_disk(node_id, &__key);				\
280})1333struct gendisk *__blk_alloc_disk(int node, struct lock_class_key *lkclass)
1334{
1335	struct request_queue *q;
1336	struct gendisk *disk;
1337
1338	q = blk_alloc_queue(node);
1339	if (!q)
1340		return NULL;
1341
1342	disk = __alloc_disk_node(q, node, lkclass);
1343	if (!disk) {
1344		blk_cleanup_queue(q);
1345		return NULL;
1346	}
1347	return disk;
1348}1279
1280struct gendisk *__alloc_disk_node(struct request_queue *q, int node_id,
1281		struct lock_class_key *lkclass)
1282{
1283	struct gendisk *disk;
1284
1285	if (!blk_get_queue(q))
1286		return NULL;
1287
1288	disk = kzalloc_node(sizeof(struct gendisk), GFP_KERNEL, node_id);
1289	if (!disk)
1290		goto out_put_queue;
1291
1292	disk->bdi = bdi_alloc(node_id);
1293	if (!disk->bdi)
1294		goto out_free_disk;
1295
1296	disk->part0 = bdev_alloc(disk, 0);
1297	if (!disk->part0)
1298		goto out_free_bdi;
1299
1300	disk->node_id = node_id;
1301	mutex_init(&disk->open_mutex);
1302	xa_init(&disk->part_tbl);
1303	if (xa_insert(&disk->part_tbl, 0, disk->part0, GFP_KERNEL))
1304		goto out_destroy_part_tbl;
1305

总结一下调用关系:blk_alloc_disk->__alloc_disk_node->__alloc_disk_node,最终__alloc_disk_node创建了gendisk对象。

创建了gendisk之后,要给gendisk做一些初始化赋值,其中很重要的part0是block_device,通过调用bdev_alloc(disk,0)创建,这里出现了本文最后一个要介绍的对象:struct block_device,

4 block_device是啥

block_device具体可以对应一个磁盘,也可以对应磁盘里面的一个分区,也就说磁盘和磁盘都可用block_device表示,block_device可以想象成磁盘的描述信息,比如设备号,分区号,是否只读等等,具体定义如下:

4struct block_device {
25	sector_t		bd_start_sect;
26	struct disk_stats __percpu *bd_stats;
27	unsigned long		bd_stamp;
28	bool			bd_read_only;	/* read-only policy */
29	dev_t			bd_dev;
30	int			bd_openers;
31	struct inode *		bd_inode;	/* will die */
32	struct super_block *	bd_super;
33	void *			bd_claiming;
34	struct device		bd_device;
35	void *			bd_holder;
36	int			bd_holders;
37	bool			bd_write_holder;
38	struct kobject		*bd_holder_dir;
39	u8			bd_partno;
40	spinlock_t		bd_size_lock; /* for bd_inode->i_size updates */
41	struct gendisk *	bd_disk;
42
43	/* The counter of freeze processes */
44	int			bd_fsfreeze_count;
45	/* Mutex for freeze */
46	struct mutex		bd_fsfreeze_mutex;
47	struct super_block	*bd_fsfreeze_sb;
48
49	struct partition_meta_info *bd_meta_info;
50#ifdef CONFIG_FAIL_MAKE_REQUEST
51	bool			bd_make_it_fail;
52#endif
53
54	ANDROID_KABI_RESERVE(1);
55	ANDROID_KABI_RESERVE(2);
56	ANDROID_KABI_RESERVE(3);
57	ANDROID_KABI_RESERVE(4);
58} __randomize_layout;

5.block_device怎么创建的

bdev_alloc创建了disk->part0这个block_device对象,我们来看下非常重要的bdev_alloc函数:

478struct block_device *bdev_alloc(struct gendisk *disk, u8 partno)
479{
480	struct block_device *bdev;
481	struct inode *inode;
482
483	inode = new_inode(blockdev_superblock);
484	if (!inode)
485		return NULL;//块设备文件对应inode设置为块设备
486	inode->i_mode = S_IFBLK;
487	inode->i_rdev = 0;
488	inode->i_data.a_ops = &def_blk_aops;
489	mapping_set_gfp_mask(&inode->i_data, GFP_USER);
490//new_inode创建的本质上是bdev_inode,I_BDEV获取bdev_inode结构体的字段bdev
491	bdev = I_BDEV(inode);
492	mutex_init(&bdev->bd_fsfreeze_mutex);
493	spin_lock_init(&bdev->bd_size_lock);//初始化block_device,设置分区号,inode,gendisk对象
494	bdev->bd_partno = partno;
495	bdev->bd_inode = inode;
496	bdev->bd_stats = alloc_percpu(struct disk_stats);
497	if (!bdev->bd_stats) {
498		iput(inode);
499		return NULL;
500	}
501	bdev->bd_disk = disk;
502	return bdev;
503}

上面new_inode函数调用可以参考:zram压缩机制看swapon系统调用_swapon设置为zram-CSDN博客

总结来讲new_inode返回的本质上是一个bdev_inode对象,其定义如下:

32struct bdev_inode {
33	struct block_device bdev;
34	struct inode vfs_inode;
35};
36

也就是说new_inode创建bdev_inode的同时,本质上也创建了一个block_device对象。这里bdev_inode就代表块设备文件的inode,比如zram驱动来讲,对应的就是/dev/block/zram0块设备文件的inode对象。

6. 激活磁盘

激活磁盘使用的是device_add_disk函数:

/*** device_add_disk - add disk information to kernel list* @parent: parent device for the disk* @disk: per-device partitioning information* @groups: Additional per-device sysfs groups** This function registers the partitioning information in @disk* with the kernel.*/
int device_add_disk(struct device *parent, struct gendisk *disk,const struct attribute_group **groups){struct device *ddev = disk_to_dev(disk);int ret;/** The disk queue should now be all set with enough information about* the device for the elevator code to pick an adequate default* elevator if one is needed, that is, for devices requesting queue* registration.*/elevator_init_mq(disk->queue);/** If the driver provides an explicit major number it also must provide* the number of minors numbers supported, and those will be used to* setup the gendisk.* Otherwise just allocate the device numbers for both the whole device* and all partitions from the extended dev_t space.*/if (disk->major) {if (WARN_ON(!disk->minors))return -EINVAL;if (disk->minors > DISK_MAX_PARTS) {pr_err("block: can't allocate more than %d partitions\n",DISK_MAX_PARTS);disk->minors = DISK_MAX_PARTS;}if (disk->first_minor > MINORMASK ||disk->minors > MINORMASK + 1 ||disk->first_minor + disk->minors > MINORMASK + 1)return -EINVAL;} else {if (WARN_ON(disk->minors))return -EINVAL;ret = blk_alloc_ext_minor();if (ret < 0)return ret;disk->major = BLOCK_EXT_MAJOR;disk->first_minor = ret;disk->flags |= GENHD_FL_EXT_DEVT;}/* delay uevents, until we scanned partition table */dev_set_uevent_suppress(ddev, 1);ddev->parent = parent;ddev->groups = groups;dev_set_name(ddev, "%s", disk->disk_name);if (!(disk->flags & GENHD_FL_HIDDEN))ddev->devt = MKDEV(disk->major, disk->first_minor);//非常重要的函数ret = device_add(ddev);if (ret)goto out_free_ext_minor;ret = disk_alloc_events(disk);if (ret)goto out_device_del;if (!sysfs_deprecated) {ret = sysfs_create_link(block_depr, &ddev->kobj,kobject_name(&ddev->kobj));if (ret)goto out_device_del;}/** avoid probable deadlock caused by allocating memory with* GFP_KERNEL in runtime_resume callback of its all ancestor* devices*/pm_runtime_set_memalloc_noio(ddev, true);ret = blk_integrity_add(disk);if (ret)goto out_del_block_link;disk->part0->bd_holder_dir =kobject_create_and_add("holders", &ddev->kobj);if (!disk->part0->bd_holder_dir) {ret = -ENOMEM;goto out_del_integrity;}disk->slave_dir = kobject_create_and_add("slaves", &ddev->kobj);if (!disk->slave_dir) {ret = -ENOMEM;goto out_put_holder_dir;}ret = bd_register_pending_holders(disk);if (ret < 0)goto out_put_slave_dir;ret = blk_register_queue(disk);if (ret)goto out_put_slave_dir;if (disk->flags & GENHD_FL_HIDDEN) {/** Don't let hidden disks show up in /proc/partitions,* and don't bother scanning for partitions either.*/disk->flags |= GENHD_FL_SUPPRESS_PARTITION_INFO;disk->flags |= GENHD_FL_NO_PART;} else {ret = bdi_register(disk->bdi, "%u:%u",disk->major, disk->first_minor);if (ret)goto out_unregister_queue;bdi_set_owner(disk->bdi, ddev);ret = sysfs_create_link(&ddev->kobj,&disk->bdi->dev->kobj, "bdi");if (ret)goto out_unregister_bdi;//非常重要bdev_add(disk->part0, ddev->devt);disk_scan_partitions(disk);/** Announce the disk and partitions after all partitions are* created. (for hidden disks uevents remain suppressed forever)*/dev_set_uevent_suppress(ddev, 0);disk_uevent(disk, KOBJ_ADD);}disk_update_readahead(disk);disk_add_events(disk);return 0;...
}

 总结起来device_add_disk调用了两个非常重要的函数:

  1. device_add
  2. bdev_add
device_add函数
device_add--->devtmpfs_create_node

devtmpfs会给devtmpfs文件系统的线程发送创建块文件的消息,类似mknode,然后再/dev/目录下创建出来块文件。 

bdev_add函数:
void bdev_add(struct block_device *bdev, dev_t dev)
{bdev->bd_dev = dev;bdev->bd_inode->i_rdev = dev;bdev->bd_inode->i_ino = dev;insert_inode_hash(bdev->bd_inode);
}

设置block_device->device的bd_dev为块设备号,同时设置block_device->bd_inode的i_rdev为块设备号,同时insert_inode_hash函数 block_device的bd_inode添加到superblock的inode hash表中,这里逻辑非常重要,对理解swapon非常重要,我们知道swapon系统调用有如下一段代码:

	swapon系统调用:swap_file = file_open_name(name, O_RDWR|O_LARGEFILE, 0);if (IS_ERR(swap_file)) {error = PTR_ERR(swap_file);swap_file = NULL;goto bad_swap;}p->swap_file = swap_file;mapping = swap_file->f_mapping;dentry = swap_file->f_path.dentry;inode = mapping->host;static int claim_swapfile(struct swap_info_struct *p, struct inode *inode)
{int error;if (S_ISBLK(inode->i_mode)) {p->bdev = blkdev_get_by_dev(inode->i_rdev,FMODE_READ | FMODE_WRITE | FMODE_EXCL, p);if (IS_ERR(p->bdev)) {error = PTR_ERR(p->bdev);p->bdev = NULL;return error;}p->old_block_size = block_size(p->bdev);error = set_blocksize(p->bdev, PAGE_SIZE);if (error < 0)return error;/** Zoned block devices contain zones that have a sequential* write only restriction.  Hence zoned block devices are not* suitable for swapping.  Disallow them here.*/if (blk_queue_is_zoned(p->bdev->bd_disk->queue))return -EINVAL;p->flags |= SWP_BLKDEV;} else if (S_ISREG(inode->i_mode)) {p->bdev = inode->i_sb->s_bdev;}return 0;
}

blkdev_get_by_dev函数返回了一个block_device,这个block_device跟前面zram块驱动blk_alloc_disk 生成的block_device有啥关系?就是同一个,我们还是要从源码视角看懂这一切:

struct block_device *blkdev_get_by_dev(dev_t dev, fmode_t mode, void *holder)
{bool unblock_events = true;struct block_device *bdev;struct gendisk *disk;int ret;ret = devcgroup_check_permission(DEVCG_DEV_BLOCK,MAJOR(dev), MINOR(dev),((mode & FMODE_READ) ? DEVCG_ACC_READ : 0) |((mode & FMODE_WRITE) ? DEVCG_ACC_WRITE : 0));if (ret)return ERR_PTR(ret);bdev = blkdev_get_no_open(dev);if (!bdev)return ERR_PTR(-ENXIO);disk = bdev->bd_disk;...return bdev;
}
struct block_device *blkdev_get_no_open(dev_t dev)
{struct block_device *bdev;struct inode *inode;inode = ilookup(blockdev_superblock, dev);if (!inode) {blk_request_module(dev);inode = ilookup(blockdev_superblock, dev);if (!inode)return NULL;}/* switch from the inode reference to a device mode one: */bdev = &BDEV_I(inode)->bdev;if (!kobject_get_unless_zero(&bdev->bd_device.kobj))bdev = NULL;iput(inode);if (!bdev)return NULL;if ((bdev->bd_disk->flags & GENHD_FL_HIDDEN) ||!try_module_get(bdev->bd_disk->fops->owner)) {put_device(&bdev->bd_device);return NULL;}return bdev;
}

 inode = ilookup(blockdev_superblock, dev);根据块设备号dev,从blockdev_superblock拿到inode节点,为什么这里能拿到块设备文件(/dev/block/zram0)的inode,就是因为bdev_add时候将inode对象加入到blockdev_superblock的inode hash表中了,这里就能拿到。

参考文章:

块设备剖析之关键数据结构分析 - block_device/gendisk/hd_struct-下雨夜-ChinaUnix博客

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

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

相关文章

旅拍景区收银系统+押金原路退回+服装租赁-SAAS本地化及未来之窗行业应用跨平台架构

一、景区旅拍一体化系统 序号系统说明1提成系统用于给照相馆介绍照相拉客的人自动计算提成2押金系统用于服装租赁&#xff08;汉服租赁&#xff09;&#xff0c;设备租赁 &#xff0c;支持押金原路退回3收银系统计算每天收银汇总&#xff0c;月度收银汇总&#xff0c;支出4提成…

云原生之高性能web服务器学习(持续更新中)

高性能web服务器 1 Web服务器的基础介绍1.1 Web服务介绍1.1.1 Apache介绍1.1.2 Nginx-高性能的 Web 服务端 2 Nginx架构与安装2.1 Nginx概述2.1.1 Nginx 功能介绍2.1.2 基础特性2.1.3 Web 服务相关的功能 2.2 Nginx 架构和进程2.2.1 架构2.2.2 Ngnix进程结构 2.3 Nginx 模块介绍…

PyInstaller问题解决 onnxruntime-gpu 使用GPU和CUDA加速模型推理

前言 在模型推理时&#xff0c;需要使用GPU加速&#xff0c;相关的CUDA和CUDNN安装好后&#xff0c;通过onnxruntime-gpu实现。 直接运行python程序是正常使用GPU的&#xff0c;如果使用PyInstaller将.py文件打包为.exe&#xff0c;发现只能使用CPU推理了。 本文分析这个问题…

流媒体与直播的基础理论(其一)

欢迎诸位来阅读在下的博文~ 在这里&#xff0c;在下会不定期发表一些浅薄的知识和经验&#xff0c;望诸位能与在下多多交流&#xff0c;共同努力 文章目录 一、流媒体简介二、流媒体协议常见的流媒体协议 三、视频直播原理与流程通用的视频直播模型视频直播链路 一、流媒体简介…

隐私计算实训营:联邦学习在垂直场景的开发实践

纵向联邦学习 纵向联邦学习的参与方拥有相同样本空间、不同特征空间的数据&#xff0c;通过共有样本数据进行安全联合建模&#xff0c;在金融、广告等领域拥有广泛的应用场景。和横向联邦学习相比&#xff0c;纵向联邦学习的参与方之间需要协同完成数据求交集、模型联合训练和…

Openharmony 下载到rk3568实现横屏

前言&#xff1a; Openharmony 源码版本4.1 release 板子&#xff1a;rk3568 1.修改“abilities”中的“orientation”实现横竖屏 entyr->src->module.json5文件里面添加 "orientation": "landscape", 2.修改系统源码属性实现横竖屏切换 通过这…

以太网--TCP/IP协议(二)

上文中讲述了IP协议&#xff0c;本文主要来讲一下TCP协议。 TCP协议 &#xff08;1&#xff09;端到端通信 直接把源主机应用程序产生的数据传输到目的主机使用这 些数据的应用程序中&#xff0c;就是端到端通信。 &#xff08;2&#xff09;传输层端口 公认端口&#xff0…

ansible--role

简介 roles是ansible&#xff0c;playbooks的目录的组织结构&#xff0c;将代码或文件进行模块化&#xff0c;成为roles的文件目录组织结构。 易读&#xff0c;代码可冲哟美好&#xff0c;层次清晰 目录机构 mkdir roles/nginx/{files,handlers,tasks,templates,vars} -ptou…

Google Play结算防掉单方案

我们公司的产品主要是出海产品,使用的是Google Play支付,但是在上线以后,经常有客诉,说支付以后,权益没有到账,于是对整个Google支付体系做了研究了一下。 我们的整个支付流程图大概如下: 其中后端参考的文档地址为: https://developers.google.com/android-publishe…

GB35114 USC安防平台 中星微国密摄像机配置 流程

中星微国密摄像机配置介绍 如下以中星微VS-IPC8021S-Y-T4摄像机为例&#xff0c;需要先各自获取p10文件&#xff0c;并通过证书签发机构或者测试SM2证书签发获取证书。 网络配置如下: 摄像机的IP地址为192.168.1.108&#xff0c;国标ID为34020000001320000015 系统的IP地址…

C#/.NET/.NET Core推荐学习路线文档文章

前言 专门为C#/.NET/.NET Core推荐学习路线&文档&文章提供的一个Issues&#xff0c;各位小伙伴可以把自己觉得不错的学习路线、文档、文章相关地址分享出来&#x1f91e;。 https://github.com/YSGStudyHards/DotNetGuide/issues/10 &#x1f3f7;️C#/.NET/.NET Cor…

【LVI-SAM】激光雷达点云处理特征提取LIO-SAM 之FeatureExtraction实现细节

激光雷达点云处理特征提取LIO-SAM 之FeatureExtraction实现细节 1. 特征提取实现过程总结1.0 特征提取过程小结1.1 类 FeatureExtraction 的整体结构与作用1.2 详细特征提取的过程1. 平滑度计算&#xff08;calculateSmoothness()&#xff09;2. 标记遮挡点&#xff08;markOcc…

nvm及nodejs安装相关

安装 1.清空文件夹&#xff0c;卸载nvm及nodejs 2.下载安装包 https://github.com/coreybutler/nvm-windows/releases &#xff08;也下载有&#xff09; 3.安装nvm 地址写D:/nvm和D:/nodejs 4.安装nodejs nvm ls available //查询版本 nvm install 16.20.2 //安装对应版…

【H2O2|全栈】关于HTML(1)认识HTML

HTML相关知识 目录 前言 准备工作 WEB前端是什么&#xff1f; HTML是什么&#xff1f; 如何运行HTML文件&#xff1f; 标签 概念 分类 双标签和单标签 行内标签和块标签 HTML文档结构 预告和回顾 UI设计相关 Markdown | Md文档相关 项目合作管理相关 后话 前…

idea快捷键_idea 2024 复制光标所在行 或 复制选择内容,并把复制内容插入光标位置下面 (必备)

Ctrl G 在当前文件跳转到指定行处 Ctrl J 插入自定义动态代码模板 &#xff08;必备&#xff09; Ctrl P 方法参数提示显示 &#xff08;必备&#xff09; Ctrl Q 光标所在的变量 / 类名 / 方法名等上面&#xff08;也可以在提示补充的时候按&#xff09;&#xff0c;显示文…

基于SpringBoot的校园跑腿系统+LW参考示例

系列文章目录 1.基于SSM的洗衣房管理系统原生微信小程序LW参考示例 2.基于SpringBoot的宠物摄影网站管理系统LW参考示例 3.基于SpringBootVue的企业人事管理系统LW参考示例 4.基于SSM的高校实验室管理系统LW参考示例 5.基于SpringBoot的二手数码回收系统原生微信小程序LW参考示…

tabBar设置底部菜单以及选项iconfont图标

1.tabBar设置底部菜单 在官网里面可以了解到tabBar组件的一些知识和注意点&#xff1a; 需要给页面设置一个底部导航的话可以在pages.json里面设置一个tabBar标签&#xff0c;在其里面设置pagePath和text属性。 可以看的pagePath是跳转的地址&#xff0c;text是下面导航的文字…

DataLoader使用

文章目录 一、认识dataloader二、DataLoader整合数据集三、使用DataLoader展示图片方法四、去除结尾不满足batch_size设值图片的展示 一、认识dataloader DataLoader 用于封装数据集&#xff0c;并提供批量加载数据的迭代器。它支持自动打乱数据、多线程数据加载等功能。datas…

SpringDataJPA系列(7)Jackson注解在实体中应用

SpringDataJPA系列(7)Jackson注解在实体中应用 常用的Jackson注解 Springboot中默认集成的是Jackson&#xff0c;我们可以在jackson依赖包下看到Jackson有多个注解 一般常用的有下面这些&#xff1a; 一个实体的示例 测试方法如下&#xff1a; 按照上述图片中的序号做个简…

汽车网络安全的未来:将车辆视为端点

汽车行业面临着许多与其他行业的成功企业相同的网络安全风险和威胁&#xff0c;但它也在应对一些独特的风险和威胁。 Nuspire 的首席威胁分析师 Josh Smith&#xff08;一家在汽车领域有着深厚根基并保护通用汽车和斯巴鲁等客户的托管安全服务提供商&#xff09;谈到了当前的风…