• 一文让你玩转Linux多进程开发


    Linux多进程开发

    主要介绍多进程开发时的要点

    进程状态转换

    进程反应了进程执行的变化。

    进程的状态分为三种 ,运行态,阻塞态,就绪态

    在五态模型中分为以下几种,新建态,就绪态,运行态,阻塞态,终止态。

    进程状态.png

    运行态:进程占用处理器正在运行。

    就绪态:进程已具备运行的条件,等待系统分配处理器运行。

    阻塞态 :又称为等待(wait)态,或睡眠(sleep)态,指进程不具备运行条件,正在等待事件的完成。

    新建态:进程已被创建,还未加入就绪队列。

    进程相关命令

    查看进程

    ps aux / ajx

    实时查看进程状态

    top

    杀死进程

    kill
    kill -l 列出所有信号
    kill -9 进程id
    killall 根据进程名杀死进程
    kill -SICKILL  进程 id
    ​
    ​

    进程创建

    系统允许一个进程创建新进程,新进程即为子进程,子进程还可以创建新的子进程,形成 进程树结构模型

    pid_t fork(void);//pid_t为int类型,进行了重载
    pid_t getpid();// 获取当前进程的 pid 值。
    pid_t getppid(); //获取当前进程的父进程 pid 值。

    通过系统调用获取进程标识符

    进程id: (PID)

    父进程id: (PPID)

    #include 
    #include 
    #include 
    #include 
    #include 
    using namespace std;
    ​
    int main()
    {
        printf("pid: %d\n", getpid());
        printf("ppid: %d\n", getppid());
    }

    通过系统调用创建进程 fork

    fork 返回值

    fork的返回值会返回两次,一次是父进程,一次是子进程

    在父进程中返回子进程的id

    子进程返回0

    通过 fork的返回值来判断子进程和父进程

    fork创建进程失败会返回 -1 并设置 error

    总结 父子进程代码共享,数据各自开辟空间,私有一份(采用写时拷贝)

    #include 
    #include 
    #include 
    #include 
    #include 
    using namespace std;
    ​
    int main()
    {
        pid_t pid = fork();
        if (pid > 0)
        {
            printf("父进程 id = %d , 子进程 ppid = %d\n", getpid(), getppid());
            // 当前是父进程 , 返回创建子进程的进程号
        }
        else
        {
            // 当前是子进程
            printf("子进程 id = %d , ppid = %d\n", getpid(), getppid());
        }
    ​
        for (int i = 0; i < 3; i++)
        {
            printf("i = %d \n", i);
        }
    }

    exec函数族

    exec函数族的作用是根据指定的文件名找到可执行文件,并用它来取代调用进程的内容。换句话说就是在进程内部调用一个可执行文件

    #include 
    extern char **environ;
    ​
    int execl(const char *path, const char *arg, ...); // 可执行文件的路径,文件名 ,参数
    int execlp(const char *file, const char *arg, ...);// 会到环境变量里面查找可执行文件
    int execle(const char *path, const char *arg,..., char * const envp[]);
    int execv(const char *path, char *const argv[]);
    int execvp(const char *file, char *const argv[]);
    int execvpe(const char *file, char *const argv[],char *const envp[]);

    返回值:只有调用错误或调用失败则会返回-1,并设置error

    execl.cpp

    #include 
    #include 
    #include 
    #include 
    #include 
    using namespace std;
    ​
    int main()
    {
        pid_t pid = fork();
        if (pid > 0) {
    ​
            // 父进程
            cout << "我是父进程" << ' ' << getpid()<进程退出、孤儿进程、僵尸进程 
    

    93b7bc5766f14b05961ab3378fa9fbbc.png

    1.进程退出

    void exit(int status)  // 标准C库函数
    void _exit(int status) // Linux系统标准库函数

    status:进程退出码

    _exit是属于 POSIX 系统调用,适用于 UNIX 和 Linux 系统。调用该系统调用后会导致当前进程直接退出,且函数不会返回。内核会关闭该进程打开的文件描述符,若还存在子进程,则交由1号进程领养,再向进程的父进程发送 SIGCHLD 信号。

    返回值:无


    2.孤儿进程

    父进程已经结束了运行,子进程还未停止运行,这样的进程就称为孤儿进程

    每当出现一个孤儿进程的时候,系统会把孤儿进程的父进程设置为init

    #include 
    #include 
    #include 
    int main()
    {
     pid_t id = fork();
     if(id < 0){
     perror("fork");
     return 1;
     }
     else if(id == 0){//child
     printf("I am child, pid : %d\n", getpid());
     sleep(10);
     }else{//parent
     printf("I am parent, pid: %d\n", getpid());
     sleep(3);
     exit(0);
     }
     return 0;
    }

    3.僵尸进程

    每个进程结束后,都会去释放自己用户区的资源;内核区的PCB无法释放,需要父进程去释放

    进程终止后,父进程尚未进行回收,子进程残留资源(PCB)存放在内核中,变成僵尸进程。

    #include 
    
    #include 
    
    #include 
    
    #include 
    
    int main()
    
    {
    
        pid_t pid;
    
        // 循环创建子进程
    
        while (1)
    
        {
    
            pid = fork();
    
            if (pid < 0)
    
            {
    
                perror("fork error:");
    
                exit(1);
            }
    
            else if (pid == 0)
    
            {
    
                printf("I am a childprocess.\nI am exiting.\n");
    
                // 子进程退出,成为僵尸进程
    
                exit(0);
            }
    
            else
    
            {
    
                // 父进程休眠20s继续创建子进程
    
                sleep(20);
    
                continue;
            }
        }
    
        return 0;
    }

    以上代码就是一个典型的僵尸进程

    使用命令杀死僵尸进程

    grep -v grep | cut -c 5-10 | xargs kill -9

    wait 函数

    C 语言中的 wait 函数

    wait 函数是符合 POSIX 标准的系统调用的封装器,定义在 头文件中。该函数用于等待子进程的程序状态变化,并检索相应的信息。wait 通常在创建新子进程的 fork 系统调用之后调用。wait 调用会暂停调用程序,直到它的一个子进程终止。

    用户应该将代码结构化,使调用进程和子进程有两条不同的路径。通常用 if...else 语句来实现,该语句评估 fork 函数调用的返回值。注意 fork 在父进程中返回子进程 ID,一个正整数,在子进程中返回 0。如果调用失败,fork 将返回-1

    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    using namespace std;
    
    int main()
    {
        pid_t pid;
    
        // 创建5个子进程
        for (int i = 0; i < 5; i++)
        {
            pid = fork();
            if (pid == 0)
            {
                break;
            }
        }
    
        if (pid > 0)
        {
            // 父进程
            while (1)
            {
                printf("parent, pid = %d\n", getpid());
    
                // int ret = wait(NULL);
                int st;
                int ret = wait(&st);
    
                if (ret == -1)
                {
                    break;
                }
    
                if (WIFEXITED(st))
                {
                    // 是不是正常退出
                    printf("退出的状态码:%d\n", WEXITSTATUS(st));
                }
                if (WIFSIGNALED(st))
                {
                    // 是不是异常终止
                    printf("被哪个信号干掉了:%d\n", WTERMSIG(st));
                }
    
                printf("child die, pid = %d\n", ret);
    
                sleep(1);
            }
        }
        else if (pid == 0)
        {
            // 子进程
            while (1)
            {
                printf("child, pid = %d\n", getpid());
                sleep(1);
            }
    
            exit(0);
        }
    
        return 0; // exit(0)
    }

    waitpid函数

    #include  
    #include 
    pid_t waitpid(pid_t pid,int *status,int options);

    功能:回收指定进程号的子进程,可以设置是否阻塞

    参数 : -pid

    参数功能
    pid<0某个子进程的id
    pid = 0回收当前进程组的所有进程
    pid = -1回收所有子进程相当于 wait
    pid < -1回收某个进程组的id

    返回值

    valuereturn value
    > 0返回子进程的id
    =0WNOHANG表示还要子进程或者
    -1错误,没有子进程了
    #include 
    #include 
    #include 
    #include 
    #include 
    int main()
    {
        pid_t pid, childpid;
        int status = 0;
        pid = fork();
        if (pid < 0)
            printf("Error occured on forking.\n");
        else if (pid == 0)    //子进程
        {
            sleep(3);   //换成30s,然后kill -9 子进程pid
            exit(0);
        }   
        else                    //父进程
        {
            //返回后继续执行父进程的代码段
        }
        printf("pid:%d\n",pid); //打印子进程id
    
        do
        {
            childpid = waitpid(pid, &status, WNOHANG);
            if (childpid == 0)
            {
                printf("No child exited,ret = %d\n", childpid);
                sleep(1);
            }
        } while (childpid == 0);
        if (WIFEXITED(status))
            printf("正常退出:%d\n",childpid);
        if(WIFSIGNALED(status) && WTERMSIG(status) == SIGKILL)
            printf("被SIGKILL信号结束\n");
    }
    

    进程间通信

    进程是一个独立的资源分配单元,不同进程之间的资源是独立的;没有关联,不能在一个进程中访问另一个进程的资源。

    进程间通信的目的:

    1.数据传输

    2.通知事件

    3.进程控制

    4.资源共享

    进程间通信的方式

    进程间通信的方式.png

    匿名管道

    匿名管道也叫无名管道,它是UNIX系统最古老的IPC(进程间通信)的方式

    所有的UNIX系统都支持这种通信机制

    管道的特点:

    管道的特点.png


    父子进程通过匿名管道通信

    创建匿名管道

    #include
    
    int pipe(int filedes[2]);

    返回值:成功,返回0,否则返回-1。参数数组包含pipe使用的两个文件的描述符。fd[0]:读管道,fd[1]:写管道

    注意:匿名管道只能用于具有关系进程之间的通信

    #include 
    #include 
    #include 
    #include 
    #include 
    #include
    using namespace std;
    int main()
    {
        int pipefd[2];
        int ret = pipe(pipefd);
        if (ret == -1)
        {
            perror("pipe");
            return -1;
        }
    
        pid_t pid = fork();
        if (pid > 0)
        {
            char buff[1024]{0};
            while(1){
                 printf("开始读数据............\n");
                int len = read(pipefd[0], buff, sizeof(buff)); // read 默认阻塞
                sleep(1);
            printf("prcv = %s ,pid = %d \n", buff, getpid());
            }
            
        }
        else if (pid == 0)
        {
           
            while(1){
                 printf("开始写数据中.........\n");
            const char *str = "hello world";
            write(pipefd[1], str, strlen(str));
            sleep(1);
            }
            
        }
    }
    

    查看缓冲区大小命令

    ulimit -a

    查看缓冲区大小函数

    与pathconf函数功能一样,只是第一个参数不一样,pathconf的第一个参数 pathname是路径名,数据类型是字符数组指针,而fpathconf的第一个参数 files是一个已打开文件的文件标识符,数据类型是一个整数。两个函数的第二个参数name完全一样。

    #include 
    
    long fpathconf(int fd, int name);
    
    #include 
    #include 
    #include 
    #include 
    #include 
    #include
    using namespace std;
    int main()
    {
          int pipefd[2];
          int ret = pipe(pipefd);
          long size = fpathconf(pipefd[0],_PC_PIPE_BUF);
           printf("pipe size = %d\n",size);
    }
    

    匿名管道通信案例

    #include 
    #include 
    #include 
    #include 
    #include 
    #include
    #include
    using namespace std;
    int main()
    {
           // 创建一个管道
        int fd[2];
        int ret = pipe(fd);
    
        if(ret == -1) {
            perror("pipe");
            exit(0);
        }
    
        // 创建子进程
        pid_t pid = fork();
    
        if(pid > 0) {
            // 父进程
            // 关闭写端
            close(fd[1]);
            // 从管道中读取
            char buf[1024] = {0};
    
            int len = -1;
            while((len = read(fd[0], buf, sizeof(buf) - 1)) > 0) {
                // 过滤数据输出
                printf("%s", buf);
                memset(buf, 0, 1024);
            }
    
            wait(NULL);
    
        } else if(pid == 0) {
            // 子进程
            // 关闭读端
            close(fd[0]);
    
            // 文件描述符的重定向 stdout_fileno -> fd[1]
            dup2(fd[1], STDOUT_FILENO);
            // 执行 ps aux
            execlp("ps", "ps", "aux", NULL);
            perror("execlp");
            exit(0);
        } else {
            perror("fork");
            exit(0);
        }
    
    }
    

    管道的读写特点和管道设置为非阻塞

    管道的读写特点: 使用管道时,需要注意以下几种特殊的情况(假设都是阻塞I/O操作) 1.所有的指向管道写端的文件描述符都关闭了(管道写端引用计数为0),有进程从管道的读端 读数据,那么管道中剩余的数据被读取以后,再次read会返回0,就像读到文件末尾一样。

    2.如果有指向管道写端的文件描述符没有关闭(管道的写端引用计数大于0),而持有管道写端的进程

    也没有往管道中写数据,这个时候有进程从管道中读取数据,那么管道中剩余的数据被读取后, 再次read会阻塞,直到管道中有数据可以读了才读取数据并返回。

    3.如果所有指向管道读端的文件描述符都关闭了(管道的读端引用计数为0),这个时候有进程 向管道中写数据,那么该进程会收到一个信号SIGPIPE, 通常会导致进程异常终止。

    4.如果有指向管道读端的文件描述符没有关闭(管道的读端引用计数大于0),而持有管道读端的进程 也没有从管道中读数据,这时有进程向管道中写数据,那么在管道被写满的时候再次write会阻塞, 直到管道中有空位置才能再次写入数据并返回。总结: 读管道: 管道中有数据,read返回实际读到的字节数。 管道中无数据: 写端被全部关闭,read返回0(相当于读到文件的末尾) 写端没有完全关闭,read阻塞等待 写管道: 管道读端全部被关闭,进程异常终止(进程收到SIGPIPE信号) 管道读端没有全部关闭: 管道已满,write阻塞 管道没有满,write将数据写入,并返回实际写入的字节数

    案例

    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    /*
        设置管道非阻塞
        int flags = fcntl(fd[0], F_GETFL);  // 获取原来的flag
        flags |= O_NONBLOCK;            // 修改flag的值
        fcntl(fd[0], F_SETFL, flags);   // 设置新的flag
    */
    int main() {
    
        // 在fork之前创建管道
        int pipefd[2];
        int ret = pipe(pipefd);
        if(ret == -1) {
            perror("pipe");
            exit(0);
        }
    
        // 创建子进程
        pid_t pid = fork();
        if(pid > 0) {
            // 父进程
            printf("i am parent process, pid : %d\n", getpid());
    
            // 关闭写端
            close(pipefd[1]);
            
            // 从管道的读取端读取数据
            char buf[1024] = {0};
    
            int flags = fcntl(pipefd[0], F_GETFL);  // 获取原来的flag
            flags |= O_NONBLOCK;            // 修改flag的值
            fcntl(pipefd[0], F_SETFL, flags);   // 设置新的flag
    
            while(1) {
                int len = read(pipefd[0], buf, sizeof(buf));
                printf("len : %d\n", len);
                printf("parent recv : %s, pid : %d\n", buf, getpid());
                memset(buf, 0, 1024);
                sleep(1);
            }
    
        } else if(pid == 0){
            // 子进程
            printf("i am child process, pid : %d\n", getpid());
            // 关闭读端
            close(pipefd[0]);
            char buf[1024] = {0};
            while(1) {
                // 向管道中写入数据
                char * str = "hello,i am child";
                write(pipefd[1], str, strlen(str));
                sleep(5);
            }
            
        }
        return 0;
    }
    
    
    

    有名管道

    创建管道的方式

    通过命令创建有名管道

    mkfifo name

    通过函数创建有名管道

    #include 
    #include 
    
    int  mknod( const char  * pathname ,  mode_t  mode ,  dev_t  dev);
     int  mkfifo( const char  * pathname ,  mode_t  mode);

    一旦使用mkfifo 创建了一个FIFO,就可以使用open函数打开了,常见的文件IO函数都可以使用。

    使用命令创建管道

    mkfifo tt

    命令创建管道.png

    使用函数创建管道

    int main()
    {
          int ret = mkfifo("test1",0664);
           if(ret == -1){
             perror("mkfifo");
             exit(0);
           }
         return 0;
    }
    

    有名管道的创建.png

    进程管道读写测试

    write.cpp

    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    
    // 向管道中写数据
    /*
        有名管道的注意事项:
            1.一个为只读而打开一个管道的进程会阻塞,直到另外一个进程为只写打开管道
            2.一个为只写而打开一个管道的进程会阻塞,直到另外一个进程为只读打开管道
    
        读管道:
            管道中有数据,read返回实际读到的字节数
            管道中无数据:
                管道写端被全部关闭,read返回0,(相当于读到文件末尾)
                写端没有全部被关闭,read阻塞等待
    
        写管道:
            管道读端被全部关闭,进行异常终止(收到一个SIGPIPE信号)
            管道读端没有全部关闭:
                管道已经满了,write会阻塞
                管道没有满,write将数据写入,并返回实际写入的字节数。
    */
    int main()
    {
    
        // 1.判断文件是否存在
        int ret = access("test", F_OK);
        if (ret == -1)
        {
            printf("管道不存在,创建管道\n");
    
            // 2.创建管道文件
            ret = mkfifo("test", 0664);
    
            if (ret == -1)
            {
                perror("mkfifo");
                exit(0);
            }
        }
    
        // 3.以只写的方式打开管道
        int fd = open("test", O_WRONLY);
        if (fd == -1)
        {
            perror("open");
            exit(0);
        }
    
        // 写数据
        for (int i = 0; i < 100; i++)
        {
            char buf[1024];
            sprintf(buf, "hello, %d\n", i);
            printf("write data : %s\n", buf);
            write(fd, buf, strlen(buf));
            sleep(1);
        }
    
        close(fd);
    
        return 0;
    }

    read.cpp

    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    
    // 从管道中读取数据
    int main() {
    
        // 1.打开管道文件
        int fd = open("test", O_RDONLY);
        if(fd == -1) {
            perror("open");
            exit(0);
        }
    
        // 读数据
        while(1) {
            char buf[1024] = {0};
            int len = read(fd, buf, sizeof(buf));
            if(len == 0) {
                printf("写端断开连接了...\n");
                break;
            }
            printf("recv buf : %s\n", buf);
        }
    
        close(fd);
    
        return 0;
    }

    内存映射

    内存映射是将磁盘中的数据映射到内存当中,用户通过修改内存就能修改磁盘文件

    20130612185647406.jpg

    函数原型

    #include
    #include //这里提供类型pid_t和size_t的定义
    #include 
    #include 
    void* mmap(void* start,size_t length,int prot,int flags,int fd,off_t offset);
    int munmap(void* start,size_t length);
    1、pathname:

    在open函数中第一个参数pathname是指向想要打开的文件路径名,或者文件名。我们需要注意的是,这个路径名是绝对路径名。文件名则是在当前路径下的。

    2、flags:

    flags参数表示打开文件所采用的操作,我们需要注意的是:必须指定以下三个常量的一种,且只允许指定一个

    • O_RDONLY:只读模式

    • O_WRONLY:只写模式

    • O_RDWR:可读可写

    以下的常量是选用的,这些选项是用来和上面的必选项进行按位或起来作为flags参数。

    • O_APPEND 表示追加,如果原来文件里面有内容,则这次写入会写在文件的最末尾。

    • O_CREAT 表示如果指定文件不存在,则创建这个文件

    • O_EXCL 表示如果要创建的文件已存在,则出错,同时返回 -1,并且修改 errno 的值。

    • O_TRUNC 表示截断,如果文件存在,并且以只写、读写方式打开,则将其长度截断为0。

    • O_NOCTTY 如果路径名指向终端设备,不要把这个设备用作控制终端。

    • O_NONBLOCK 如果路径名指向 FIFO/块文件/字符文件,则把文件的打开和后继 I/O设置为非阻塞模式(nonblocking mode)

    以下三个常量同样是选用的,它们用于同步输入输出

    • O_DSYNC 等待物理 I/O 结束后再 write。在不影响读取新写入的数据的前提下,不等待文件属性更新。

    • O_RSYNC read 等待所有写入同一区域的写操作完成后再进行

    • O_SYNC 等待物理 I/O 结束后再 write,包括更新文件属性的 I/O

    3、mode:

    mode参数表示设置文件访问权限的初始值,和用户掩码umask有关,比如0644表示-rw-r–r–,也可以用S_IRUSR、S_IWUSR等宏定义按位或起来表示,详见open(2)的Man Page。要注意的是,有以下几点

    • 文件权限由open的mode参数和当前进程的umask掩码共同决定。

    • 第三个参数是在第二个参数中有O_CREAT时才作用,如果没有,则第三个参数可以忽略

    使用内存映射实现进程间通信
    #include 
    #include 
    #include 
    #include 
    #include
    #include 
    #include 
    #include 
    
    
    using namespace std;
    /*
    使用内存映射实现进程间通信
    
    */
    int main()
    {
    	int fd = open("test.txt", O_RDWR);
    	int size = lseek(fd, 0, SEEK_END);
    
    	// 创建内存映射区
    	void *ptr = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    
    	if (ptr == MAP_FAILED) {
    		perror("mmap");
    		exit(0);
    
    	}
    
    	pid_t pid = fork();
    	if (pid > 0) {
    		strcpy((char*)ptr, "hello world");
    	  // 父进程
    	}
    	else {
    		// 子进程
    		char buf[64]{ 0 };
    		strcpy(buf, (char*)ptr);
    		printf("read data = %s", buf);
    
    	}
    	// 关闭内存映射区
    	munmap(ptr, size);
    
    
    }

    输出

    read data = hello world

    内存映射的注意事项

    1.如果对mmap的返回值做++操作,munmap是否能成功?

     ```c++
     void * ptr = mmap(...)
         ptr++ // 可以++
         munmap(ptr,len)  // error 要保存首地址
     ```
    1. 如果open时O_RDONLY,mmap时prot参数指定PROT_READ|PROT_WRITE会怎么样?

      错误,要返回MAP_FAILED

    open函数中的权限建议和prot中的权限保持一致

    3.如果文件偏移量为1000会怎么样?

    偏移量必须是1024的整数倍,返回MAP_FAILED

    4.mmap什么情况下会调用失败?

    第二个参数 - lengh = 0
      第三个参数 port
        - 只指定了写权限
        - PROT_READ | PORT|WRITE
        第五个参数 fd 通过open打开文件时指定的参数使用 O_RDONLY / O_WRONLY
        

    5.可以open的时候O_CREAT一个新文件来创建映射区吗?

    可以,但是创建文件的大小不能为0

    可以对新文件进行扩展

    -lessk()

    - truncate()

    6.mmap后关闭文件描述符,对mmap映射有没有影响?

    映射区还存在,创建映射区的fd被关闭,没有任何影响

    7.对映射区越界操作 会怎样?

    越界操作访问的是非法的内存,会导致段错误 (Segment fault)


    使用内存映射实现文件拷贝
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    using namespace std;
    int main()
    {
      int fd = open("test.txt", O_RDWR);
      if (fd == -1)
      {
        perror("open");
        exit(0);
      }
     // 拓展文件大小
      int len = lseek(fd, 0, SEEK_END);
    
      int fd1 = open("cpy.txt", O_RDWR | O_CREAT, 0664);
      if (fd1 == -1)
      {
        perror("open");
        exit(0);
      }
     // 拓展新文件
      truncate("cpy.txt", len);
      write(fd1, " ", 1);
    
      // 分别做内存映射
      void *x = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
      void *x1 = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED, fd1, 0);
      if (x == MAP_FAILED)
      {
        perror("mmap");
        exit(0);
      }
     // 内存拷贝
      memcpy(x, x1, len);
      // 释放资源
      munmap(x, len);
      munmap(x1, len);
    
      close(fd1);
      close(fd);
    }

    匿名映射

    匿名映射:不需要文件实体进行内存映射

    代码实现

    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    using namespace std;
    int main()
    {
        int len = 4096;
        void *ptr = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);
        if (ptr == MAP_FAILED)
        {
            perror("mmap");
            exit(-1);
        }
    
        // 父子进程间通信
        pid_t pid = fork();
        if (pid > 0)
        {
            strcpy((char *)ptr, "hello world\n");
            wait(NULL);
        }
        else if (pid == 0)
        {
            sleep(1);
            printf("%s \n", (char *)ptr);
        }
    
        int ret = munmap(ptr, len);
        if (ret == -1)
        {
            perror("munmap");
            exit(-1);
        }
    }

    信号概述

    信号是Linux进程间通信最古老的方式之一,是事件发生时对进程的通知机制,有时也称为软件中断,它是在软件层次上对中断机制的一种模拟,是一种异步通信的方式。

    使用信号的两个目的:

    • 让进程知道已经发生了一个特定的事件

    • 强迫进程执行它自己代码中的信号处理程序

    信号的特点:

    • 简单

    • 不能携带大量信息

    • 满足某个特定条件才发送

    • 优先级比较高

    查看系统定义的信号列表

    kill -l

    前三十一个为常规信号,其余为实时信号。

    信号的5种默认处理动作

    信号处理.png

    kill raise abort 函数

    函数原型

    #include 
    #include 
    int kill(pid_t pid, int sig);
    

    函数参数: pid—进程号或者某个进程组的编号。

    • 如果该值是正数,代表信号发给某个进程;

    • 如果该值是0,代表信号发给调用该函数的进程所在组的其他所有进程。

    • 如果该值是-1,信号发送给该进程允许发送的所有进程,除了进程1(init)。

    • 如果该值小于-1,该信号发送给进程组ID为-pid内的所有进程。sig—信号的编号或者宏值。 如果信号的值为0,

    • 不会发送任何信号,可以用来检查该进程ID或进程组ID是否存在。

    返回值:

    如果至少一个信号被发送成功,返回0;如果错误返回-1,并且全局变量errno被设置成相应的值。

    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    using namespace std;
    int main()
    {
      pid_t pid = fork();
      if (pid == 0)
      {
           for (int i = 0; i < 5; i++)
                cout << "chird process\n", sleep(1);
      }
      else if (pid > 0)
      {
           cout << "parent process\n";
           sleep(2);
           cout << "kill child process now \n";
           kill(pid, SIGINT);
      }
    }


    int raise(int sig) // 参数  : 要发送的信号

    功能:向进程发送信号(给调用者发送一个信号,等价于kill(getpid(), sig)

    返回值:默认返回0,发送失败返回 -1


    void abort(void )// 

    函数说明:发送SIGABRT信号(值为6)给当前进程,杀死当前进程,等价于kill(getpid(), SIGABRT)

    alarm 函数

    1.引用头文件:

    #include ;

    2.函数标准式:

    unsigned int alarm(unsigned int seconds); // 参数 :  倒计时的时长(单位:秒) 如果倒计时为0,不进行倒计时

    作用 :

    设置定时器,函数调用,开始倒计时,当倒计时为0的时候会给进程发送一个SIGALARM信号

    SIGALARM:

    返回值:

    倒计时剩余的时间

    案例 1
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    using namespace std;
    int main()
    {
    
         int seconds = alarm(5);
         printf("seconds = %d \n",seconds); // 0
    
         sleep(2);
    
         seconds = alarm(2); // 不阻塞
         printf("seconds = %d \n",seconds);
    
         while(1){
    
         }
          
    }

    输出

    alarm.png

    案例 2 : 计算机一秒钟可以数多少个数
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    using namespace std;
    int main()
    {
         alarm(1);
         int i = 0;
         while(1){
               printf("%i\n",i++);
         }
    
       
    }

    ./alarm >> a.txt

    输出

    663565

    总结:

    实际的时间= 内核时间+ 用户时间+消耗的时间

    进行文件IO操作的时候比较消耗时间

    定时器与进程的状态无关 (自然定时器)


    setitimer 定时器函数

    头文件

    #include

    函数原型

    int setitimer(int which, const struct itimerval *new_value, struct itimerval *old_value)

    which有以下可选参数类型:

    ITIMER_REAL:以系统真实的时间来计算,它送出SIGALRM信号。

    ITIMER_VIRTUAL:以该进程在用户态下花费的时间来计算,它送出SIGVTALRM信号。

    ITIMER_PROF:以该进程在用户态下和内核态下所费的时间来计算。它送出SIGPROF信号。

    功能: 实现周期性的定时

    返回值:

    成功返回0,失败返回 -1

    过3秒以后,每隔2秒钟定时一次

    #include 
    #include 
    #include 
    
    // 过3秒以后,每隔2秒钟定时一次
    int main()
    {
    
        struct itimerval new_value;
    
        // 设置间隔的时间
        new_value.it_interval.tv_sec = 2;
        new_value.it_interval.tv_usec = 0;
    
        // 设置延迟的时间,3秒之后开始第一次定时
        new_value.it_value.tv_sec = 3;
        new_value.it_value.tv_usec = 0;
    
        int ret = setitimer(ITIMER_REAL, &new_value, NULL); // 非阻塞的
        printf("定时器开始了...\n");
    
        if (ret == -1)
        {
            perror("setitimer");
            exit(0);
        }
    
        getchar();
    
        return 0;
    }	

    signal 信号捕捉 函数

    函数原型

    void (*signal(int sig, void (*func)(int)))(int)

    功能

    设置某个信号的捕捉行为

    返回值

    该函数返回信号处理程序之前的值,当发生错误时返回 SIG_ERR

    实例

    #include 
    #include 
    #include 
    #include 
    #include 
    using namespace std;
    void sighandler(int);
    
    int main()
    {
        // 注册信号捕捉
        signal(SIGINT, sighandler);
    
        while (1)
        {
            printf("开始休眠一秒钟...\n");
            sleep(1);
        }
    
        return 0;
    }
    
    void sighandler(int signum)
    {
        printf("捕获信号 %d,跳出...\n", signum);
        exit(1);
    }

    信号集函数

    sigset_t类型对于每种信号用一个bit表示“有效”或“无效”状态,至于这个类型内部如何存储这些bit则依赖于系统 实现,从使用者的角度是不必关心的,使用者只能调用以下函数来操作sigset_ t变量,而不应该对它的内部数据做 任何解释,比如用printf直接打印sigset_t变量是没有意义的

    /*
    
        int sigemptyset(sigset_t *set);
            - 功能:清空信号集中的数据,将信号集中的所有的标志位置为0
            - 参数:set,传出参数,需要操作的信号集
            - 返回值:成功返回0, 失败返回-1
    
        int sigfillset(sigset_t *set);
            - 功能:将信号集中的所有的标志位置为1
            - 参数:set,传出参数,需要操作的信号集
            - 返回值:成功返回0, 失败返回-1
    
        int sigaddset(sigset_t *set, int signum);
            - 功能:设置信号集中的某一个信号对应的标志位为1,表示阻塞这个信号
            - 参数:
                - set:传出参数,需要操作的信号集
                - signum:需要设置阻塞的那个信号
            - 返回值:成功返回0, 失败返回-1
    
        int sigdelset(sigset_t *set, int signum);
            - 功能:设置信号集中的某一个信号对应的标志位为0,表示不阻塞这个信号
            - 参数:
                - set:传出参数,需要操作的信号集
                - signum:需要设置不阻塞的那个信号
            - 返回值:成功返回0, 失败返回-1
    
        int sigismember(const sigset_t *set, int signum);
            - 功能:判断某个信号是否阻塞
            - 参数:
                - set:需要操作的信号集
                - signum:需要判断的那个信号
            - 返回值:
                1 : signum被阻塞
                0 : signum不阻塞
                -1 : 失败
    
    */
    

    阻塞信号集和未决信号集

    1.用户通过键盘 Ctrl + C, 产生2号信号SIGINT (信号被创建)

    2.信号产生但是没有被处理 (未决) - 在内核中将所有的没有被处理的信号存储在一个集合中 (未决信号集) - SIGINT信号状态被存储在第二个标志位上 - 这个标志位的值为0, 说明信号不是未决状态 - 这个标志位的值为1, 说明信号处于未决状态

    3.这个未决状态的信号,需要被处理,处理之前需要和另一个信号集(阻塞信号集),进行比较 - 阻塞信号集默认不阻塞任何的信号 - 如果想要阻塞某些信号需要用户调用系统的API

    4.在处理的时候和阻塞信号集中的标志位进行查询,看是不是对该信号设置阻塞了 - 如果没有阻塞,这个信号就被处理 - 如果阻塞了,这个信号就继续处于未决状态,直到阻塞解除,这个信号就被处理

          #### 案例
    #include 
    #include 
    #include 
    #include 
    #include 
    using namespace std;
    
    int main()
    {
        // 创建信号集
        sigset_t set;
    
        // 清空信号集的内容
        sigemptyset(&set);
    
        // 判断信号 SIGINT 是否在信号集里面
        int ret = sigismember(&set, SIGINT);
        if (ret == 0)
        {
            printf("SIGINT 不阻塞\n");
        }
        else
        {
            printf("SIGINT 阻塞\n");
        }
    
        // 添加几个信号
        sigaddset(&set, SIGINT);
        sigaddset(&set, SIGQUIT);
    
        // 判断SIGINT 是否在信号集当中
        ret = sigismember(&set, SIGINT);
        if (ret == 0)
        {
            printf("SIGINT 不阻塞\n");
        }
        else
        {
            printf("SIGINT 阻塞\n");
        }
    
        ret = sigismember(&set, SIGQUIT);
        if (ret == 0)
        {
            printf("SIGQUIT 不阻塞\n");
        }
        else
        {
            printf("SIGQUIT 阻塞\n");
        }
        // 删除一个信号,判断SIGQUIT 是否在当前信号集当中
        sigdelset(&set, SIGQUIT);
        ret = sigismember(&set, SIGQUIT);
        if (ret == 0)
        {
            printf("SIGQUIT 不阻塞\n");
        }
        else
        {
            printf("SIGQUIT 阻塞\n");
        }
    }
    

    sigprocmask函数
    #include 
    int sigprocmask( int how, const sigset_t *restrict set, sigset_t *restrict oset );

    功能:将自定义信号集中的数据设置到内核中(阻塞,非阻塞)

    // 编写一个程序,把所有的常规信号(1-31)的未决状态打印到屏幕
    // 设置某些信号是阻塞的,通过键盘产生这些信号
    
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include
    using namespace std;
    int main() {
    
        // 设置2、3号信号阻塞
        sigset_t set;
        sigemptyset(&set);
        // 将2号和3号信号添加到信号集中
        sigaddset(&set, SIGINT);
        sigaddset(&set, SIGQUIT);
    
        // 修改内核中的阻塞信号集
        sigprocmask(SIG_BLOCK, &set, NULL);
    
        int num = 0;
    
        while(1) {
            num++;
            // 获取当前的未决信号集的数据
            sigset_t pendingset;
            sigemptyset(&pendingset);
            sigpending(&pendingset);
    
            // 遍历前32位
            for(int i = 1; i <= 31; i++) {
                if(sigismember(&pendingset, i) == 1) {
                    printf("1");
                }else if(sigismember(&pendingset, i) == 0) {
                    printf("0");
                }else {
                    perror("sigismember");
                    exit(0);
                }
            }
    
            printf("\n");
            sleep(1);
            if(num == 10) {
                // 解除阻塞
                sigprocmask(SIG_UNBLOCK, &set, NULL);
            }
    
        }
    
    
        return 0;
    }
    

    sigaction 信号捕捉函数

    何为信号:信号就是由用户、系统或进程发送给目标进程的信息,以通知目标进程中某个状态的改变或是异常。

    信号产生:总体来说,其产生的条件有两种,分别是:硬件和软件原因,又称为:硬中断和软中断。可细分为如下几种原因:

    ①系统终端Terminal中输入特殊的字符来产生一个信号,比如按下:ctrl+\会产生SIGQUIT信号。

    ②系统异常。比如访问非法内存和浮点数异常。

    ③系统状态变化。如设置了alarm定时器,当该定时器到期时候会引起SIGVTALRM信号。

    ④调用了kill命令或是kill函数。

    int sigaction(int signum, const struct sigaction *act,struct sigaction *oldact);

    参数:

    • 参数1:要捕获的信号

    • 参数2:接收到信号之后对信号进行处理的结构体

    • 参数3:接收到信号之后,保存原来对此信号处理的各种方式与信号(可用来做备份)。如果不需要备份,此处可以填NULL

    返回值:成功返回0,失败返回-1

    内核实现信号捕捉的流程

    信号捕捉的流程.png

    测试代码

    #include 
    #include 
    #include 
    #include 
     
    /*自定义的信号捕捉函数*/
    void sig_int(int signo)
    {
    	printf("catch signal SIGINT\n");//单次打印
        sleep(10);
        printf("----slept 10 s\n");
    }
     
    int main(void)
    {
    	struct sigaction act;		
     
    	act.sa_handler = sig_int;
    	act.sa_flags = 0;
    	sigemptyset(&act.sa_mask);		//不屏蔽任何信号
        sigaddset(&act.sa_mask, SIGQUIT);
     
    	sigaction(SIGINT, &act, NULL);
     
        printf("------------main slept 10\n");
        sleep(10);
     
    	while(1);//该循环只是为了保证有足够的时间来测试函数特性
     
    	return 0;
    }


    SIGCHLD 信号

    SIGCHLD 信号产生的条件

    • 子进程终止时

    • 子进程接收到SIGSTOP信号停止时

    • 子进程处在停止态,接受到SIGCONT后唤醒时

    以上三种条件都会给父进程发送SIGCHLD信号,父进程默认忽略

    SIGCHLD解决僵尸进程
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    using namespace std;
    void myFun(int num)
    {
    	printf("捕捉到的信号 :%d\n", num);
    	// 回收子进程PCB的资源
    	// while(1) {
    	//     wait(NULL);
    	// }
    	while (1)
    	{
    		int ret = waitpid(-1, NULL, WNOHANG);
    		if (ret > 0)
    		{
    			printf("child die , pid = %d\n", ret);
    		}
    		else if (ret == 0)
    		{
    			// 说明还有子进程或者
    			break;
    		}
    		else if (ret == -1)
    		{
    			// 没有子进程
    			break;
    		}
    	}
    }
    
    int main()
    {
    
    	// 提前设置好阻塞信号集,阻塞SIGCHLD,因为有可能子进程很快结束,父进程还没有注册完信号捕捉
    	sigset_t set;
    	sigemptyset(&set);
    	sigaddset(&set, SIGCHLD);
    	sigprocmask(SIG_BLOCK, &set, NULL);
    
    	// 创建一些子进程
    	pid_t pid;
    	for (int i = 0; i < 20; i++)
    	{
    		pid = fork();
    		if (pid == 0)
    		{
    			break;
    		}
    	}
    
    	if (pid > 0)
    	{
    		// 父进程
    
    		// 捕捉子进程死亡时发送的SIGCHLD信号
    		struct sigaction act;
    		act.sa_flags = 0;
    		act.sa_handler = myFun;
    		sigemptyset(&act.sa_mask);
    		sigaction(SIGCHLD, &act, NULL);
    
    		// 注册完信号捕捉以后,解除阻塞
    		sigprocmask(SIG_UNBLOCK, &set, NULL);
    
    		while (1)
    		{
    			printf("parent process pid : %d\n", getpid());
    			sleep(2);
    		}
    	}
    	else if (pid == 0)
    	{
    		// 子进程
    		printf("child process pid : %d\n", getpid());
    	}
    
    	return 0;
    }

    共享内存

    什么是共享内存?

    共享内存.png

    内存映射是共享内存的一种方式。共享内存是一种允许不同进程或线程访问相同物理内存区域的技术,而内存映射是实现这种共享的具体方法之一。

    内存映射通常是通过将一个文件或其他资源映射到一个进程的地址空间实现的。这样,当多个进程映射同一个文件或资源时,它们实际上在访问相同的内存区域,从而实现了内存共享。内存映射可以用于进程间通信,以及实现文件的高效访问。

    总的来说,共享内存是一种概念,而内存映射是实现共享内存的一种具体技术。

    共享内存相关操作

    (1)创建/打开共享内存:创建共享内存需要用到shmget()函数,原型如下

    #include 
    #include 
    #include 
    int shmget(key_t key, int size, int flag);

    创建成功返回共享内存的ID,出错返回-1。

    参数key为共享内存的键值,参数size为创建共享内存的大小,参数flag为调用函数的操作类型。参数key和参数flag共同决定的shmget()的作用:

    • key为IPC_PRIVATE时,创建一个新的共享内存,flag取值无效。

    • key不为IPC_PRIVATE,且flag设置了IPC_CREAT位,而没有设置IPC_EXCL位时,如果key为内核中的已存在的共享内存键值,则打开,否则创建一个新的共享内存。

    • key不为IPC_PRIVATE,且flag设置了IPC_CREAT和IPC_EXCL位时,则只执行创建共享内存操作。如果key为内核中的已存在的共享内存键值,返回EEXIST错误。

    (2)共享内存的附加(映射)

    创建一个共享内存后,某个进程若想使用,需要将此内存区域附加(attach)到自己的进程空间(或称地址映射),需要用到shmat()函数:

    #include 
    #include 
    #include 
    int *shmat(int shmid, const void *addr, int flag);

    运行成功返回指向共享内存段的地址指针,出错返回-1。

    参数shmid为共享内存的ID,参数addr和参数flag共同说明要引入的地址值,通常只有2种用法:

    • addr为0,表明让内核来决定第1个可引用的位置

    • addr非0,且flag中指定SHM_RND,则此段引入到addr所指向的位置。

    shmat()函数执行成功后,会将shmid的共享内存段的shmid_ds结构的shm_nattch计数器值加1

    (3)共享内存的分离 当进程使用完共享内存后,需要将共享内存从其进程空间中去除(detach),使用shmdt()函数:

    #include 
    #include 
    #include 
    int shmdt(void *addr);

    运行成功返回0,出错返回-1。

    参数addr是调用shmat()函数的返回值,即共享内存段的地址指针。shmdt()函数执行成功后,shm_nattch计数器值减1。

    (4)共享内存的控制 使用shmctl()可以对共享内存段进行多种控制操作,函数原型:

    #include 
    #include 
    #include 
    int shmctl(int shmid, int cmd, struct shmid_s *buf);

    运行成功返回0,出错返回-1。

    参数shmid为共享内存的ID,参数cmd指明了所要进行的操作,与参数*buf配合使用:

    取shmid指向的共享内存的shmid_ds结构,对参数buf指向的结构赋值

    案例

    • 生成key,ftok()

    • 使用key创建/获得一个共享内存,shmget()

    • 映射共享内存,得到虚拟地址,shmat()

    • 使用共享内存,通过地址指针

    • 移除映射,shmdt()

    • 销毁共享内存,shmctl()

    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    using namespace std;
    int main()
    {
    	// generate key
    	key_t key = ftok("./", 200);
    	printf("key=%#x\n", key);
    
    	// create a share memory
    	int shmid = shmget(key, 8, IPC_CREAT | 0666 | IPC_EXCL);
    	if (shmid == -1)
    	{
    		perror("shmget failed\n");
    		exit(1);
    	}
    	printf("shmid=%#x\n", shmid);
    
    	// map share memory to get the virtual address
    	void *p = shmat(shmid, 0, 0);
    	if ((void *)-1 == p)
    	{
    		perror("shmat failed");
    		exit(2);
    	}
    
    	// write data to share memory
    	int *pi = (int *)p;
    	*pi = 0xaaaaaaaa;
    	*(pi + 1) = 0x55555555;
    
    	// remove the map
    	if (shmdt(p) == -1)
    	{
    		perror("shmdt failed");
    		exit(3);
    	}
    
    	// delete the share memory
    	printf("use Enter to destroy the share memory\n");
    	getchar();
    	if (shmctl(shmid, IPC_RMID, NULL) == -1)
    	{
    		perror("shmctl");
    		exit(4);
    	}
    
    	return 0;
    }

    共享内存操作命令

    共享内存操作命令.png


    守护进程

    终端.png

    进程组

    进程组和会话之间形成了一种两级层次关系,进程组是一组进程相关的集合。

    进行组是由一个或多个共享同一进程组标识符(PGID)的进程组成

    进程组拥有一个生命周期,其开始时间为首进程创建组的时间,结束时间为最后一个进程退出的时间。一个进程可能会因为进程结束而退出进程组,也有可能会因为别的进程组加入而退出当前进程组。


    会话

    会话是一组进程组的集合。会话首进程是创建新会话的进程,其进程id会成为会话id。

    一个会话中的所有进程共享单个控制终端。

    在任意时刻,会话中的其中一个进程组会成为终端的前台进程组,其他进程组会成为后台进程组。

    当控制终端的连接建立起来之后,会话首进程会成为终端的控制进程。


    守护进程

    守护进程(Daemon Process),也就是通常说的 Daemon 进程(精灵进程),是 Linux 中的后台服务进程。它是一个生存期较长的进程,通常独立于控制终端并且周 期性地执行某种任务或等待处理某些发生的事件。一般采用以 d 结尾的名字。

    • 守护进程具备下列特征: 生命周期很长,守护进程会在系统启动的时候被创建并一直运行直至系统被关闭。

    • 它在后台运行并且不拥有控制终端。没有控制终端确保了内核永远不会为守护进 程自动生成任何控制信号以及终端相关的信号(如 SIGINT、SIGQUIT)。

    • Linux 的大多数服务器就是用守护进程实现的。比如,Internet 服务器 inetd, Web 服务器 httpd 等

    守护进程的创建步骤

    • 执行一个 fork(),之后父进程退出,子进程继续执行。

    • 子进程调用 setsid() 开启一个新会话。

    • 清除进程的 umask 以确保当守护进程创建文件和目录时拥有所需的权限。

    • 修改进程的当前工作目录,通常会改为根目录(/)。

    • 关闭守护进程从其父进程继承而来的所有打开着的文件描述符。 ◼

    • 在关闭了文件描述符0、1、2之后,守护进程通常会打开/dev/null 并使用dup2() 使所有这些描述符指向这个设备。

    • 核心业务逻辑

    案例

    /*
        写一个守护进程,每隔2s获取一下系统时间,将这个时间写入到磁盘文件中。
    */
    
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    
    void work(int num)
    {
        // 捕捉到信号之后,获取系统时间,写入磁盘文件
        time_t tm = time(NULL);
        struct tm *loc = localtime(&tm);
        // char buf[1024];
    
        // sprintf(buf, "%d-%d-%d %d:%d:%d\n",loc->tm_year,loc->tm_mon
        // ,loc->tm_mday, loc->tm_hour, loc->tm_min, loc->tm_sec);
    
        // printf("%s\n", buf);
    
        char *str = asctime(loc);
        int fd = open("time.txt", O_RDWR | O_CREAT | O_APPEND, 0664);
        write(fd, str, strlen(str));
        close(fd);
    }
    
    int main()
    {
    
        // 1.创建子进程,退出父进程
        pid_t pid = fork();
    
        if (pid > 0)
        {
            exit(0);
        }
    
        // 2.将子进程重新创建一个会话
        setsid();
    
        // 3.设置掩码
        umask(022);
    
        // 4.更改工作目录
        chdir("/home/nowcoder/");
    
        // 5. 关闭、重定向文件描述符
        int fd = open("/dev/null", O_RDWR);
        dup2(fd, STDIN_FILENO);
        dup2(fd, STDOUT_FILENO);
        dup2(fd, STDERR_FILENO);
    
        // 6.业务逻辑
    
        // 捕捉定时信号
        struct sigaction act;
        act.sa_flags = 0;
        act.sa_handler = work;
        sigemptyset(&act.sa_mask);
        sigaction(SIGALRM, &act, NULL);
    
        struct itimerval val;
        val.it_value.tv_sec = 2;
        val.it_value.tv_usec = 0;
        val.it_interval.tv_sec = 2;
        val.it_interval.tv_usec = 0;
    
        // 创建定时器
        setitimer(ITIMER_REAL, &val, NULL);
    
        // 不让进程结束
        while (1)
        {
            sleep(10);
        }
    
        return 0;
    }
  • 相关阅读:
    《统计学习方法》第三章习题
    减压二十六式
    SpringBoot实现注解方式日志log记录
    vue中使用vue-property-decorator
    adb server version (19045) doesn‘t match this client (41); killing.的解决办法
    LeetCode之二:字母异位词分组
    面试总结个人版
    进程程序替换与exec系统调用
    Stateful DHCPv6
    【C++】Linux下如何查看opencv的版本
  • 原文地址:https://blog.csdn.net/weixin_72686492/article/details/133826730