块设备是针对存储设备的,比如 SD 卡、EMMC、NAND Flash、Nor Flash、SPI Flash、机械硬盘、固态硬盘等。因此块设备驱动其实就是这些存储设备驱动,块设备驱动相比字符设备驱动的主要区别如下:
①、块设备只能以块为单位进行读写访问,块是 linux 虚拟文件系统(VFS)基本的数据传输单位。字符设备是以字节为单位进行数据传输的,不需要缓冲。
②、块设备在结构上是可以进行随机访问的,对于这些设备的读写都是按块进行的,块设备使用缓冲区来暂时存放数据,等到条件成熟以后在一次性将缓冲区中的数据写入块设备中。这么做的目的为了提高块设备寿命,大家如果仔细观察的话就会发现有些硬盘或者 NAND Flash就会标明擦除次数(flash 的特性,写之前要先擦除),比如擦除 100000 次等。因此,为了提高块设备寿命而引入了缓冲区,数据先写入到缓冲区中,等满足一定条件后再一次性写入到真正的物理存储设备中,这样就减少了对块设备的擦除次数,提高了块设备寿命.
字符设备是顺序的数据流设备,字符设备是按照字节进行读写访问的。字符设备不需要缓冲区,对于字符设备的访问都是实时的,而且也不需要按照固定的块大小进行访问。
3.块设备结构的不同其 I/O 算法也会不同.比如对于 EMMC、SD 卡、NAND Flash 这类没有任何机械设备的存储设备就可以任意读写任何的扇区(块设备物理存储单元)。。但是对于机械硬盘这样带有磁头的设备,读取不同的盘面或者磁道里面的数据,磁头都需要进行移动,因此对于机械硬盘而言,将那些杂乱的访问按照一定的顺序进行排列可以有效提高磁盘性能,linux 里面针对不同的存储设备实现了不同的 I/O 调度算法.
Linux内核使用block_device(结构体)表示块设备,定义include/linux/fs.h 文件中,可以理解为一个对象。
struct block_device {
dev_t bd_dev; /* not a kdev_t - it's a search key */
int bd_openers;
struct inode * bd_inode; /* will die */
struct super_block * bd_super;
struct mutex bd_mutex; /* open/close mutex */
struct list_head bd_inodes;
void * bd_claiming;
void * bd_holder;
int bd_holders;
bool bd_write_holder;
#ifdef CONFIG_SYSFS
struct list_head bd_holder_disks;
#endif
struct block_device * bd_contains;
unsigned bd_block_size;
struct hd_struct * bd_part;
/* number of times partitions within this device have been opened. */
unsigned bd_part_count;
int bd_invalidated;
struct gendisk * bd_disk;
struct request_queue * bd_queue;
struct list_head bd_list;
/*
* Private data. You must have bd_claim'ed the block_device
* to use this. NOTE: bd_claim allows an owner to claim
* the same device multiple times, the owner must take special
* care to not mess up bd_private for that case.
*/
unsigned long bd_private;
/* The counter of freeze processes */
int bd_fsfreeze_count;
/* Mutex for freeze */
struct mutex bd_fsfreeze_mutex;
};
其中 struct gendisk * bd_disk;表示的是硬盘这个块设备对象。
注册块设备
和字符设备驱动一样,我们需要向内核注册新的块设备、申请设备号,块设备注册函数为
register_blkdev
/*
major:主设备号
name:块设备名字。
return: 0 成功 负数 失败
*/
int register_blkdev(unsigned int major, const char *name)
注销块设备
void unregister_blkdev(unsigned int major, const char *name)
定义在 include/linux/genhd.h中
struct gendisk {
/* major, first_minor and minors are input parameters only,
* don't use directly. Use disk_devt() and disk_max_parts().
*/
int major; /* major number of driver */
int first_minor; /*第一个次设备号*/
int minors; /* maximum number of minors, =1 for
* disks that can't be partitioned. */
char disk_name[DISK_NAME_LEN]; /* name of major driver */
char *(*devnode)(struct gendisk *gd, umode_t *mode);
unsigned int events; /* supported events */
unsigned int async_events; /* async events, subset of all */
/* Array of pointers to partitions indexed by partno.
* Protected with matching bdev lock but stat and other
* non-critical accesses use RCU. Always access through
* helpers.
*/
struct disk_part_tbl __rcu *part_tbl; /*磁盘分区表*/
struct hd_struct part0;
const struct block_device_operations *fops;/*设备操作集*/
struct request_queue *queue; /*磁盘对应的请求队列*/
void *private_data;
int flags;
struct device *driverfs_dev; // FIXME: remove
struct kobject *slave_dir;
struct timer_rand_state *random;
atomic_t sync_io; /* RAID */
struct disk_events *ev;
#ifdef CONFIG_BLK_DEV_INTEGRITY
struct blk_integrity *integrity;
#endif
int node_id;
};
申请gendisk设备
//minors:次设备号数量,也就是 gendisk 对应的分区数量
struct gendisk *alloc_disk(int minors)
删除 gendisk
void del_gendisk(struct gendisk *gp)
将 gendisk 添加到内核
使用 alloc_disk 申请到 gendisk 以后系统还不能使用,必须使用 add_disk 函数将申请到的
gendisk 添加到内核中
void add_disk(struct gendisk *disk)
设置 gendisk 容量
每一个磁盘都有容量,所以在初始化 gendisk 的时候也需要设置其容量,使用函数
set_capacity
void set_capacity(struct gendisk *disk, sector_t size)
size:磁盘容量大小,注意这里是扇区数量。块设备中最小的可寻址单元是扇区,一个扇区
一般是 512 字节。所以 set_capacity 函数设置的大小就是块设备实际容量除以512 字节得到的扇区数量。比如一个 2MB 的磁盘,其扇区数量就是(2*1024*1024)(字节)/512=4096(扇区)。
调整 gendisk 引用计数
内核会通过 get_disk 和 put_disk 这两个函数来调整 gendisk 的引用计数,根据名字就可以
知道,get_disk 是增加 gendisk 的引用计数,put_disk 是减少 gendisk 的引用计数
truct kobject *get_disk(struct gendisk *disk)
void put_disk(struct gendisk *disk)
和字符设备的 file _operations 一样,块设备也有操作集,为结构体 block_device_operations,
此结构体定义在 include/linux/blkdev.h 中
struct block_device_operations {
int (*open) (struct block_device *, fmode_t);
void (*release) (struct gendisk *, fmode_t);
int (*rw_page)(struct block_device *, sector_t, struct page *, int rw);
int (*ioctl) (struct block_device *, fmode_t, unsigned, unsigned long);
int (*compat_ioctl) (struct block_device *, fmode_t, unsigned, unsigned long);
long (*direct_access)(struct block_device *, sector_t,
void **, unsigned long *pfn, long size);
unsigned int (*check_events) (struct gendisk *disk,
unsigned int clearing);
/* ->media_changed() is DEPRECATED, use ->check_events() instead */
int (*media_changed) (struct gendisk *);
void (*unlock_native_capacity) (struct gendisk *);
int (*revalidate_disk) (struct gendisk *);
int (*getgeo)(struct block_device *, struct hd_geometry *);
/* this callback is with swap_lock and sometimes page table lock held */
void (*swap_slot_free_notify) (struct block_device *, unsigned long);
struct module *owner;
};
和字符设备操作函数类似,区别的地方:
仔细观察的话会在 block_device_operations 结构体中并没有找到 read 和 write 这样
的读写函数,那么块设备是怎么从物理块设备中读写数据?
块设备驱动中非常重要的 request_queue、request 和 bio
内核将对块设备的读写都发送到请求队列 request_queue 中,request_queue 中是大量request(请求结构体),而 request 又包含了 bio,bio 保存了读写相关数据,比如从块设备的哪个地址开始读取、读取的数据长度,读取到哪里,如果是写的话还包括要写入的数据等。在gendisk结构体中就有该队列成员。
在编写块设备驱动的时候,每个磁盘(gendisk)都要分配一个 request_queue。
首先需要申请并初始化一个 request_queue,然后在初始化 gendisk 的时候将这个
request_queue 地址赋值给 gendisk 的 queue 成员变量。
/*
* 申请与初始化队列
*/
request_queue *blk_init_queue(request_fn_proc *rfn, spinlock_t *lock)
参数含义:
rfn:请求处理函数指针,每个 request_queue 都要有一个请求处理函数,请求处理函request_fn_proc 原型如下:(需要驱动人员自行实现)
void (request_fn_proc) (struct request_queue *q)
lock:自旋锁指针,需要驱动编写人员定义一个自旋锁,然后传递进来。请求队列会使用这个自旋锁。
返回值:如果为 NULL 的话表示失败,成功的话就返回申请到的 request_queue 地址。
当卸载块设备驱动的时候我们还需要删除掉前面申请到的 request_queue,删除请求队列使
用函数 blk_cleanup_queue,函数原型如下:
void blk_cleanup_queue(struct request_queue *q)
1中blk_init_queue 函数完成了请求队列的申请已经请求处理函数的绑定,这个一般用于像机械
硬盘这样的存储设备,需要 I/O 调度器来优化数据读写过程,但是对于 EMMC、SD 卡这样的
非机械设备,可以进行完全随机访问,所以就不需要复杂的 I/O 调度器了。
对于非机械设备我们可以先申请 request_queue,然后将申请到的 request_queue 与“制造请求”函数绑定在一起。
1. request_queue 申请函数 blk_alloc_queue,
struct request_queue *blk_alloc_queue(gfp_t gfp_mask)
gfp_mask:内存分配掩码,具体可选择的掩码值请参考 include/linux/gfp.h 中的相关宏定义,
一般为 GFP_KERNEL。
返回值:申请到的无 I/O 调度的 request_queue。
2.用函数 blk_queue_make_request绑定“制造请求函数”
void blk_queue_make_request(struct request_queue *q, make_request_fn *mfn)
q:需要绑定的请求队列,也就是 blk_alloc_queue 申请到的请求队列。
mfn:需要绑定的“制造”请求函数,函数原型如下:
“制造请求”函数需要驱动编写人员实现。
原型如下:
void (make_request_fn) (struct request_queue *q, struct bio *bio)
请求队列(request_queue)里面包含的就是一系列的请求(request),request 是一个结构体,定义在 include/linux/blkdev.h 里面.request 里面有一个名为“bio”的成员变量,类型为 bio 结构体指针。前面说了,真正的数据就保存在 bio 里面,所以我们需要从 request_queue 中取出一个一个的 request,然后再从每个 request 里面取出 bio,最后根据 bio 的描述将数据写入到块设备,或者从块设备中读取数据。
我们需要从request_queue中依次获取每个request,使用blk_peek_request函数完成此操作,
函数原型如下:
request *blk_peek_request(struct request_queue *q)
q:指定 request_queue。
返回值:request_queue 中下一个要处理的请求(request),如果没有要处理的请求就返回
NULL。
blk_start_request 函数
void blk_start_request(struct request *req)
req:要开始处理的请求。
也可以使用 blk_fetch_request 函数来一次性完成请求的获取和开启。
struct request *blk_fetch_request(struct request_queue *q)
{
struct request *rq;
rq = blk_peek_request(q);
if (rq)
blk_start_request(rq);
return rq;
}
实际h上还是调用了上面两个函数。
每个 request 里面里面会有多个 bio,bio 保存着最终要读写的数据、地址等信息。上层应用程序对于块设备的读写会被构造成一个或多个 bio 结构,bio 结构描述了要读写的起始扇区、要读写的扇区数量、是读取还是写入、页偏移、数据长度等等信息。
上层会将 bio 提交给 I/O 调度器,I/O 调度器会将这些 bio 构造成 request 结构,而一个物理存储设备对应一个 request_queue,request_queue 里面顺序存放着一系列的 request。新产生的 bio 可能被合并到 request_queue 里现有的 request 中,也可能产生新的 request,然后插入request_queue 中合适的位置,这一切都是由 I/O 调度器来完成的。
request_queue、request 和 bio 之间的关系如图:
bio 是块设备最小的数据传输单元,bio 是个结构体,定义在 include/linux/blk_types.h 中:
/*
* main unit of I/O for the block layer and lower layers (ie drivers and
* stacking drivers)
*/
struct bio {
struct bio *bi_next; /* request queue link */
struct block_device *bi_bdev; /* 指向块设备 */
unsigned long bi_flags; /* status, command, etc */
unsigned long bi_rw; /* bottom bits READ/WRITE,
* top bits priority
*/
struct bvec_iter bi_iter; /* I/O 操作,读或写 */
/* Number of segments in this BIO after
* physical address coalescing is performed.
*/
unsigned int bi_phys_segments;
/*
* To keep track of the max segment size, we account for the
* sizes of the first and last mergeable segments in this bio.
*/
unsigned int bi_seg_front_size;
unsigned int bi_seg_back_size;
atomic_t bi_remaining;
bio_end_io_t *bi_end_io;
void *bi_private;
#ifdef CONFIG_BLK_CGROUP
/*
* Optional ioc and css associated with this bio. Put on bio
* release. Read comment on top of bio_associate_current().
*/
struct io_context *bi_ioc;
struct cgroup_subsys_state *bi_css;
#endif
union {
#if defined(CONFIG_BLK_DEV_INTEGRITY)
struct bio_integrity_payload *bi_integrity; /* data integrity */
#endif
};
/* bio_vec 列表中元素数量 */
unsigned short bi_vcnt; /* how many bio_vec's */
/*
* Everything starting with bi_max_vecs will be preserved by bio_reset()
*/
/* bio_vec 列表长度 */
unsigned short bi_max_vecs; /* max bvl_vecs we can hold */
atomic_t bi_cnt; /* pin count */
/* bio_vec 列表 */
struct bio_vec *bi_io_vec; /* the actual vec list */
struct bio_set *bi_pool;
/*
* We can inline a number of vecs at the end of the bio, to avoid
* double allocations for a small number of bio_vecs. This member
* MUST obviously be kept at the very end of the bio.
*/
struct bio_vec bi_inline_vecs[0];
};
第13行,bvec_iter 结构体描述了要操作的设备扇区等信息
struct bvec_iter {
sector_t bi_sector; /* I/O 请求的设备起始扇区(512 字节) */
unsigned int bi_size; /* 剩余的 I/O 数量 */
unsigned int bi_idx; /* blv_vec 中当前索引 */
unsigned int bi_bvec_done; /* 当前 bvec 中已经处理完成的字节数 */
};
第56行,bio_vec 结构体描述了内容如下
/*
* was unsigned short, but we might as well be ready for > 64kB I/O pages
*/
struct bio_vec {
struct page *bv_page; /*页 */
unsigned int bv_len; /* 长度 */
unsigned int bv_offset; /* 偏移 */
};
*bi_io_vec指向 bio_vec 数组首地址,bio_vec 数组就是 RAM 信息,比如页地址、页偏移以及长度,“页地址”是 linux 内核里面内存管理相关的概念.
总结:
我们对于物理存储设备的操作不外乎就是将 RAM 中的数据写入到物理存储设备中,或者将物理设备中的数据读取到 RAM 中去处理。数据传输三个要求:数据源、数据长度以及数据目的地,也就是你要从物理存储设备的哪个地址开始读取、读取到 RAM 中的哪个地址处、读取的数据长度是多少。
请求中包含有大量的 bio,因此就涉及到遍历请求中所有 bio 并进行处理。遍历请求中的 bio 使用函数__rq_for_each_bio,这是一个宏。
#define __rq_for_each_bio(_bio, rq) \
if ((rq->bio)) \
for (_bio = (rq)->bio; _bio; _bio = _bio->bi_next)
bio 包含了最终要操作的数据,因此还需要遍历 bio 中的所有段,这里要用bio_for_each_segment 函数,此函数也是一个宏。
#define bio_for_each_segment(bvl, bio, iter) \
__bio_for_each_segment(bvl, bio, iter, (bio)->bi_iter)
第一个 bvl 参数就是遍历出来的每个 bio_vec,第二个 bio 参数就是要遍历的 bio,类型为bio 结构体指针,第三个 iter 参数保存要遍历的 bio 中 bi_iter 成员变量。
如果使用“制造请求”,也就是抛开 I/O 调度器直接处理 bio 的话,在 bio 处理完成以后要通过内核 bio 处理完成,使用 bio_endio 函数。
bvoid bio_endio(struct bio *bio, int error)
error:如果 bio 处理成功的话就直接填 0,如果失败的话就填个负值,比如-EIO。
编写RAM存储设备的驱动程序。
分为两种情况,一种是针对传统的机械硬盘编写驱动,它们需要调用IO调度器。本实验参考自 linux 内核 drivers/block/z2ram.c。
1.宏定义一些硬盘参数
#define RAMDISK_SIZE (2 * 1024 * 1024) /* 容量大小为 2MB */
#define RAMDISK_NAME "ramdisk" /* 名字 */
#define RADMISK_MINOR 3 /* 表示三个磁盘分区!不是次设备号为 3! */
2.定义硬盘设备结构体
/* ramdisk 设备结构体 */
struct ramdisk_dev{
int major; /* 主设备号 */
unsigned char *ramdiskbuf; /* ramdisk 内存空间,用于模拟块设备 */
spinlock_t lock; /* 自旋锁 */
struct gendisk *gendisk; /* gendisk */
struct request_queue *queue;/* 请求队列 */
};
struct ramdisk_dev ramdisk; /* ramdisk 设备 */
3.驱动模块加载与卸载框架
/*驱动入口函数*/
驱动入口函数完成了硬盘设备向内核的内存申请,创建了gendisk设备,并在内核中注册。此外,还初始化了请求队列,用于处理硬盘设备向内核的请求(读取或写入数据)。
/*驱动入口函数*/
static int __init ramdisk_init(void)
{
int ret = 0 ;
/*1.先使用 kzalloc 函数申请用于 ramdisk 实验的内存,大小为 2MB。*/
ramdisk.ramdiskbuf = kzalloc(RAMDISK_SIZE,GFP_KERNEL);
if(ramdisk.ramdiskbuf == NULL) {
{
ret = -EINVAL;
goto ram_fail;
}
/*2.初始化一个自旋锁,blk_init_queue 函数在分配并初始化请求队列的时候需要用到一次自旋锁。*/
spin_lock_init(&ramdisk.lock);
/* 3、注册块设备 参数为0的话内核自动分配设备号*/
ramdisk.major = register_blkdev(0, RAMDISK_NAME); /* 自动分配 */
if(ramdisk.major < 0) {
{
goto register_blkdev_fail;
}
else{
printk("ramdisk major = %d\r\n", ramdisk.major);
}
/* 4、分配并初始化 gendisk */
ramdisk.gendisk = alloc_disk(RADMISK_MINOR);
if(!ramdisk.gendisk) {
ret = -EINVAL;
goto gendisk_alloc_fail;
}
/* 5、分配并初始化一个请求队列 请求处理函数为ramdisk_request_fn,
具体的块设备读写操作就在此函数中完成,这个需要驱动开发人员去编写 */
ramdisk.queue = blk_init_queue(ramdisk_request_fn,&ramdisk.lock);
if(!ramdisk.queue) {
ret = -EINVAL;
goto blk_init_fail;
}
/* 6、添加(注册)disk */
ramdisk.gendisk->major = ramdisk.major; /* 主设备号 */
ramdisk.gendisk->first_minor = 0; /*起始次设备号) */
ramdisk.gendisk->fops = &ramdisk_fops; /* 操作函数 */
ramdisk.gendisk->private_data = &ramdisk; /* 私有数据 */
ramdisk.gendisk->queue = ramdisk.queue; /* 请求队列 */
sprintf(ramdisk.gendisk->disk_name, RAMDISK_NAME);/* 名字 */
set_capacity(ramdisk.gendisk, RAMDISK_SIZE/512); /* 设备容量(单位为扇区)*/
add_disk(ramdisk.gendisk);
return 0;
blk_init_fail:
put_disk(ramdisk.gendisk);
gendisk_alloc_fail:
unregister_blkdev(ramdisk.major, RAMDISK_NAME);
register_blkdev_fail:
kfree(ramdisk.ramdiskbuf); /* 释放内存 */
ram_fail:
return ret;
}
/*驱动出口函数*/
static void __exit ramdisk_exit(void)
{
/* 释放 gendisk */
del_gendisk(ramdisk.gendisk);
put_disk(ramdisk.gendisk);
/* 清除请求队列 */
blk_cleanup_queue(ramdisk.queue);
/* 注销块设备 */
unregister_blkdev(ramdisk.major, RAMDISK_NAME);
/* 释放内存 */
kfree(ramdisk.ramdiskbuf);
}
module_init(ramdisk_init);
module_exit(ramdisk_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("dongdong");
/*设备操作集函数ramdisk_fops*/
/*设备操作集函数ramdisk_fops*/
static struct block_device_operations ramdisk_fops =
{
.owner = THIS_MODULE,
.open = ramdisk_open,
.release = ramdisk_release,
.getgeo = ramdisk_getgeo,
};
/* 获取磁盘信息*/
int ramdisk_getgeo(struct block_device *dev,struct hd_geometry *geo)
{
/* 这是相对于机械硬盘的概念 */
geo->heads = 2; /* 磁头 */
geo->cylinders = 32; /* 柱面 */
geo->sectors = RAMDISK_SIZE / (2 * 32 *512); /* 磁道上的扇区数量 */
return 0;
}
重点是 getgeo 函数,,此函数用户获取磁盘信息,信息保存在参数 geo 中,为结构
体 hd_geometry 类型
struct hd_geometry {
unsigned char heads; /* 磁头 */
unsigned char sectors; /*一个磁道上的扇区数量 */
unsigned short cylinders; /* 柱面 */
unsigned long start;
}
最后就是非常重要的请求处理函数,使用 blk_init_queue 函数初始化队列的时候需要指定一个请求处理函数.(在驱动入口函数的第五步)
/* 处理传输过程*/
static void ramdisk_transfer(struct request *req)
{
/* blk_rq_pos 获取到的是扇区地址,左移 9 位转换为字节地址 */
unsigned long start = blk_rq_pos(req) << 9;
unsigned long len = blk_rq_cur_bytes(req); /* 大小 */
/* bio 中的数据缓冲区
* 读:从磁盘读取到的数据存放到 buffer 中
* 写:buffer 保存这要写入磁盘的数据
*/
void *buffer = bio_data(req->bio);
if(rq_data_dir(req) == READ) /* 读数据 */
memcpy(buffer, ramdisk.ramdiskbuf + start, len);
else if(rq_data_dir(req) == WRITE) /* 写数据 */
memcpy(ramdisk.ramdiskbuf + start, buffer, len);
}
/* 请求处理函数*/
void ramdisk_request_fn(struct request_queue *q)
{
int err = 0;
struct request *req;
/* 循环处理请求队列中的每个请求 */
req = blk_fetch_request(q);
while(req != NULL) {
/* 针对请求做具体的传输处理 */
ramdisk_transfer(req);
/* 判断是否为最后一个请求,如果不是的话就获取下一个请求
循环处理完请求队列中的所有请求。*/
if (!__blk_end_request_cur(req, err))
req = blk_fetch_request(q);
}
}
在/dev目录下会生成一个“ramdisk”的设备文件
使用 fdisk -l 查看磁盘信息
提示没有分区表,因为我们还没有格式化/dev/ramdisk 。 使用mkfs.vfat /dev/ramdisk格式化磁盘,将其格式化成 vfat 格式。
格式化完成以后就可以挂载/dev/ramdisk 来访问了,挂载点可以自定义,这里笔者就将其挂 载到/tmp 目录下,输入如下命令:
驱动出口函数报错如下:
block-scope函数只能包含外部存储类
删除static关键字解决。为什么会出现此问题?
补充:
函数也有存储类别,可以是外部函数(默认)或静态函数。
C99 新增了 第 3 种类别——内联函数。外部函数可以被其他文件的 函数访问,但是静态函数只能用于其定义所在的文件。假设一个文件中包含 了以下函数原型:
double gamma(double); /* 该函数默认为外部函数 */
static double beta(int, int);
extern double delta(double, int);
在同一个程序中,其他文件中的函数可以调用gamma()和delta(),但是
不能调用beta(),因为以static存储类别说明符创建的函数属于特定模块私 有。这样做避免了名称冲突的问题,由于beta()受限于它所在的文件,所以 在其他文件中可以使用与之同名的函数。
通常的做法是:用 extern 关键字声明定义在其他文件中的函数。
这样做是为了表明当前文件中使用的函数被定义在别处。除非使用static关键字, 否则一般函数声明都默认为extern。
存储类别的选择
对于“使用哪种存储类别”的回答绝大多数是“自动存储类别”,要知道默认类别就是自动存储类别。初学者会认为外部存储类别很不错,为何不把所有的变量都设置成外部变量,这样就不必使用参数和指针在函数间传递信息 了。然而,这背后隐藏着一个陷阱。如果这样做,A()函数可能违背你的意 图,私下修改B()函数使用的变量。多年来,无数程序员的经验表明,随意使用外部存储类别的变量导致的后果远远超过了它所带来的便利。
唯一例外的是const数据。因为它们在初始化后就不会被修改,所以不用担心它们被意外篡改:
const int DAYS = 7;
const char * MSGS[3] = {"Yes", "No", Maybe"};
保护性程序设计的黄金法则是:“按需知道”原则。尽量在函数内部解决 该函数的任务,只共享那些需要共享的变量。除自动存储类别外,其他存储 类别也很有用。不过,在使用某类别之前先要考虑一下是否有必要这样做。
原文链接:https://blog.csdn.net/weixin_43914889/article/details/105047780