3.Linux文件系统
文件系统:磁盘、分区的组织形式
cd /
/home:用户目录,存放普通用户相关文件
/proc:进程,此目录的数据在内存中,包含进程等信息,如系统核心,外部设备,网络状态等
/mnt: Mount,用于挂载各种文件系统,如: U盘、SD卡等
/tmp: Temp,存放临时文件。一般用户或正在执行的程序临时存放文件的目录,任何人都可以访问,不能存放重要的数据
/etc: 系统所有的配置文件都在该目录下
/usr: User,应用程序存放目录
/lib: Library,存放系统的函数库
/bin:Binary,存放常用的系统命令。
/sbin: System Binary,存放系统管理员 root 使用的命令
来源
cd /dev:设备相关信息
lp:打印池,一个打印池可以对应一个打印机
磁盘设备:sda、sda1、sda2,磁盘插入后就会出现
stderr:标准错误设备,文件标识符为2
stdin:标准输入设备,文件标识符为0
stdout:标准输出设备,文件标识符为1
01.Linux文件系统
磁盘(中分区)组织文件的方法
001.创建文件系统
- 识别硬盘
- 双击虚拟机参数中的硬盘→添加→…→开机
- 硬盘位置:
/dev
硬盘命名规则:
– hd(IDE),比如hda,hdb;
– sd(SCSI),比如sda,sdb,sdc
- 查看磁盘基本信息:fdisk -l
分区命名规则:
– sda1:主分区,引导分区
– sda2:扩展分区
– sda5~:逻辑分区
- 给硬盘分区:fdisk <磁盘标志符>
fdisk /dev/sdb
– 输入m获得帮助
– 输入l获得分区类型介绍
– 输入n添加新分区:
可以看到当前硬盘MBR主分区表空闲
– 选p,一路回车,使整块硬盘为一个主分区
– 写入更改并退出
– 查看/dev
- 给分区格式化,创建文件系统:mkfs(make file system)
mkfs -t <文件系统类型> <分区名称>
Linux常见分区格式:
– ext2 支持反删除
– ext3 开始支持大文件(32768G,一个块8K才支持),支持日志(log)文件(有挂载开机自检)
– ext4 ext3的改进,没有日志文件了。不支持反删除
mkfs -t ext4 /dev/sdb1 对第二块硬盘的第一个分区创建文件系统
- 绑定(挂载)目录
Linux中的根目录以外的文件要想被访问,需要将其“关联”到根目录下的某个目录来实现,这种关联操作就是“挂载”,这个目录就是“挂载点”,解除次关联关系的过程称之为“卸载”
– mkdir /disk_2
– mount /dev/sdb1 /disk_2
– cd /disk_2、ls -al
– fdisk -l
– 查看磁盘基本信息df -l
可以看到每一个分区对应一个挂载点,分区表占了一点空间
– 卸载(解挂载):umount <挂载点>,然后目录和磁盘就解除关系了
002.文件与目录操作
- 权限位10位
ls -l | more
- 第一位:描述文件类型
- 普通文件
c char,字符文件(串行)
b block,块文件(并行)
l link,链接文件,一种特殊的快捷方式
p pipe,管道文件
s socket,套接字文件
d directory,文件夹(目录) - 第二~十位
2~4:属主(owner)权限rwx
5~7:属组(group)权限
8~10:非同组用户(other)权限
关于用户权限:
– 可执行权限全部去掉,属主和root用户都无法执行
– 读写权限全部去掉,文件属主无权读写,root用户依然可以
- 用户权限设定(由文件属主或root用户设定)
chmod <权限取值> <文件>
按权限取值分类:- 3位8进制数,直接设置权限,每位对应一类用户权限,用1表示有权限,0表示无权限
比如chmod 755 abc表示abc权限位为rwx r-x r-x - u/g/o +/- r/w/x ,增加减少权限,u:user;g:group;o:other
可以看到不可执行的文件被授予可执行权限后也被标记为绿色,但依然不可执行 - 文件的链接
- 硬链接:ln <原文件> <链接文件>,本质上是原文件的复制,原文件不存在硬链接文件依然有效
- 软连接:ln -s <原文件> <链接文件>,本质上就是快捷方式,原文件不在原位置后链接文件会报错,除非恢复原文件或重新创建快捷方式。作用就是可以把链接文件放到文件系统中的任意位置
- 3位8进制数,直接设置权限,每位对应一类用户权限,用1表示有权限,0表示无权限
02.文件操作相关命令
- 路径
- 绝对路径:从根到文件名称 /…/…/…/filename
- 相对路径:相对于当前路径的路径
- ./ 当前路径 ./open open.c
- . ./ 当前父路径
其中的cp ./file/open open1就是在/home/sonya/code目录拷贝子目录file下的open文件到当前目录,为open1
- $HOME 相当于Windows下的PATH路径参数,是当前用户家目录的默认值,即/home/txp
- 查看文件
ls -l 以长格式显示 -a显示隐藏文件
分别是①文件属性、②节点(子目录)个数(如果一个文件不是目录,此时这一字段表示这个文件所具有的硬链接数)、③文件(目录)拥有者、④文件(目录)拥有者所在的组、⑤文件所占用的空间(以字节为单位)、⑥文件(目录)最近访问(修改)时间、⑦文件名
ls -la
vim .bash_history
可以看到记录的是从系统登录以来以root身份执行的命令,用history命令可以查看包括普通用户的命令
vim .bash_profile
vim .bashrc
指定了系统执行命令默认的查找路径 - 路径相关
cd
mkdir
rmdir:删除目录(目录必须为空)
rm:删除文件
rm -r:删除一个非空的目录,rm -r /是最危险的命令 - 环境变量
PATH:指定一些默认目录,其下的命令可以直接执行,不用指定路径,系统会自动去这些目录找
显示环境变量echo $PATH
which:查看命令位置
- 文件操作
cp,mv,rm(不支持反删除)
通配符:*指一切内容,?是单一符号的替换 - 可执行文件
Linux没有像Windows那样的文件类型,后缀只是方便识别,能否执行要看权限 - 查看文本文件
- 用软件查看,vi编辑器、gedit等
- 用cat,head,tail等命令查看
- 创建文本文件
echo “abcabcabc”>test.log 覆盖写,>为定向符
echo “xyzxyzxyz”>>test.log 追加写 - 查看
- cat
cat /var/log/messages
cat -n messages
cat messages | more分页显示 - head messages
- tail messages
- cat
- 掩码umask
– root用户为022,普通用户为002
– 可执行文件最高权限位默认为777,文本文件为666
- 查找文件
– which:求文件的绝对路径(在$PATH路径范围内查找)
– whereis:和which相比多了通配符查找
– find:find <查找位置> -name <文件名> 能指定查找位置
03.相关程序设计
文件读写
系统函数:open/creat,read,write,close- open/creat函数:返回文件句柄(用来读写、关闭)
– man open
flags如果只是读写、追加的形式,不需要第三个参数,如果是创立的形式,则要第三个参数;mode_t是创立的文件的权限说明。open三参数和creat函数效果等同
flags(打开模式):
— O_RDONLY只读
— O_WRONLY只写
— O_RDWR可读写
— O_APPEND追加写
— O_CREAT创建
— O_TRUNC截断(以前的内容全部放弃)
mode_t(权限模式):S_I<权限><对象> S_I是引导词,其后占4位;或者用3个八进制数的权限位代替
— 权限:R,W,X,RWX
— 对象:USR,GRP,OTH,U,G,O
— 比如说
0是八进制标志,10是权限引导 - close函数
close(句柄)
– 代码#include <unistd.h> //Linux标准头文件 #include <stdio.h> //标准输入输出 #include <stdlib.h> //标准库文件 #include <fcntl.h> //文件控制头文件 #include <errno.h> //出错处理头文件 #include <string.h> //将错误信息转换为字符串输出 #include <sys/types.h> //在此头文件中定义了一种转换,将int等常见类型转换为有字面含义的类型,这种类型方便理解int bail(const char*str) {perror(str); //程序出错时打印用户提供的错误说明,一定要有printf("%s\n",strerror(errno)); //打印错误代码 当系统出错时会自动生成整数errno错误说明,strerror函数返回错误代码exit(-1); } int main(int argc,char** argv) //可以有0~3个参数。argc记录当前命令的参数个数,argv是命令的字符串内容(char*表示不定长字符串,char**表示不定长字符串数组,比如说cp abc.txt test.log命令,argv[1]=abc.txt) {int fd=-1; //文件句柄fd=open(argv[1],O_RDONLY); //系统函数int open(const char *pathname, int flags);以只读模式打开文件if(fd<0)bail("Open file error.\n");printf("fd=%d\n",fd);close(fd);return 0; }
read函数
在C中,有fgets函数,文件读取结束标志EOF;而read函数只有读操作,先读到缓冲区中,在对缓冲区中的内容进行操作、分析,行结束判断只能靠用户编码实现
– man 2 read(read函数说明,而不是命令)
int read(int fd,void* buf,int len);
返回值:- -1:读取失败
- 0:没有读取到数据
- n(1~len):读取到的字节数,若恰好为len,不一定读完;若在0到len-1之间,则读完了
– 代码:
#define LEN 512 //定义一次要读取的字节数 #include <unistd.h> #include <stdio.h> #include <stdlib.h> #include <fcntl.h> #include <errno.h> #include <string.h> #include <sys/types.h>int bail(const char*str) {perror(str);printf("%s\n",strerror(errno)); exit(-1); } int main(int argc,char** argv) {int fd=-1;char buf[LEN]={0}; //定义一个缓冲区,保存读取的内容int rdret=-1; //read的返回值fd=open(argv[1],O_RDONLY); if(fd<0)bail("Open file error.\n");printf("fd=%d\n",fd);rdret=read(fd,buf,sizeof(buf));while(rdret>0){printf("%s",buf);memset(buf,0,sizeof(buf)); //读到最后有可能少于LEN,缓冲区覆盖不全,出现乱码。所以字符数组用了就清空rdret=read(fd,buf,sizeof(buf));}if(rdret<0){close(fd); //打开的句柄不关掉不能退出的bail("read file error.");}close(fd); //rdret=0直接退出return 0; }
write函数
将缓冲区的内容写到句柄中
– man 2 write
ssize_t write(int fd, const void *buf, size_t count);
— const:防止缓冲区被用户操作改变,增加程序的安全性
— size_t:期望写入的字节(ascii)个数
— ssize_t:实际写入的长度,0表示没有写,-1表示写入失败,0~count表示实际写入的字符个数。有没有写完不是靠返回值,而是看用户是否给输入了,read读完了就结束了
– 代码:将从键盘中键入的内容写入文件中,键入完毕文件也自动关闭#define LEN 512 //定义一次要读取的字节数 #include <unistd.h> #include <stdio.h> #include <stdlib.h> #include <fcntl.h> #include <errno.h> #include <string.h> #include <sys/types.h>int bail(const char*str) {perror(str);printf("%s\n",strerror(errno)); exit(-1); } int main(int argc,char** argv) {int fd=-1;char buf[LEN]={0}; //定义一个缓冲区,保存读取的内容int wtret=-1; //read的返回值fd=open(argv[1],O_WRONLY | O_APPEND); //句柄为附加写,针对已经存在的文件//针对要写入的文件不存在//fd=open(argv[1],O_CREAT | O_RDWR,S_IWUSR | S_IRUSR | S_IRGRP | S_IROTH);if(fd<0)bail("Open file error.\n");printf("fd=%d\n",fd);printf(">"); //表示这是一个输入gets(buf); //从键盘得到一个字符串输入到缓冲区while(strcmp(buf,"quit")!=0) //判断输入是否结束{wtret=write(fd,buf,strlen(buf)); //输入都以字符接收的,所以用strlen计算长度if(wtret<0){close(fd);bail("Write file error.\n");}//往下为下一次循环作准备memset(buf,0,sizeof(buf)); //字符串操作前缓冲区要清空printf(">");gets(buf);}close(fd); //rdret=0直接退出return 0; }
– 代码:自己写个copy命令
— 打开源文件
— 创建目标文件
— 读取源文件
— 写入目标文件
— 关闭句柄,退出#define LEN 512 #include <unistd.h> #include <stdio.h> #include <stdlib.h> #include <fcntl.h> #include <errno.h> #include <string.h> #include <sys/types.h>int bail(const char*str) {perror(str);printf("%s\n",strerror(errno)); exit(-1); } int main(int argc,char** argv) {int rdfd=-1;int wtfd=-1;char buf[LEN]={0};int rdret=-1; int wtret=-1; //打开源文件rdfd=open(argv[1],O_RDONLY);if(rdfd<0)bail("Open file error.\n");//创建目标文件wtfd=open(argv[2],O_CREAT | O_WRONLY,010755);//有可执行权限,一般以二进制形式处理if(wtfd<0){close(rdfd);bail("Create file error.");}//rdfd、wdfd都有了//读取源文件rdret=read(rdfd,buf,sizeof(buf));//从读句柄中读while(rdret>0){//写入目标文件wtret=write(wtfd,buf,rdret);//将缓冲区中的实际内容写入句柄,而不是以缓冲区的大小或strlen(以/0结束计算,有多个字符串只计算第一个)if(wtret<rdret){close(rdfd);close(wtfd);bail("Write file error.");}memset(buf,0,sizeof(buf));rdret=read(rdfd,buf,sizeof(buf));}if(rdret<0){close(rdfd);close(wtfd);bail("read file error.");}//rdret=0,关闭句柄,退出close(rdfd);close(wtfd);return 0; }
./copy open myopen
问题:权限不一样,要读取权限;读文件不存在、写文件存在、读文件没有读权限等
- open/creat函数:返回文件句柄(用来读写、关闭)
文件控制
access函数:访问函数,看文件是否存在、获得文件的读写权限
man 2 access
int access(const char *pathname, int mode);
– 返回值:0为测试成功,-1为失败
– pathname:文件名
– mode:测试权限,多个条件都要满足用 | 符号连接
— R_OK:读可以
— W_OK:写可以
— X_OK:执行可以
— F_OK:文件存在
vimx access.c#define LEN 512 //定义一次要读取的字节数 #include <unistd.h> #include <stdio.h> #include <stdlib.h> #include <fcntl.h> #include <errno.h> #include <string.h> #include <sys/types.h>int bail(const char*str) {perror(str);printf("%s\n",strerror(errno)); exit(-1); } int main(int argc,char** argv) {int ret=-1;ret=access(argv[1],F_OK);//文件名由参数argv[1]决定if(ret<0)printf("File not exist.\n");elseprintf("File exist.\n");return 0; }
读:
ret=access(argv[1],R_OK); if(ret==0)fd=open();
写:
ret=access(argv[1],F_OK); if(ret<0)fd=creat(); elseprintf("overwrite?");
stat函数:读取权限位
man 2 stat
– sys/types.h中类型的宏定义转换,比如uid_t转为int
– sys/stat.h中定义了stat结构体
– int stat(const char *pathname, struct stat *statbuf);
将文件名为pathname的文件的状态读入结构体中。引用结构体用的是指针,在调用时要用取地址符&
struct stat st;
ret=stat(文件名,&st);
返回值:-1,失败
st返回值:
– st.st_size 文件大小
– st.st_mode 文件类型、权限
– st.st_?time 时间相关参数(秒为单位,用ctime转换)
– st.mode:- st.mode&S_IFMT
- 宏函数
S_ISDIR
S_ISREG
S_ISLNK
S_IFIFO
S_ISCHR
S_ISOCK
代码:复制源文件权限到目标文件
#define LEN 512 //定义一次要读取的字节数 #include <unistd.h> #include <stdio.h> #include <stdlib.h> #include <fcntl.h> #include <errno.h> #include <string.h> #include <sys/stat.h>//stat头文件 #include <sys/types.h>int bail(const char*str) {perror(str);printf("%s\n",strerror(errno)); exit(-1); } int main(int argc,char** argv) {int ret=-1;int sfd=-1;int dfd=-1;struct stat bs;memset(&bs,0,sizeof(bs));ret=stat(argv[1],&bs);//读取源文件状态dfd=creat(argv[2],bs.st_mode);//以源文件权限位创建目标文件close(dfd);//啥都不做return 0; }
目录操作
dir,opendir,closedir,readdir
步骤:打开目录、读取目录信息、关闭目录打开目录(要头文件dirent.h)
打开目录返回的句柄不是一个整数,而是一个指针:
DIR* opendir(目录名称)
– 成功:返回地址
– 失败:返回NULL关闭目录
closedir(DIR* 目录指针)
– 成功:0
– 失败:-1读取目录信息
怎么读取信息、信息放到哪去?
struct dirent* readdir(DIR* 指针);
– 成功:返回结构体指针(每次读完的信息返回到这个结构体)
– 失败:NULL
– 方式:循环自动读取
man 3 readdir(3是传统的)
主要就是为了得到那两个分量#define LEN 512 //定义一次要读取的字节数 #include <unistd.h> #include <stdio.h> #include <stdlib.h> #include <fcntl.h> #include <errno.h> #include <string.h> #include <dirent.h>//与目录操作有关的头文件 #include <sys/stat.h> #include <sys/types.h>int bail(const char*str) {perror(str);printf("%s\n",strerror(errno)); exit(-1); } int main(int argc,char** argv) {DIR* dp=NULL;//定义一个目录指针struct dirent* dir=NULL;//定义一个结构体指针,指向读取的内容dp=opendir(argv[1]);if(dp==NULL)bail("open dir error.");//readdir(dp);//不重复读,读取的是目录的第一个文件while((dir=readdir(dp))!=NULL){if(dir->d_name[0]=='.')//如果文件名以.引导,不可见;.为当前目录,..为父目录,这两个在目录中一定存在continue;if(dir->d_type==DT_REG)//各种目录操作的位置printf("Regular file:%s\n",dir->d_name);if(dir->d_type==DT_DIR)printf("Dir file:%s\n",dir->d_name);}closedir(dp);return 0; }
./readdir .
./readdir $HOME创建目录函数
man 2 mkdir
#include <sys/stat.h>:权限参数可以从<stat.h>中的结构体得到
int mkdir(const char *pathname, mode_t mode);
– 返回值:成功:0,失败:-1切换目录函数
chdir(新目录名称)
– 代码:给定源目录,遍历目录,创建一个新目录,将源目录中子项添加到新目录中去
— 假定:仅复制一级目录,不进入子目录,即只创建,不复制
— cpdir(source,dest):将源目录的子项复制到目标目录中去- 可行性:source要存在、可读,dest不存在,可写;
- 步骤:
– 读source状态,作为dest的状态创建目标目录
– source:readdir;chdir(dest)
– dest:mkdir/creat;chdir(source)
– 关闭目录
#define LEN 512 #include <unistd.h> #include <stdio.h> #include <stdlib.h> #include <fcntl.h> #include <errno.h> #include <string.h> #include <sys/types.h> #include <dirent.h>//目录头文件 #include <sys/stat.h>//文件状态int bail(const char*str) {perror(str);printf("%s\n",strerror(errno)); exit(-1); } int main(int argc,char** argv) {char sdir[1024]={0};//源目录路径char ddir[1024]={0};//目标目录路径char sname[1024]={0};//源文件名char dname[1024]={0};//目标文件名int ret=-1;struct dirent* dir=NULL;//读取的目录中每一条(目录子项)的信息,用readdir返回DIR* dp=NULL;//指针指向目录的指针,用opendir返回struct stat sb;//存放源目录的权限状态int fd=-1;//creat返回的句柄if(access(argv[1],F_OK)<0)//第一件事看源目录是否存在bail("Source directory not exist.");if(access(argv[1],R_OK)<0)bail("Perssion denied.");if(access(argv[2],F_OK)==0)bail("Dest directory exist.");getcwd(sdir,sizeof(sdir));//获取源目录(当前路径)绝对路径,get current workspace directorymemset(&sb,0,sizeof(sb));stat(argv[1],&sb);//获得源目录的权限if(mkdir(argv[2],sb.st_mode)<0)//创建目标路径bail("Creat dest directory error.");chdir(argv[2]);getcwd(ddir,sizeof(ddir));//为什么不直接是argv[2]?因为那可能是相对路径chdir(sdir);dp=opendir(argv[1]);while((dir=readdir(dp))!=NULL){if(dir->d_name[0]=='.')continue;else if(dir->d_type==DT_DIR){memset(&sb,0,sizeof(sb));stat(dir->d_name,&sb);//根据源文件的权限模式创建目标文件chdir(ddir);mkdir(dir->d_name,sb.st_mode);chdir(sdir);continue;}else//目录用mkdir,其他用creat{memset(&sb,0,sizeof(sb));stat(dir->d_name,&sb);chdir(ddir);fd=creat(dir->d_name,sb.st_mode);close(fd);chdir(sdir);continue;}}closedir(dp);}
效果:
3.Linux文件系统相关推荐
- Linux 文件系统剖析
Linux 文件系统剖析 按照分层结构讨论 Linux 文件系统 M. Tim Jones, 顾问工程师, Emulex Corp. 简介: 在文件系统方面,Linux® 可以算得上操作系统中的 &q ...
- linux文件系统dentry_NFS 文件系统源代码剖析
NFS 文件系统概述 NFS(Network File System,网络文件系统)是一种基于网络的文件系统.它可以将远端服务器文件系统的目录挂载到本地文件系统的目录上,允许用户或者应用程序像访问本地 ...
- Linux文件系统构成(第二版)
Linux文件系统构成 /boot目录: 内核文件.系统自举程序文件保存位置,存放了系统当前的内核[一般128M即可] 如:引导文件grub的配置文件等 /etc目录: 系统常用的配置文件,所以备份系 ...
- linux检查文件一致性,3.20 fsck(检查并修复Linux 文件系统)
3.20 fsck(检查并修复Linux 文件系统) (1)频度等级:☆☆ (2)功能说明: 检查文件系统的一致性并且以交互方式修复文件系统.在出现系统故障之后,总是运行fsck 命令.矫正的动作也许 ...
- 文件系统:Linux文件系统剖析
查看原文:http://www.ibm.com/developerworks/cn/linux/l-linux-filesystem/ 在文件系统方面,Linux® 可以算得上操作系统中的 " ...
- linux对文件的描述,对Linux文件系统的简单理解
姓名:邝念君 学号:14020150024 [嵌牛导读]:EXT3,EXT4,BTRFS和XFS是现在最常见的四个Linux文件系统.为了便于理解,本文以相对简单的EXT2为切入点,介绍其原理.便不难 ...
- linux扩文件系统大小,调整卷大小后扩展 Linux 文件系统 - Amazon Elastic Compute Cloud...
AWS 文档中描述的 AWS 服务或功能可能因区域而异.要查看适用于中国区域的差异,请参阅中国的 AWS 服务入门. 调整卷大小后扩展 Linux 文件系统 在增加 EBS 卷的大小后,您必须使用特定 ...
- 低调的 Linux 文件系统家族
在 Linux 中,最直观.最可见的部分就是 文件系统(file system).下面我们就来一起探讨一下关于 Linux 中国的文件系统,系统调用以及文件系统实现背后的原理和思想. 这些思想中有一些 ...
- 《Linux嵌入式实时应用开发实战(原书第3版)》——3.5 Linux文件系统
本节书摘来自华章计算机<Linux嵌入式实时应用开发实战(原书第3版)>一书中的第3章,第3.5节,作者:(美)Doug Abbott 更多章节内容可以访问云栖社区"华章计算机& ...
- 开发板与linux文件系统,基于topeer 4412开发板 ***面linux文件系统的制作
Exynos-4412不仅可以运行Android,还可以运行简单的linux最小文件系统(不带显示界面的linux系统),下面我们来讲解一下这种文件系统的制作. 制作文件系统我们需要使用到Busybo ...
最新文章
- 构建RHEL上的extmail
- 一道Python面试题,据说大部分人都中招了,纷纷开始怀疑自己
- 【NLP实战系列】朴素贝叶斯文本分类实战
- boost::fibers模块实现single stream的测试程序
- ssl2331OJ1373-鱼塘钓鱼 之3【dp】
- Ubuntu Sudo 无法解析的主机
- Codeforces Round #437 (Div. 2, based on MemSQL Start[c]UP 3.0 - Round 2)
- 轨迹系列13——多轨迹展示在实际项目中的落地和优化
- Parquet文件格式简介
- Leetcode 惊现马化腾每天刷题 ? 为啥大佬都这么努力!
- 华东理工大计算机专业,华东理工大学计算机专业怎么样(计算机专业大学排名50)...
- 罗格斯的计算机科学博士奖学金,罗格斯大学cs怎么样
- 设计模式:设配器模式
- 【百度大脑新品体验】人脸面部动作识别
- matlab算kdj指标,KDJ指标(随机指标)详解
- 分销商城怎样设计分销体系以及会员成长体系_OctShop
- numeric_limits 解析
- 峰会/论坛现场签约怎么签?君子签提供区块链电子签约技术支持
- C#,数值计算,矩阵的乔莱斯基分解(Cholesky decomposition)算法与源代码
- python 圆形检测_基于opencv-python的圆形检测