• Linux知识点总结(文件,进程,进程间通信)


    一、文件操作
    windows:DOS命令
    linux:shell命令
    都是为了操作文件
    rm -rf +目录名:删除目录
    mkdir :制作目录
    touch :制作文件
    vi se+tab键:快速进入某个文件
    命令行直接G到达文件末尾
    yy p复制粘贴
    命令行模式下:gg=G:自动对齐

    文件操作:
    打开文件、创建文件、读、写、光标移动,给文件写入结构体。
    
    • 1
    • 2
    touch file  创建文件
    1、open:
    	(1)  int open(const char *pathname, int flags);//文件存在时用这种方式打开
    		fd = creat("/home/yx/file3",S_IRWXU);//创建一个可读可写可执行的文件
    	(2)  int open(const char * pathname, int flags, mode_t mode);//不存在时用这种方式先创建再打开
    		 open(fd,O_RDWR|O_CREAT|O_TRUNC,0600);//文件不存在就创建存在就清空。
    		open("./file",O_RDWR|O_APPEND);//每次打开都移动到文件末端换行,从文件下一行写入
    
    		O_RDWR//可读可写
    		O_CREAT//没有就创建
    		O_APPEND//从文件下一行起
    		!!!O_TRUNC//清空文件内容
    		结论:如果没有O_TRUNC|O_APPEND,就会从文件头开始写入,会覆盖原先的文件内容
    		
    2、write/read:一定要关心写到哪,从哪读
    	ssize_t write(int fd, const void *buf, size_t count)返回写入的字节数
    	不知道文件大小时,就lseek求文件大小
    	read(int fd, void *buf, size_t count);
    	指针使用前一定要分配地址
    	linux下指针默认分配8字节,8个字母
    	注意:写完文件后,要读取的话,光标一定要移动到文件头
    3、lseek:
    	off_t  lseek(int fd, off_t offset, int whence);
    	fd:对fd所指向的文件进行光标偏移
    	offset:往后偏移这么多字节
    	whence:从哪里偏移;
    	SEEK_SET :光标移到文件头开始偏移
    	SEEK_CUR:从光标当前位置开始偏移
    	SEEK_END:从文件末尾开始偏移
    	lseek返回值是从文件头到偏移到当前位置(whence)所偏移的值(反应的是文件偏移量)。
    	!!!int size = lseek(fdSrc,0,SEEK_END);//求文件大小(当文件头移动到文件末有多少字节)
    	lseek(fdSrc,0,SEEK_SET);
    	lseek(fdSrc,-20,SEEK_CUR);//从当前位置向前偏移20字节
    close:
    	close(fd)关闭文件
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    从键盘输入输出:
    0:标准输入,从键盘获取
    1:标准输出,给到终端
    2:标准错误
    read(0,readbuf,5);//从键盘读取5个字节
    write(1,readbuf,strlen(readbuf));//输出到终端上
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    非同步修改文件配置:
    思想,把源文件的内容写到readbuf里面(计算完源文件大小后要重新把光标移到文件头),
    在readbuf里面寻找需要需要修改的文件内容(一定要写入字符型数字,文件里存的都是字符),
    进行修改(利用了指针偏移),此时readbuf里面的内容被修改,把readbuf里面的内容
    写回到源文件可以覆盖也可以重新打开清空文件内容。
    
    核心:char *strstr(const char *haystack, const char *needle);
    在haystack字符串里面查找needle字符串。如果找到则返回到要找的字符串的开始位置处;查找失败返回空指针。
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    给文件写入结构体:(是打开文件)
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    struct data
    {
            int a;
            char b;
    };
    //给一个指定的文件写入自己想要的数据,然后读出来
    //流程:数组写给源文件,源文件读到test2,对结构体&等于地址
    int main()
    {
            int fdSrc;
            struct data test[2] = {{98,'w'},{89,'m'}};
            struct data test1[2];
            fdSrc = open("./file",O_RDWR);
            write(fdSrc,&test,sizeof(struct data)*2);//给源文件写入后面这么多字节,写的内容是test2里面的内容
            lseek(fdSrc,0,SEEK_SET);//写完数据光标重新移到文件头要读数据
            read(fdSrc,&test1,sizeof(struct data)*2);
    
            printf("%d  %c\n",test1[0].a,test[0].b);
            printf("%d  %c\n",test1[1].a,test[1].b);
            close(fdSrc);
            return 0;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    linux指针分配的是8字节,1字母1字节
    
    • 1
    -rwxrwxr-x:
    -代表普通文件
    rwx代表当前用户的权限4+2+1
    rwx:代表同组其他用户权限4+2+1
    r-x:代表其他用户可读可执行不可写
    
    • 1
    • 2
    • 3
    • 4
    • 5
    r:只读打开文本
    rb:只读打开二进制文件
    w:只写打开文本
    wb:只写打开二进制文件
    r+:可读可写打开文本
    w+:可读可写创建文本
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    二、C标准库的文件操作

    FILE *fopen(const char *path, const char *mode);//返回的是文件标识符
    
    size_t fwrite(const void *ptr, size_t size, size_t nmemb,  FILE *stream);
    参数一:要往文件写入的内容,是字符串格式
    参数二:一次写入的字节数
    参数三:写多少次
    参数四:目标文件标识符
    size_t fread(const void *ptr, size_t size, size_t nmemb,  FILE *stream);
    
    fclose(*FILE)//关闭文件
    
    fputc(str,fp),往目标文件写入一个字符
    
    feof(fp)//没到达文件末尾,返回值是0,到达文件末尾,返回值不为0
    c = fgetc(fp);//每次从文件里面读取一个字符存到c,读完以后自动后移
    这俩经常搭配在一起使用
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    文件应用:
    往文件写入东西时:要创建文件,而非readbuf(例如把结构体数组写到文件,把文件写到另一个结构体)
    复制文件时:创建readbuf并且读写(把文件读到readbuf,把readbuf写到另一个文件)
    两个文件之间的操作用readbuf(自己写cp命令)
    单文件内的赋值用另外一个文件赋值作中介(把结构体写入文件)
    
    • 1
    • 2
    • 3
    • 4
    • 5

    三、进程的概念:

    进程的操作:fork创建进程、vfork()创建进程,等待函数,子进程调用exit(0)退出
    wait(&status);//子进程退出状态放到status里面
    WEXITSTATUS(status)把子进程的退出状态解析到status里面如之前的1
    getpid();//获取当前进程进程号
    getppid();//获取父进程的进程号
    exec族函数:
    子进程使用exec调用另外一个程序来运行,该进程会被完全替换为该程序,进程号不变
    调用失败返回-1,调用成功无返回值
    第一个参数是:路径下哪个程序
    execl("./echoarg","echoarg","abc",NULL) == -1//当前目录下运行echoarg,后面是参数,参数必须以NULL结尾
    execl("/bin/ls","ls",NULL,NULL) //绝对路径下ls,传参
    execl("/bin/date","date",NULL,NULL) //调用系统时间
    whereis ls  查看ls的绝对路径
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    进程就是跑起来的程序
    ps -aux|grep init  把很多进程中带init的过滤出来
    top:类似windows下的任务管理器,用来查看进程的cpu占用率
    每个进程都有一个非负整数表示进程号pid,类似身份证
    getpid();//获取当前进程进程号
    getppid();//获取父进程的进程号
    
    pid_t fork(void);
    fork函数调用成功返回两次
    fork = 0,表示当前进程是子进程
    fork返回值是非负数,表示当前进程是父进程,返回值为子进程pid号
    创建进程就是把fork后面的代码(包括fork行)重新运行一次,不过得父进程运行完成之后再来运行子进程
    这就是返回两次的概念
    此时就有了父子进程的概念,程序的进程先运行,运行结束子进程运行
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    程序空间分配:
    全局数据区∶存放全局变量,静态变量口(会被默认初始化为0)
    数据段:初始化过的变量
    BSS段:函数外未初始化过的变量
    栈空间∶存放函数参数,局部变量,函数调用信息也保存此(栈用于保存短暂时间变量)
    堆空间∶用于动态创建变量(malloc)
    寄存器:最快的存储区
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    父进程创建子进程发生了什么呢?
    1、以前的linux拷贝处理
    fork()以后,同时有两个代码段,子进程会复制父进程的所有代码,形成自己独立的物理空间(A和B等价)
    所以在子进程中改变的变量值不会影响父进程中变量值
    2、当前的linux写时拷贝
    即子进程不对父进程变量动手脚时,这些代码都是共享而非独立,当子进程想要改变父进程变量时,
    就会复制一份变量用来改变,省去大篇幅的复制代码,节省空间
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    fork创建一个子进程的一般目的:
    应用1.一个父进程希望复制自己,使父子进程同时执行不同的代码段(多个子进程同时运行),在这个网络服务进程中是常见的。父进程等待客户端的服务请求。当这种请求到达时,父进程调用fork,使子进程处理此请求。父进程则继续等待下一个服务请求到达。
    应用2.一个进程要执行一个不同的程序,这对shell常见的情况。在这种情况下,子进程从fork返回后立即调用exec
    
    • 1
    • 2
    • 3
    int main()
    {
            int data;
            pid_t pid;
            while(1)
            {
                    printf("please input a data:");
                    scanf("%d",&data);
                    if(data==1)
                    {
                            pid = fork();
                            if(pid>0)
                            {
    
                            }
                            if(pid==0)
                            {
                                    while(1)
                                    {
                                            printf("this is child fork,pid is %d,my father pid id %d\n",getpid(),getppid());
                                            sleep(3);
                                    }
                            }
    
                    }
                    else
                    {
                            printf("do nothing \n");
                    }
    
            }
            return 0;
    }
    这里,在while(1)内不断地查询输入,如果不是1就什么都不干;
    如果是1就创建子进程且复制物理空间,此时父进程什么也不干,
    假如有了子进程1,那么子进程1永远在循环无法退出;此时的父进程那份空间由于没有死循环
    一直在查询用户输入,当用户再次输入1时,再创建子进程2,同样困死无法结束。
    这俩各自都拥有自己的物理空间,各自进行自己的死循环,而父进程一直在查询用户输入,用于创建子进程
    这些子进程是在争夺资源
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39

    在这里插入图片描述
    vfork与fork区别:

    fork创建的子进程会争夺CPU资源,不一定谁先谁后
    vfork:
    (1)保证子进程先运行,子进程调用exit(0)退出后父进程运行
    		子进程退出一定要调用退出函数
    		exit(0)推荐   
    		_Exit(0)  _exit(0)
    (2)使用父进程的存储空间(无自己的存储空间),不拷贝
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    父进程等待子进程退出:
    fork() + wait() = vfork();
    wait()等待子进程退出再运行父进程·并且回收子进程,避免子进程变为僵尸进程
    waitpid(pid_t pid, int *status, int options);//不等待子进程,子进程会变为僵尸进程
    第一个参数:进程的pid号,在父进程调用,这个pid即为子进程pid号。
    第二个参数:子进程退出状态存到该地址里
    第三个参数:多用WNOHANG
    
    
    int main()
    {
            int data = 0;
            pid_t pid;
                            pid = fork();
                            if(pid>0)
                            {
                                    wait(NULL);//只等待子进程退出并且回收,不关心退出状态
                                    
                                    //wait(&status);//子进程退出状态放到status里面
                                    //WEXITSTATUS(status)把子进程的退出状态解析到status里面如之前的1
                                    
                                    while(1)
                                    {
    
                                            printf("this is fork,pid is %d\n",getpid());
                                    }
                            }
                            if(pid==0)
                            {
                                    while(1)
                                    {
                                            data++;
                                            printf("this is child fork,pid is %d,my father pid id %d\n",getpid(),getppid());
                                            sleep(3);
                                            if(data==4)
                                            {
                                                    exit(0);
                                            }
                                    }
                            }
            return 0;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43

    子进程与父进程同时运行,过一会父进程退出,此时子进程变为孤儿进程,系统默认把init进程作为子进程的父进程(init进程默认pid为1)

    exec族函数:
    子进程使用exec调用另外一个程序来运行,该进程会被完全替换为该程序,进程号不变
    调用失败返回-1,调用成功无返回值
    第一个参数是:路径下哪个程序
    execl("./echoarg","echoarg","abc",NULL) == -1//当前目录下运行echoarg,后面是参数,参数必须以NULL结尾
    execl("/bin/ls","ls",NULL,NULL) //绝对路径下ls,传参
    execl("/bin/date","date",NULL,NULL) //调用系统时间
    whereis ls  查看ls的绝对路径
    
    配置环境变量的作用:系统执行程序时,都是先去环境变量目录下去寻找
    可以通过把当前路径配置到环境变量路径下,不要./相对路径了
    export PATH = $PATH:(pwd所指的绝对路径)
    
    execlp("date","date",NULL,NULL)
    多了个p相比于execl,p:PATH就是环境变量,即位于环境变量目录下的命令,不需要带绝对路径
    
    char  *argv[] = {"date",NULL,NULL};
    execvp("date",argv);//把参数作为argv,参数做成数组
    
    char  *argv[] = {"date",NULL,NULL};
    execv("/bin/date",argv);//把参数作为argv,参数做成数组
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    system:本质和execl一致,都是去执行一个shell脚本
    system(“./change  file”);//在当前路径下(即命令行)执行这样的shell脚本,调用change  file修改文件配置
    
    syetem与execl的区别:
    用system还会继续回到程序中执行接下来的代码,
    使用execl()程序不会回到代码中执行以下代码
    
    
    popen和system相比可以获取程序的运行输出结果
    ps r  筛选出正在运行的进程。
    #include 
    int main()
    {
            FILE *fp;
            char text[1024]={0};
            fp = popen("ps","r");//结果保存到fp的文件流中
            int nread = fread(text,1,1024,fp);//对文件里读取要用fread
            printf("read %d byte ,context is %s\n",nread,text);
            return 0;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    三、进程间通信
    在这里插入图片描述

    无名管道:创建的在内核
    有名管道:创建的空间可以以文件打开操作
    消息队列:队列号、进程号
    共享内存:
    信号量:P、V操作
    
    • 1
    • 2
    • 3
    • 4
    • 5
    我们希望进程一、二都有自己独立的空间,而在两进程中间建立某种联系空间,形成进程间通信
    进程间通信又分为单机版进程间通信与网络进程通信(两个不同主机的通信)
    其中单机进程通信有以下五种:
    1、无名管道(半双工管道):
    	同一时间数据只能在一个方向上流动,管道里数据读走就没了
    	创建的管道位于内核,不计入文件
    	
    	pipe是创建一个无名管道,当一个管道建立时,它会创建两个参数,fd[0]为读而打开,fd[1]为写而打开;要关闭管道,只需要关闭这两个文件描述符即可。
    	
    	要考虑的问题:从哪写给fd[1],把fd[0]读给谁?
    	
    	int pipe(int pipefd[2]);//管道创建失败返回-1
    	应用场景:创建管道并且创建父子进程,父进程(pid>0)负责往管道写(写的时候关闭读端),
    	子进程(pid<0)负责读(读的时候关闭写端),假如从管道读不到数据就堵塞,
    	
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    在内核中创立空间,不是文件,但是可以用write/read;
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    int main()
    {
            int fd[2];
            char readbuf[128] = {0};
            int mark = pipe(fd);
            pid_t pid = fork();
            if(pid>0)
            {
                    close(fd[0]);
                    write(fd[1],"hello from father",strlen("hello from father"));
                    wait(NULL);
            }
            if(pid == 0)
            {
    
                    close(fd[1]);
                    read(fd[0],readbuf,strlen("hello from father"));
                    printf("%s\n",readbuf);
                    exit(0);
            }
            return 0;
    }
    编程思想:父子进程同时运行,父亲打开写管道,写完等待子进程退出,假如父进程没写完
    子进程就读不到数据阻塞住,所以一定是父进程先运行的
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    2、命名管道FIFO(全双工管道):
    与无名管道不同的是:FIFO可以在无关进程间通信,且在文件系统中以文件形式存在
    无名管道只适用于父子进程的通信,管道问文件名,只存在于内存中,文件中无文件名
    一旦创建了一个FIFO就可以用一般的文件I/O进行操作
    mkfifo("./file",0600) //当前路径下创建file管道文件,权限是可读可写
    errno = EEXIST//管道创建失败,原因是已存在
    perror("why");//打印创建失败的原因
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    #include 
    #include 
    #include 
    #include 
    #include 
    int main()
    {
            if((mkfifo("./file",0600) == -1) && errno != EEXIST)//如果管道创建失败且失败的原因不是已存在
            {
                    printf("make fifo failed\n");
                    perror("why");
            }
            return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    当open一个fifo时,是否设置非阻塞标志O_NONBLOCK的区别:
    若没有指定(默认没有阻塞),只读open阻塞到其他进程为写而打开次fifo,只写open要阻塞到其他进程为读而打开次fifo。最好设置阻塞
    即FIFO要读取的时候,只有当其他进程要给FIFO写入数据时,才会顺利往下进行。
    FIFO适用于两个无关系进程间的通信
    
    • 1
    • 2
    • 3
    • 4
    FIFO的双方收发:
    //写入端代码
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    
    int main()
    {
            char *writebuf = "hello world";
            int fd = open("./file",O_WRONLY);
            write(fd,writebuf,strlen(writebuf));
            printf("write success\n");
            close(fd);
            return 0;
    }
    //读取端代码
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    
    int main()
    {
            char readbuf[32];
            if((mkfifo("./file",0600) == -1) && errno != EEXIST)//如果管道创建失败且
    失败的原因不是已存在
            {
                    printf("make fifo failed\n");
                    perror("why");
            }
            int fd = open("./file",O_RDONLY);
            read(fd,readbuf,32);
            printf("read success\n");
            printf("read context:%s\n",readbuf);
            return 0;
    }
    先运行读再运行写
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    3、消息队列
    创建后是在内核中创建消息队列,在内核中时不会消失的
    (1)创建或打开消息队列:成功返回队列ID,失败返回-1
    	int msgget(key_t key, int flag);//key 队列索引值,通过索引值找到队列;flag打开队列的方式(多用0)
    	msgget(0x1234,IPC_CREAT|0777)
    	
    (2)发送消息:成功返回0,失败返回-1
    	msgsnd(int msqid, const void *ptr, size_t size, int flag);
    	
    	msgid:由msgget函数返回的消息队列标识码
    	*ptr:准备发送的消息
    	size:发送的消息的长度(不包括消息类型的long int长整型)
    	flag:默认为0
    	
    (3)读取消息:成功返回消息数据的长度,失败返回-1
    	int msgrcv(int msqid, void *ptr, size_t size, long type,int flag);
    	!!!这里的type属于队列类型,即要在发送时定义发送内容与类型为一个结构体
    	
    (4)控制消息队列:成功返回0,失败返回-1
    	int msgctl(int msqid, int cmd, struct msqid_ds *buf);
    	msgctl(msgID,IPC_RMID,NULL);//移除队列
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    消息队列的双方收发:两个进程的结构为发收、收发。
    #include 
    #include 
    #include 
    
    struct msg
    {
            long type;
            char text[128];
    };
    int main()
    {
            struct msg readbuf;
            struct msg sendbuf = {888,"this is from send"};
            
            int msgID = msgget(0x1234,IPC_CREAT|0700);//确保得到同一个队列号
    
            msgsnd(msgID,&sendbuf,sizeof(sendbuf.text),0);
            printf("send success\n");
            msgrcv(msgID,&readbuf,sizeof(readbuf.text),998,0);
            printf("%s\n",readbuf.text);
            msgctl(msgID,IPC_RMID,NULL);
    
            return 0;
    }
    进程A创建888的队列号并且往里面发送数据,发送完成后等待998队列发送完消息然后读取
    
    
    #include 
    #include 
    #include 
    
    struct msg
    {
            long type;
            char text[128];
    };
    int main()
    {
            struct msg readbuf;
            struct msg sendbuf = {998,"this is from write"};
            
            int msgID = msgget(0x1234,IPC_CREAT|0700);//确保得到同一个队列号
    
            msgrcv(msgID,&readbuf,sizeof(readbuf.text),888,0);
            printf("%s\n",readbuf.text);
            msgsnd(msgID,&sendbuf,sizeof(sendbuf.text),0);
            printf("send success\n");
    
            return 0;
    }
    进程B创建998的队列号并且往里面发送数据,发送完成后等待888队列发送完消息然后读取
    !!!注:这里的队列索引值一定要相同
    
    前面把队列号强制为某个数,这样的做法显然不是很明智,可以这样做:
    key_t key;//定义一个键值
    key = ftok(".",'z');//获取当前队列号
    printf("key is %x\n",key);
    msgget(key,IPC_CREAT|0777)
    
    当两个进程同时自动获取队列索引时,系统会自动分配成功的,最后一个消息队列读完要记得移除消息队列
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    
    4、共享内存
    开发步骤:获取共享内存,连接共享内存,给共享内存写入数据,断开与共享内存的连接
    
    (1)创建或获取一个共享内存:成功返回共享内存ID,失败返回-1
    	key_t key;
        key = ftok(".",1);
        
    	int shmget(key_t key, size_t size, int flag);
    	(1)生成键值
    	(2)开辟的内存大小必须以M为基本单位
    	(3)共享内存权限
    	
    (2)连接共享内存到当前进程的地址空间:成功返回指向共享内存的指针,失败返回-1
    	  void *shmat(int shm_id, const void *addr, int flag);//获取共享内存地址
    	 (1)共享内存的ID
    	 (2)一般写0,让系统自动分配共享内存的地址
    	 (3)一般写0,内存可读可写
    	 
    	 给共享内存写入数据:
    	 strcpy(shmaddr,"hwx")
    	 
    (3) 断开与共享内存的连接:成功返回0,失败返回-1
     	  int shmdt(void *addr); 
     	(1)共享内存连接成功后返回的指针
     	
    (4)卸载共享内存:成功返回0,失败返回-1
     int shmctl(int shm_id, int cmd, struct shmid_ds *buf);
     (1)共享内存ID
     (2)IPC_RMID,一般写这个
     (3)不关心这个写0
    
    ipcs -m查看共享内存号
    ipsrm -m  共享内存ID:移除共享内存
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    
    5、信号量
    	比如你正在看电视,这个时候外卖到了,你需要放下手边的工作去处理这个事情,这就是一个信号
    	类似于软中断(模拟中断)。
    	假如有两个进程,进程二给进程一一个信号,那么进程1对相应的信号做出一个应对,同样信号也有优先级,
    	看优先处理哪个信号,类似中断。可用kill -l 查看信号的名字及序号。kill -9 信号ID杀死信号。
    
    	例如:单片机和PC进行数据交互时,PC给单片机的串口一个信号,单片机的串口得知有信号过来,
    	触发一个中断进而执行Pc命令,
    	
    	信号都是以SIG开头的,信号的处理有三种办法,忽略,捕捉,默认。
    	kill -9 进程ID   杀死进程,使用信号编号和名字都可以
    	kill  -l查看所有信号,10、12 SIGURE是可以用户自定义信号   共62个信号
    	SIGINT:默认是结束进程的(ctrl+c)
    	SIGKILL和SIGSTOP是不能被忽略和捕捉的
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    sighandler_t signal(int signum, sighandler_t handler)
    第一个参数signum:表示要捕捉哪个信号。  
    第二个参数handler:函数指针,当捕捉到这个信号时,执行信号处理函数
    子进程杀死后,父进程不回收会造成僵尸进程
    
    1、初级信号编程(信号不携带消息)
    捕捉信号:
    #include 
    #include 
    void handler(int signum)//捕捉到信号时自动把信号代数送进去
    {
            printf("get signum is %d\n",signum);
            printf("never quit\n");
    }
    int main()
    {
            signal(SIGINT,SIG_IGN);//第一个参数是要忽略的信号,第二个参数是忽略宏定义
             signal(2,SIG_IGN);
            signal(SIGINT,handler);//捕捉到SIGNAL指令时,自动执行handler指令
            signal(SIGKILL,handler);
            while(1);
            return 0;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    
    int kill(pid_t pid, int sig);
    1、pid_t:指定信号的pid号
    2、sig:信号的编号
    自己封装的命令可以提到/bin下面使用,sudo   mv  t_kill  /bin
    杀死信号:
    两种方式完成
    #include 
    #include 
    
    int main(int argc,char* argv[3])//命令行的都是字符串形式
    {
            int signum;
            int pid;
            char cmd[128];
            signum = atoi(argv[1]);//字符串转换为整型数的函数
            pid = atoi(argv[2]);
            printf("num = %d,pid = %d\n",signum,pid);
            sprintf(cmd,"kill -%d %d",signum,pid);//做出一个cmd指令,是kill xx xx型的,>参数是后面的
            //kill(pid,signum);//类似 kill  9  pid
            system(cmd);//让系统运行杀死指令
            printf("send ok\n");
            return 0;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    alarm(seconds);闹钟信号
    alarm(1);//取消闹钟
    
    
    #include 
    #include 
    
    int main()
    {
            int i;
            alarm(1);//到1s就终止进程,闹钟信号
            for(i=1;i>0;i++)
            {
                    printf("%d\n",i);
            }
            return 0;
    }
    
    忽略信号:
    
    	signal(SIGINT,SIG_IGN);//第一个参数是要忽略的信号,第二个参数是忽略宏定义
    	即收到ctrl+c时,忽略此信号,SIG_IGN护理的宏定义
    
    前面三种信号类似于只收到信号动作,比如敲门知道中断,而不知道这个敲门意味着什么。
    发信号:kill
    操作信号:signal()
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    
    2、高级信号编程(信号携带消息)
    解决问题:
    发送:用什么发送信号,信号如何携带消息
    接受:用什么接受信号,如何读出消息
    用sigaction来收信号,用sigqueqe来发送信号。
    int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
    (1)第一个参数表示接受的信号。
    (2)第二个参数表示收到这个信号想干嘛,需要构造结构体
    (3)第三个参数表示备份,通常NULL
    
    ```c
    //发送信号
    #include 
    #include 
    //       int sigqueue(pid_t pid, int sig, const union sigval value);
    int main(int argc,char **argv)
    {
    	int signum;
    	int pid;
    	signum = atoi(argv[1]);//信号代号,SIGUSR1代号是10
    	pid = atoi(argv[2]);//要发送到的进程号
    	
    	union sigval value;
    	value.sival_int = 100;//写入信号要携带的消息
    	
    	sigqueue(pid,signum,value);//给这个进程发送数据
    	
    	printf("%d done\n",getpid());
    	return 0;
    }
    发送信号步骤:
    1、构造联合体union sigval value;
    2、联合体里面的value里面传输要发送的内容
    3、pid:给哪个进程发信号  signum:给这个进程发哪个信号
    
    
    //接收信号
    开发流程:配置act结构体、配置处理函数
    #include 
    #include 
    void   handler (int signum, siginfo_t *info, void *context)
    {
            printf("get signum = %d\n",signum);
            if(context != NULL)
            {
                    printf("get data = %d\n",info->si_int);//这里是指针所以要用->,收到信号内容
                    printf("get data = %d\n",info->si_value.sival_int);//同样是信号内容
                    printf("from %d\n",info->si_pid);//发送者的pid
            }
    }
    int main()
    {
            struct sigaction act;
            printf("pid = %d\n",getpid());
            
            act.sa_flags = SA_SIGINFO;//要收信号就把这个参数设置为SA_SIGINFO
            act.sa_sigaction = handler;//收到信号处理该函数
            
            sigaction(SIGUSR1,&act,NULL);//接收SIGUSR1信号,SIGUSR1代号是10,接收到信号放act里面解析
            
            while(1);
            return 0;
    }
    
    
    接收信号步骤:1、sigaction()收到SIGUSR1信号执行act,第三个参数用来备份,
    			 2、要接收信号,必须把act构造为结构体,并且设置里面参数为
    			 act.sa_flags = SA_SIGINFO;//要收信号就把这个参数设置为SA_SIGINFO
            	 act.sa_sigaction = handler;//收到信号处理该函数
            	 3、handler收到该信号先把该信号值打出来
            	 4、判断context内容,如果有内容就把info里面的数据打出来
    
    先运行接收信号知道该进程的pid号,一直不退出;再运行发送信号,发给接收进程一个信号,
    写入value信息。
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    一次仅一个进程使用的就是临界资源,如打印机
    设想一个场景:有一个房间,有一把钥匙,当A拿走要是进入该房间时,B不能进入到该房间,
    只有当A把钥匙放回,B才能进入该房间。
    钥匙就叫信号量,房间就称为临界资源,共享内存就是一种临届资源,当处理A进程时,B进程不能被处理。
    p操作:拿锁   V操作:放回锁
    
    #include 
    #include 
    #include 
    #include 
    union semun
     {
           int              val;    /* 信号量个数 */
           struct semid_ds *buf;    /* Buffer for IPC_STAT, IPC_SET */
           unsigned short  *array;  /* Array for GETALL, SETALL */
           struct seminfo  *__buf;  /* Buffer for IPC_INFO
                                               (Linux-specific) */
    };
    void pGetKey(int id)//拿走锁,最终目的:锁数量-1
    {
    	struct sembuf set;
    	set.sem_num=0;//对第0个锁进行操作
    	set.sem_op=-1;//拿走锁,锁数量-1
    	set.sem_flg=SEM_UNDO;//一般都用这个
    	semop(id,&set,1);//
    	printf("getKey\n");
    }
    void vPutKey(int id)//放回锁,最终目的:锁数量+1
    {
    	struct sembuf set;
    	set.sem_num=0;//对第0个锁进行操作
    	set.sem_op=1;//放回锁,锁数量+1
    	set.sem_flg=SEM_UNDO;//一般都用这个
    	semop(id,&set,1);//
    	printf("putKey\n");
    }
    int main()
    {
    	key_t key;
    	int semid;
    	int pid;
    	union semun initsem;
    	key = ftok(".",2);
    	semid = semget(key,1,IPC_CREAT|0666);//信号量创建成功,返回信号量ID
    	
    	initsem.val = 0;//初始化锁数量
    	semctl(semid,0,SETVAL,initsem);//初始化信号量
    	
    	pid = fork();
    	if(pid>0)
    	{
    		pGetKey(semid);
    		printf("this is father\n");
    		vPutKey(semid);
    	}
    	if(pid == 0)
    	{
    		printf("this is child\n");
    		vPutKey(semid);
    	}
    	return 0;
    }
    
    首先对创建信号量,并且获取信号量ID,其次初始化信号量(锁为0),创建P,V操作,创建进程,
    父进程先拿锁(锁数量为0,拿不到),只有等子进程放锁,父进程才能拿到,巧妙地设计了一个
    让子进程先运行的案例。
    
    对信号量的操作,唯一的操作也就是父子进程如何拿锁,P、V操作都是固定的构造好的。
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68

    四、多线程

    多线程开发包含三种:线程、互斥锁、条件
    (1)线程操作分为:创建、退出、等待
    (2)互斥锁操作分为:创建、销毁、加锁、解锁
    (3)条件操作有五种:创建、销毁、触发、等待、广播
    
    一个线程死掉,整个进程死掉,多进程程序比多线程程序健壮
    使用多线程的理由之一是和进程相比,它是一种非常"节俭"的多任务操作方式。我们知道,在Linux系统下,
    启动一个**新的进程必须分配给它独立的地址空间,建立众多的数据表来维护它的代码段、堆栈段和数据段,
    这是一种"昂贵"的多任务工作方式。而运行于一个进程中的**多个线程,它们彼此之间使用相同的地址空间,
    共享大部分数据,启动一个线程所花费的空间远远小于启动一个进程所花费的空间**,而且,**线程间彼此
    切换所需的时间也远远小于进程间切换所需要的时间。
    	不论是不是写时copy,都会重新开辟空间供子进程运行,而一个进程中多线程是共享进程的空间。
    	同一进程的空间多线程进行共享。
    	
    使用多线程的理由之二是线程间方便的通信机制。对不同进程来说,它们具有独立的数据空间,要进行数据
    的传递只能通过通信的方式进行,这种方式不仅费时,而且很不方便。线程则不然,由于同一进程下的线
    程之间共享数据空间,所以一个线程的数据可以直接为其它线程所用,这不仅快捷,而且方便
    	线程间通信更加方便,进程间通信耗CPU。
    
    
    pthread_self():线程ID号获取
    getpid():进程ID号获取
    1、pthread_create(pthread_t* thread, pthread_attr_t * attr, void *(*start_routine)(void *), void * arg);
    (1)创建的线程名字
    (2)常常NULL
    (3)线程执行的函数
    (4)给该函数传参
    	如果该函数参数不止一个,那就把该函数的参数做成一个结构体,把该结构体地址作为参数传进去。
    创建成功返回0
    
    2、pthread_join(pthread_t pthid, void **thread_return);
    (1)等待的线程名字。
    (2)thread_return 是一个传出参数,接收线程函数的返回值。类型(void **)
    3、pthread_exit((void*)p);//线程退出,并且返回p的值为无类型
    
    多线程空间共享代码验证:
    注:进行编译时一定要链接 -lpthread库
    #include 
    #include 
    int g_data = 0;
    void *func1(void* arg)
    {
            //static char* p = "t1 is run over";//必须要用static
            printf("t1:%ld pthread is creat\n",(unsigned long)pthread_self());//获取t1线程号
            printf("t1.arg = %d\n",*(int*)arg);//把arg转化为int*型,然后取内容
            //pthread_exit((void*)p);
            while(1)
            {
                    printf("t1:%d\n",g_data++);
                    sleep(1);
            }
    }
    
    void *func2(void* arg)
    {
            //static char* p = "t1 is run over";//必须要用static
            printf("t2:%ld pthread is creat\n",(unsigned long)pthread_self());//获取t1线程号
            printf("t2.arg = %d\n",*(int*)arg);//把arg转化为int*型,然后取内容
            //pthread_exit((void*)p);
            while(1)
            {
                    printf("t2:%d\n",g_data++);
                    sleep(1);
            }
    }
    int main()
    {
            int ret1;//定义线程返回标志
            int ret2;
            int param = 100;//给线程传参
            pthread_t t1;
            pthread_t t2;
            //      char* pret = NULL;
            ret1 = pthread_create(&t1,NULL,func1,(void *)¶m);
            ret2 = pthread_create(&t2,NULL,func2,(void *)¶m);
            if(ret1 == 0 && ret2 == 0)
            {
                    printf("main: pthred is creat success\n");
            }
            printf("main: %ld\n",(unsigned long)pthread_self());//获取主函数线程号
            while(1)
            {
                    printf("main:%d\n",g_data++);
                    sleep(1);
            }
            pthread_join(t1,NULL);//pret是为了获取线程退出所返回的内容,无返回值则NULL,等待线程退出再往下执行,否则主进程直接退出,线程来不及运行就退
    出。
            pthread_join(t2,NULL);
    //      printf("main is run over,%s\n",pret);
            return 0;
    }
    
    互斥锁造成死锁:两个线程都有自己的锁,想在对方未解锁时还要拿到对方的锁就会造成死锁。
    
    条件锁:t1只运行到g_data=3的时候
    ```c
    pthread_cond_signal(&cond);//条件发生
    pthread_cond_wait(&cond,&mutex);//运行等待处以后的代码
    
    #include 
    #include 
    int g_data = 0;
    pthread_mutex_t mutex;//创建互斥锁
    pthread_cond_t cond;//创建条件线程
    void *func1(void* arg)
    {
    
    	printf("t1:%ld pthread is creat\n",(unsigned long)pthread_self());//获取t1线程号
    	printf("t1.arg = %d\n",*(int*)arg);//把arg转化为int*型,然后取内容
    	while(1)
    	{
    		pthread_cond_wait(&cond,&mutex);//等待上锁条件,条件满足才运行,无条件则阻塞,那t2便获得运行权限
    		printf("========t1 run=======");
    		printf("%d\n",g_data);
    		g_data = 0;
    		sleep(1);
    	}
    	
    }
    void *func2(void* arg)
    {
    	
    	printf("t2:%ld pthread is creat\n",(unsigned long)pthread_self());//获取t2线程号
    	printf("t2.arg = %d\n",*(int*)arg);//把arg转化为int*型,然后取内容
    	while(1)//假设t2先运行
    	{
    		
    		printf("t2:%d\n",g_data);
    		pthread_mutex_lock(&mutex);
    		g_data++;
    		
    		if(g_data == 3)
    		{
    			pthread_cond_signal(&cond);//(达到上锁条件)满足条件,转到func1里的wait
    		}
    		pthread_mutex_unlock(&mutex);
    		sleep(1);//在这段时间里t1可与t2竞争获得锁权限
    	}
    }
    int main()
    {
    	int ret;//定义线程返回标志
    	int param = 100;//给线程传参
    	
    	pthread_t t1;
    	pthread_t t2;
    	
    	pthread_mutex_init(&mutex,NULL);//初始化锁
    	pthread_cond_init(&cond,NULL);//初始化条件锁
    	
    	ret = pthread_create(&t1,NULL,func1,(void *)¶m);
    	ret = pthread_create(&t2,NULL,func2,(void *)¶m);
    
    	pthread_join(t1,NULL);//pret是为了获取线程退出所返回的内容,无返回值则NULL,等待线程退出再往下执行,否则主进程直接退出,线程来不及运行就退出。
    	pthread_join(t2,NULL);
    	
    	pthread_mutex_destroy(&mutex);//销毁锁
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 152
    • 153
    • 154
    • 155
    • 156
    • 157
    • 158
    • 159

    加入t1先运行,那么t1则阻塞,此时t2获取锁运行,当t2把参量运行到3时,t1达到运行条件开始运行,运行完参量归0,延时1秒,此时t2开始运行。

    网络通信:
    1、套接字
    网络地址包括IP地址和端口号
    多人聊天室:
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
  • 相关阅读:
    每个 Flutter 开发者都应该知道的一些原则
    为什么不推荐在Spring Boot中使用@Value加载配置
    微信小程序开发校园第二课堂+后台管理系统|前后分离VUE.js在线学习网
    shiro关于认证的学习
    Golang专题——fsnotify 文件及目录监控
    msvcp140.dll是什么?msvcp140.dll丢失的有哪些解决方法
    hyoerf手记
    C++面试经典100问
    vue3: 2.如何利用 effectScope 自己实现一个青铜版pinia 一 getters篇
    企业申报“专精特新”,对知识产权有哪些要求?
  • 原文地址:https://blog.csdn.net/qq_54017644/article/details/127579724