第二章 Linux多进程开发:进程控制,进程通信和守护进程

1.进程控制

进程退出

exit()是标准C库的函数,在linux系统下底层会调用linux系统的函数_exit()

第二章 Linux多进程开发:进程控制,进程通信和守护进程_第1张图片

两个的区别:exit()在调用之后需要比_exit()做更多的事情,其中有一个就是刷新I/O缓冲关闭文件描述符

对于下面的程序

#include 
using namespace std;
#include 

int main() {
    cout << "hello" << endl;
    cout << "world";

    // exit(0);
    _exit(0);

    return 0;
}

使用exit(0)和_exit(0)会有如下两个不同结果

exit:

image-20230718195742100

_exit:

image-20230718195756973

那么为什么会这样呢?

刷新缓冲区问题(重要)

c++和c语言中的cout或者printf()也好,本质都是printf(),printf()作为C语言标准库的函数,并不是直接输出到终端屏幕上,而是先写在缓冲区当中,然后当缓冲区刷新的时候在清空缓冲区输出到屏幕当中!!!

第二章 Linux多进程开发:进程控制,进程通信和守护进程_第2张图片

于是乎刚才的代码:

cout << "hello" << endl;
cout << "world";

// exit(0);
_exit(0);

hello遇到endl,相当于 ‘\n’,刷新缓冲区,显示;第二个world在缓冲区中,本来正常程序结束就显示在屏幕上了,现在遇到了_exit()不刷新缓冲区,那么就丢掉了,不显示

变式
#include 
using namespace std;
#include 

int main() {
    cout << "hello";

    sleep(2);

    return 0;
}

这个程序在输出的时候不会立刻输出hello,因为这个时候缓冲区没有刷新,会谁2秒,然后return 0主程序结束的时候再刷新缓冲区进行显示!!!

孤儿进程

父进程有义务回收子进程的资源,但是当父进程结束而子进程未结束的时候这一点无法做到,所以这个时候内核会把这个子进程的父进程设置为init进程(linux上的第一层进程,linux下的进程都是不断的创建子进程而创建出来的),init进程会循环的wait()等待这个子进程,当这个子进程结束了之后,init进程会将其回收,处理善后工作!!!

第二章 Linux多进程开发:进程控制,进程通信和守护进程_第3张图片

演示代码:

#include 
using namespace std;
#include 

int main() {
    int pid = fork();
    if (pid == -1) {
        perror("fork");
        return -1;
    }

    // 判断是父进程还是子进程
    if (pid > 0) {  // 父进程走,返回的是创建的子进程编号
        printf("I am parent process, pid : %d , ppid : %d\n", getpid(), getppid());
    } else if (pid == 0) {  // 子进程走
        sleep(1);           // 强制让子进程睡1秒,让父进程跑完,子进程称为孤儿进程
        printf("I am child process, pid : %d , ppid : %d\n", getpid(), getppid());
    }

    // for
    for (int i = 0; i < 3; ++i) {
        printf("i : %d , pid : %d\n", i, getpid());
    }

    return 0;
}

输出效果:

第二章 Linux多进程开发:进程控制,进程通信和守护进程_第4张图片

父进程创建出来子进程之后,子进程休眠1秒,父进程早已跑完,结束,子进程交给_init进程作为他的父进程进行管理,所以会显示终端是因为父进程结束了,但是这个时候子进程尚未结束,父进程和子进程在内核区域有一些数据是一样的,比如文件描述符012的标准输入,输出和错误,所以仍然可以在当前终端输出信息。并且这里 _init还让他结束之后阻塞了

僵尸进程

每个进程结束之后,内核区的PCB没有办法自己释放,需要父进程释放;用户区的数据可以自己释放

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

僵尸进程不能被 kill -9 杀死

僵尸进程多了会占据进程号,进程号范围 0 ~ 32767,占据完了就会有危险

第二章 Linux多进程开发:进程控制,进程通信和守护进程_第5张图片

演示代码:

#include 
using namespace std;
#include 

int main() {
    int pid = fork();
    if (pid == -1) {
        perror("fork");
        return -1;
    }

    // 判断是父进程还是子进程
    if (pid > 0) {   // 父进程走,返回的是创建的子进程编号
        while (1) {  // 强制让父进程一直循环,不退出,让子进程结束,父进程没办法回收他的资源
            printf("I am parent process, pid : %d , ppid : %d\n", getpid(), getppid());
            sleep(1);
        }
    } else if (pid == 0) {  // 子进程走
        printf("I am child process, pid : %d , ppid : %d\n", getpid(), getppid());
    }

    return 0;
}

输出结果:

第二章 Linux多进程开发:进程控制,进程通信和守护进程_第6张图片

可以看出,子进程结束了但是父进程没结束,没有办法释放子进程内核区域的数据,导致了僵尸进程

image-20230718205324350

现在的状态,子进程Z+代表是僵尸进程,< defunct >代表不存在的;父进程S+代表睡眠

kill -9 杀不掉僵尸进程

第二章 Linux多进程开发:进程控制,进程通信和守护进程_第7张图片

image-20230718205647171

这个时候想要解决这个问题只能杀掉父进程,把子进程托管给 /init ,这样才能将其释放,但是实际开发当中杀掉父进程往往不现实,所以需要父进程调用wait()和waitpid()来保证父进程会把子进程的内核PCB的数据给释放掉,这样才能避免僵尸进程

第二章 Linux多进程开发:进程控制,进程通信和守护进程_第8张图片

进程回收

第二章 Linux多进程开发:进程控制,进程通信和守护进程_第9张图片

wait()

为了避免僵尸进程,父进程需要回收子进程的资源

wait()函数会阻塞,父进程调用之后会阻塞在那里等待子进程结束然后释放子进程的资源

#include 

pid_t wait(int *wstatus);
//功能:等待任意一个子进程结束,如果任意一个子进程结束了,此函数会回收这个子进程的资源
//参数:int *wstatus
    //进程退出时候的状态信息,传入的是一个int类型的地址,传出参数
//返回值:
    //成功 返回被回收的子进程的id
    //失败 -1(所有的子进程都结束,调用函数失败) 并且修改errno

//调用wait()函数,进程会阻塞,知道他的一个子进程退出或者收到一个不能被忽略的信号,这个时候才被唤醒
//如果没有子进程,这个函数立刻返回-1;如果子进程都已经结束了,也会返回-1

示例代码:

#include 
using namespace std;
#include 
#include 

int main() {
    // 有一个父进程,创建5个子进程
    pid_t pid;

    for (int i = 0; i < 5; ++i) {
        pid = fork();

        if (pid == -1) {
            perror("fork");
            return -1;
        }

        if (pid == 0)  // 说明是子进程,如果不加这行代码,子进程也会走for循环,他也会fork()产生更多的孙子进程,重孙进程等等
            break;
    }

    if (pid > 0) {
        // 父进程
        while (1) {
            printf("parent , pid = %d\n", getpid());

            // int ret = wait(NULL);

            int status;
            int ret = wait(&status);

            printf("child die,pid = %d\n", ret);

            sleep(1);
        }
    } else if (pid == 0) {
        // 子进程
        printf("child , pid = %d\n", getpid());
        sleep(1);

        exit(0);
    }

    return 0;
}

这个代码用父进程创建了5个子进程(注意看怎么创建的),父进程中用wait()函数阻塞等待子进程结束,子进程结束一个,父进程输出关于ret的一段信息表示子进程已经被回收,然后重复,直到所有子进程全部结束被回收完毕;这个时候父进程依然在循环,只不过wait()返回-1

执行结果:

第二章 Linux多进程开发:进程控制,进程通信和守护进程_第10张图片

稍加修改,现在我需要查看进程退出的状态:

我让进程正常退出

#include 
using namespace std;
#include 
#include 

int main() {
    // 有一个父进程,创建5个子进程
    pid_t pid;

    for (int i = 0; i < 5; ++i) {
        pid = fork();

        if (pid == -1) {
            perror("fork");
            return -1;
        }

        if (pid == 0)  // 说明是子进程,如果不加这行代码,子进程也会走for循环,他也会fork()产生更多的孙子进程,重孙进程等等
            break;
    }

    if (pid > 0) {
        // 父进程
        while (1) {
            printf("parent , pid = %d\n", getpid());

            // int ret = wait(NULL);

            int status;
            int ret = wait(&status);

            if (ret == -1)  // 没有子进程
                break;

            // 有子进程,回收了子进程的资源
            if (WIFEXITED(status)) {  // 是不是正常退出
                printf("退出的状态码: %d\n", WEXITSTATUS(status));
            } else if (WIFSIGNALED(status)) {  // 是不是异常退出
                printf("被哪个信号干掉了: %d\n", WTERMSIG(status));
            }

            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;
}

这个时候子进程退出时候调用C标准库函数exit(0),状态码就是0,因此正常退出的状态码用图中形式接受并打印

执行结果:

第二章 Linux多进程开发:进程控制,进程通信和守护进程_第11张图片

现在我把子进程改成while(1)循环,然后用 kill -9 杀掉他,看看他是被哪个信号干掉的

退出信息宏函数(status相关)

第二章 Linux多进程开发:进程控制,进程通信和守护进程_第12张图片

执行结果:

第二章 Linux多进程开发:进程控制,进程通信和守护进程_第13张图片

第二章 Linux多进程开发:进程控制,进程通信和守护进程_第14张图片

waitpid()
#include 

pid_t waitpid(pid_t pid, int *_Nullable wstatus, int options);
// 作用:回收指定进程号的子进程,可以设置是否阻塞
// 参数:
//     pid:> 0 表示某个子进程的id
//         == 0 回收当前进程组的所有子进程(我自己的子进程不一定和我属于一个组,有可能被我给出去了)
//         == -1 回收所有的子进程,相当于wait(),最常用
//         < -1 回收某个进程组当中的所有子进程,组号是这个参数的绝对值
//     options:设置阻塞或者非阻塞
//         0 阻塞
//         WNOHANG:非阻塞
// 返回值:
//     > 0 返回子进程的id
//     == 0 options = WNOHANG,表示还有子进程活着
//     ==-1 错误,并且设置错误号,在非阻塞的情况下返回-1可以代表没有子进程了

// 这么来看,waitpid(-1,&status,0) 相当于是 wait(&status)

代码:

#include 
using namespace std;
#include 
#include 

int main() {
    // 有一个父进程,创建5个子进程
    pid_t pid;

    for (int i = 0; i < 5; ++i) {
        pid = fork();

        if (pid == -1) {
            perror("fork");
            return -1;
        }

        if (pid == 0)  // 说明是子进程,如果不加这行代码,子进程也会走for循环,他也会fork()产生更多的孙子进程,重孙进程等等
            break;
    }

    if (pid > 0) {
        // 父进程
        while (1) {
            printf("parent , pid = %d\n", getpid());
            sleep(1);

            // int ret = wait(NULL);

            int status;
            // int ret = waitpid(-1, &status, 0);        // 阻塞
            int ret = waitpid(-1, &status, WNOHANG);  // 非阻塞

            if (ret == -1)  // 没有子进程
                break;
            else if (ret == 0)
                // 非阻塞就是执行到这个位置判断一下,然后遇到了就回收了,没有就走了
                // ret==0表明还有子进程活着,重开循环判断
                // 非阻塞的好处:父进程不用一直阻塞这等待子进程结束,可以做自己的逻辑,然后每隔一段时间就回来看子进程是否运行完毕然后回收。提高效率
                continue;
            else if (ret > 0) {
                // 有子进程,回收了子进程的资源
                if (WIFEXITED(status)) {  // 是不是正常退出
                    printf("退出的状态码: %d\n", WEXITSTATUS(status));
                } else if (WIFSIGNALED(status)) {  // 是不是异常退出
                    printf("被哪个信号干掉了: %d\n", WTERMSIG(status));
                }
                printf("child die,pid = %d\n", ret);
            }
        }
    } else if (pid == 0) {
        // 子进程
        while (1) {
            printf("child , pid = %d\n", getpid());
            sleep(1);
        }

        exit(0);
    }

    return 0;
}

非阻塞的含义:非阻塞就是执行到这个位置判断一下,然后遇到了就回收了,没有就走了

非阻塞的好处:父进程不用一直阻塞这等待子进程结束,可以做自己的逻辑,然后每隔一段时间就回来看子进程是否运行完毕然后回收。提高效率

图中的代码含义就是:子进程一直循环输出,父进程每次非阻塞的waitpid(),有子进程则重新循环再次判断,如果子进程结束(正常或异常),则进入下方循环输出相关信息然后再回去循环

执行结果:

我不管他是这样,可以父进程是非阻塞的一直在工作的

第二章 Linux多进程开发:进程控制,进程通信和守护进程_第15张图片

现在我把子进程kill掉

第二章 Linux多进程开发:进程控制,进程通信和守护进程_第16张图片

kill一次输出一个信息,然后kill完毕之后结束进程

第二章 Linux多进程开发:进程控制,进程通信和守护进程_第17张图片

2.进程间通信 IPC

概念

第二章 Linux多进程开发:进程控制,进程通信和守护进程_第18张图片

进程间通信方式(记忆!!!)

第二章 Linux多进程开发:进程控制,进程通信和守护进程_第19张图片

(同一主机之间----------)
2.1.管道(数据结构是环形队列)

为什么是环形?大概率是为了处理边界问题吧

管道拥有文件的特质,读操作和写操作;

匿名管道没有文件实体;有名管道有文件实体,但是不存储数据

可以按照操作文件的方式对管道进行操作,也具有文件描述符,有两个,分别指向读端和写端

一个管道就是一个字节流,管道没有消息或者消息边界的概念,从管道读数据可以任意读,不用考虑写入的数据是多少;并且管道传递的数据是有顺序的

第二章 Linux多进程开发:进程控制,进程通信和守护进程_第20张图片

管道单向,一边写,一边读;半双工

匿名管道只能在拥有共同祖先的进程当中使用,例如父进程和子进程;两个兄弟进程

第二章 Linux多进程开发:进程控制,进程通信和守护进程_第21张图片

为什么可以用管道通信

父进程 fork() 之后,子进程和父进程共享了一份这个文件描述符表。管道也具有文件的性质,他的两端分别对应读和写的文件描述符,因此需要两个进程指向这同一个位置,所以必须是具有一定关系的进程才能使用,就像如图所示,父子进程的5号都对应写数据,6号都对应读数据,然后这样就可以联系起来了

那么在fork之前还是fork之后建立管道呢?

答案是fork()之前。

因为fork()之前建立管道,比如如图,管道占据两个文件描述符5和6,现在fork()之后,由于管道具有文件描述符,子进程内核区域文件描述符指向的东西是和父进程一块东西,所以他们两个指向的是同一块管道,这样才能进行通信!!!

第二章 Linux多进程开发:进程控制,进程通信和守护进程_第22张图片

管道的数据结构:环形队列

第二章 Linux多进程开发:进程控制,进程通信和守护进程_第23张图片

匿名管道(pipe)

第二章 Linux多进程开发:进程控制,进程通信和守护进程_第24张图片

看如图的命令:

ls | wc -l

这是两个命令 ls 和 wc -l,整个的作用就是用ls查看目录下的文件然后传递给 wc -l 命令实现统计并且在屏幕上进行输出

那么怎么进行传递呢?或者说怎么把ls获取到的数据交给wc进程呢?

这就需要管道了,命令当中的 | 就是指在两个进程之间建立一个匿名管道,然后前面的进程向后面的进程进行通信,提供数据信息

匿名管道的使用

第二章 Linux多进程开发:进程控制,进程通信和守护进程_第25张图片

pipe()
#include 

int pipe(int pipefd[2]);
// 功能:创建一个匿名管道,用于进程间通信
// 参数:int pipefd[2] 这个数组是一个传出参数
//     pipefd[0] 对应的是管道的读端
//     pipefd[1] 对应的是管道的写端
// 返回值:
//     成功 0
//     失败 -1,并且设置errno

// 注意:匿名管道只能用于具有关系的进程通信之间,比如:父子进程,兄弟进程等等

//     管道默认是阻塞的,如果管道中没有数据,read阻塞;管道满了,write阻塞
#include 
#include 
#include 
using namespace std;
#include 
#define _size 1024

int main() {
    // 子进程发送数据给父进程,父进程读取到数据输出

    // 在fork之前创建管道,因为要指向一个管道
    int pipefd[2];

    int ret = pipe(pipefd);
    if (ret == -1) {
        perror("pipe");
        return -1;
    }

    // 管道创建成功了,现在创建子进程
    pid_t pid = fork();
    if (pid == -1) {
        perror("fork");
        return -1;
    }

    if (pid > 0) {  // 父进程
        printf("i am parent process , pid : %d\n", getpid());

        char buf[_size] = {0};
        const char *str = "hello,i am parent";
        while (1) {
            // 读数据
            // read默认是阻塞的
            read(pipefd[0], buf, sizeof(buf));
            printf("parent recv : \"%s\" , pid : %d\n", buf, getpid());
            // 读完清空buf
            bzero(buf, _size);

            // 写数据
            write(pipefd[1], str, strlen(str));
            // read会在缓冲区空的时候阻塞,父进程写一次子进程读一次,然后再次阻塞,所以sleep是在父进程这边
            // 如果在子进程这边sleep,父进程在这段时间疯狂写
            sleep(1);
        }

    } else if (pid == 0) {  // 子进程
        printf("i am child process , pid : %d\n", getpid());

        char buf[_size] = {0};
        const char *str = "hello,i am child";
        while (1) {
            // 写数据
            write(pipefd[1], str, strlen(str));
            sleep(1);

            // 读数据
            // read默认是阻塞的
            read(pipefd[0], buf, sizeof(buf));
            printf("child recv : \"%s\" , pid : %d\n", buf, getpid());
            // 读完清空buf
            bzero(buf, _size);
        }
    }

    return 0;
}

这个程序的本意是让父子进程之间建立管道,然后子进程向管道写数据,父进程接受数据并输出

代码当中用了两个循环,子进程循环写数据给管道,父进程调用read()函数接受,read()函数默认是阻塞的,调用while(1)一直读,因此最后的结果是这样

第二章 Linux多进程开发:进程控制,进程通信和守护进程_第26张图片

变式:

父进程和子进程相互通信,一个道理

#include 
#include 
#include 
using namespace std;
#include 

int main() {
    // 子进程发送数据给父进程,父进程读取到数据输出

    // 在fork之前创建管道,因为要指向一个管道
    int pipefd[2];

    int ret = pipe(pipefd);
    if (ret == -1) {
        perror("pipe");
        return -1;
    }

    // 管道创建成功了,现在创建子进程
    pid_t pid = fork();
    if (pid == -1) {
        perror("fork");
        return -1;
    }

    if (pid > 0) {  // 父进程
        printf("i am parent process , pid : %d\n", getpid());
        while (1) {
            // 读数据
            char buf[1024] = {0};
            // read默认是阻塞的
            read(pipefd[0], buf, sizeof(buf));
            printf("parent recv : \"%s\" , pid : %d\n", buf, getpid());

            // 写数据
            const char *str = "hello,i am parent";
            write(pipefd[1], str, strlen(str));
            sleep(1);
        }

    } else if (pid == 0) {  // 子进程
        printf("i am child process , pid : %d\n", getpid());

        while (1) {
            // 写数据
            const char *str = "hello,i am child";
            write(pipefd[1], str, strlen(str));
            sleep(1);

            // 读数据
            char buf[1024] = {0};
            // read默认是阻塞的
            read(pipefd[0], buf, sizeof(buf));
            printf("child recv : \"%s\" , pid : %d\n", buf, getpid());
        }
    }

    return 0;
}

父子进程都设置读操作和写操作,只是要注意一点,这里顺序要相反,因为父进程和子进程如果都先读都阻塞程序没办法推进了

执行结果:

第二章 Linux多进程开发:进程控制,进程通信和守护进程_第27张图片

fpathconf()

用来获取管道缓冲区的大小(4096 bytes)

#include 

long fpathconf(int fd, int name);
// 作用:获取管道的大小
// 参数:fd 管道的文件描述符(两个填一个即可)
//     name:宏值
//         获取大小使用 _PC_PIPE_BUF
#include 
using namespace std;
#include 

int main() {
    int pipefd[2];

    int ret = pipe(pipefd);
    if (ret == -1) {
        perror("pipe");
        return -1;
    }

    // 获取管道大小
    long size = fpathconf(pipefd[0], _PC_PIPE_BUF);
    printf("pipe size : %ld\n", size);  // 4096

    return 0;
}
匿名管道通信案例
注意

刚才的案例有一个问题就是说匿名管道在实现相互通信的时候,可能会出现进程自己写的数据被自己读取

一般的交互情况如下所示:

第二章 Linux多进程开发:进程控制,进程通信和守护进程_第28张图片

第二章 Linux多进程开发:进程控制,进程通信和守护进程_第29张图片

但是有可能在cpu分配时间片的时候处理不得当,或者说我写了数据之后忘了加sleep让自己的进程和对面抢夺read,就可能导致自己写的数据被自己读取,并且在实际开发的过程中我们肯定不可能让写的一方sleep来让出cpu让对方来读取,所以就很可能出现这种情况,这个是没有办法避免的

所以我们匿名管道在实际运用的时候一般规定数据流只从一端流向另一端,不会去实现双向的数据流动,因为这样就可能发生自己数据被自己接受的情况

第二章 Linux多进程开发:进程控制,进程通信和守护进程_第30张图片

所以在实际的开发过程中我们往往直接关闭一方的写端和另一方的读端,如下所示:

我们想要的就是第三种情况

第二章 Linux多进程开发:进程控制,进程通信和守护进程_第31张图片
#include 
#include 
#include 
using namespace std;
#include 
#define _size 1024

int main() {
    // 子进程发送数据给父进程,父进程读取到数据输出

    // 在fork之前创建管道,因为要指向一个管道
    int pipefd[2];

    int ret = pipe(pipefd);
    if (ret == -1) {
        perror("pipe");
        return -1;
    }

    // 管道创建成功了,现在创建子进程
    pid_t pid = fork();
    if (pid == -1) {
        perror("fork");
        return -1;
    }

    if (pid > 0) {  // 父进程
        printf("i am parent process , pid : %d\n", getpid());

        char buf[_size] = {0};
        // const char *str = "hello,i am parent";
        while (1) {
            // 读数据
            // read默认是阻塞的
            read(pipefd[0], buf, sizeof(buf));
            printf("parent recv : \"%s\" , pid : %d\n", buf, getpid());
            // 读完清空buf
            bzero(buf, _size);

            // 关闭写端
            close(pipefd[1]);
        }

    } else if (pid == 0) {  // 子进程
        printf("i am child process , pid : %d\n", getpid());

        char buf[_size] = {0};
        const char *str = "hello,i am child";
        while (1) {
            // 写数据
            write(pipefd[1], str, strlen(str));
            sleep(1);

            // 关闭读端
            close(pipefd[0]);
        }
    }

    return 0;
}
案例(!!!)

实现 ps aux | grep root,父子进程之间通信

第二章 Linux多进程开发:进程控制,进程通信和守护进程_第32张图片

代码:(看代码理解!!!)

注意:

父进程需要调用wait()函数来释放子进程,防止出现僵尸进程;

dup2()函数的作用:

#include 
int dup2(int oldfd , int newfd);

//作用:重定向文件描述符
    //oldfd指向a.txt,newfd指向b.txt
    //调用函数成功后,newfd和b.txt的连接做close(oldfd仍指向a.txt),newfd指向a.txt
    //oldfd必须是一个有效的文件描述符
    //如果相同则相当于什么都没做
//返回值:
    //newfd,他们都指向的是oldfd之前指向的文件
#include 
using namespace std;
#include 
#include 
#include 
#include 
#define _size 1024

/*
    实现 ps aux 父子进程之间通信

    子进程:ps aux,子进程结束之后将数据发送给父进程
    父进程:获取到数据,打印

    思路:
    子进程需要执行 ps aux 命令,调用exec族函数,但是这些函数的默认输出端是在stdout_fileno
    所以需要使用dup2()函数将其重定向到管道的写端
    将读取的内容存到文本中,然后去执行grep命令即可
*/

int main() {
    // 创建管道
    int pipefd[2];

    int ret = pipe(pipefd);
    if (ret == -1) {
        perror("pipe");
        return -1;
    }

    // 创建子进程
    pid_t pid = fork();
    if (pid == -1) {
        perror("fork");
        return -1;
    }

    if (pid > 0) {
        // 父进程

        // 关闭写端
        close(pipefd[1]);

        // 读
        char buf[_size] = {0};
        int len = -1;

        // 先打开,如果不存在则创建,存在则删除再创建
        // 建议不要重复打开,这里就打开一次然后写,最后关闭即可
        int fd = open("file.txt", O_RDONLY);
        if (fd != -1)            // 存在,将其删除
            remove("file.txt");  // 不能用exec()函数族,因为这就把这个主进程替换了,不会回来
        close(fd);

        // 然后创建一个
        fd = open("file.txt", O_RDWR | O_CREAT, 0664);
        if (fd == -1) {
            perror("open");
            return -1;
        }

        // -1 留一个结束符
        // 循环读
        while ((len = read(pipefd[0], buf, sizeof(buf) - 1)) != 0) {
            if (len == -1) {
                perror("read");
                return -1;
            }

            // printf("%s", buf);
            write(fd, buf, strlen(buf));

            bzero(buf, _size);
        }

        close(fd);

        // grep筛选root
        execlp("grep", "grep", "root", "file.txt", nullptr);

        // 父进程回收子进程资源防止出现僵尸进程
        wait(nullptr);
    } else if (pid == 0) {
        // 子进程

        // 关闭读端
        close(pipefd[0]);

        // 将标准输入stdout_fileno重定向到管道的写端
        // dup2() newfd指向oldfd指向的位置,oldfd被释放
        int ret = dup2(pipefd[1], STDOUT_FILENO);
        if (ret == -1) {
            perror("dup2");
            return -1;
        }

        // 使用exec函数族执行shell命令,他输出靠的是的是stdout_fileno
        // 为防止管道大小不够,循环的去执行保证指令被写完
        while (ret = execlp("ps", "ps", "aux", nullptr))
            if (ret == -1) {
                perror("execlp");
                return -1;
            }
    }

    return 0;
}

执行结果:

第二章 Linux多进程开发:进程控制,进程通信和守护进程_第33张图片

管道的读写特点

使用管道的时候,需要注意一下几种特殊的情况(假设都是阻塞I/O操作):

  • 所有指向管道写端的文件描述符都关闭了(管道写端引用计数为0),有进程从管道的读端读数据,那么管道中剩余的数据被读取以后,再次去read()会返回0,就像读到文件末尾一样

    如图所示,这里的读端计数为2,写端计数为0

    第二章 Linux多进程开发:进程控制,进程通信和守护进程_第34张图片

  • 如果有指向管道写端的文件描述符没有关闭(管道写端的引用计数大于0),而持有管道写端的进程也没有往管道中写数据,这个时候有进程往管道中读取数据,那么管道中剩余的数据被读取完毕之后,再次read阻塞,直到管道中有数据可以读取了才会读取数据并且返回
    image-20230721135603117

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

    第二章 Linux多进程开发:进程控制,进程通信和守护进程_第35张图片

  • 如果有指向管道读端的文件描述符没有关闭(管道读端的引用计数大于0),而持有管道读端的进程也没有从管道中读取数据,这时候有进程向管道中写数据,那么在管道被写满的时候再次调用write()会阻塞,直到管道中有空位置才能再次写入数据并返回
    第二章 Linux多进程开发:进程控制,进程通信和守护进程_第36张图片

总结:

  • 读管道:
    • 管道中有数据,读取会返回实际读到的字节数
    • 管道中无数据:
      • 写端全部关闭,read返回0(相当于读到文件的末尾)
      • 写端没有完全关闭,read阻塞等待
  • 写管道:
    • 管道读端全部关闭,产生信号SIGPIPE,进程异常终止
    • 管道读端没有全部关闭:
      • 管道已满,write阻塞
      • 管道没有满,write将数据写入,并返回实际写入的字节数
设置管道非阻塞(fcntl)
#include 
#include 

int fcntl(int fd, int cmd, ...); ...当中是可变参数
// 参数:
//     fd:需要操作的文件描述符
//     cmd:表示对文件描述符进行如何操作
//         F_DUPFD 复制文件描述符,复制的是第一个参数,得到一个新的文件描述符(返回值)
//             int ret = fcntl(fd,F_DUPFD);
//         F_GETFL 获取指定文件描述符的文件状态flag
//             获取的flag和我们通过open函数传递的flag是一个东西
//         F_SETFL 设置文件描述符的文件状态flag
//             必选项:O_RDONLY O_WRONLY O_RDWR 不可以被修改
//             可选项:O_APPEND O_NONBLOCK
//                 O_APPEND 表示追加数据
//                 O_NONBLOCK 设置成非阻塞
//                     阻塞和非阻塞:描述的是函数调用的行为

如何设置?

int flags = fcntl(pipefd[0], F_GETFL);
flags |= O_NONBLOCK;
fcntl(pipefd[0], F_SETFL, flags);

代码:

#include 
#include 
#include 
using namespace std;
#include 
#include 
#define _size 1024

/*
    设置管道非阻塞
    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");
        return -1;
    }

    // 管道创建成功了,现在创建子进程
    pid_t pid = fork();
    if (pid == -1) {
        perror("fork");
        return -1;
    }

    if (pid > 0) {  // 父进程
        printf("i am parent process , pid : %d\n", getpid());

        char buf[_size] = {0};

        int flags = fcntl(pipefd[0], F_GETFL);
        flags |= O_NONBLOCK;
        fcntl(pipefd[0], F_SETFL, flags);

        // 关闭写端
        close(pipefd[1]);

        while (1) {
            // 读数据
            // read默认是阻塞的
            // 设置成为非阻塞
            int len = read(pipefd[0], buf, sizeof(buf));
            printf("len = %d\n", len);
            printf("parent recv : \"%s\" , pid : %d\n", buf, getpid());
            // 读完清空buf
            bzero(buf, _size);

            sleep(1);  // 子进程和父进程睡眠的时间不同,这样可以方便的观察是否阻塞
        }

    } else if (pid == 0) {  // 子进程
        printf("i am child process , pid : %d\n", getpid());
        const char *str = "hello,i am child";

        // 关闭读端
        close(pipefd[0]);

        while (1) {
            // 写数据
            write(pipefd[1], str, strlen(str));
            sleep(5);
        }
    }

    return 0;
}

执行结果:

可见子进程在睡眠的时候父进程执行到read()并没有阻塞,而是执行走了!!!

第二章 Linux多进程开发:进程控制,进程通信和守护进程_第37张图片

有名管道(FIFO)

有名管道和匿名管道的区别在于:匿名管道本身没有一个文件描述符或者说路径可以让两个进程找到他,这就导致我们只能通过某种方式让两个进程指向同一块管道,比如主进程先建立管道,然后创建子进程,这样保证了两个进程的读端和写端的文件描述符指向的是同样的匿名管道的两端,这样就只能用于亲缘关系的进程之间通信,而有名管道则恰好克服了这个问题;设置了一个路径名方便两个进程关联,并且这个路径名可以像文件一样被访问(FIFO),这样就可以被任意关系的两个进程找到并且建立通信

第二章 Linux多进程开发:进程控制,进程通信和守护进程_第38张图片

区别:

第二章 Linux多进程开发:进程控制,进程通信和守护进程_第39张图片

mkinfo()

第二章 Linux多进程开发:进程控制,进程通信和守护进程_第40张图片

通过命令 mkfifo < name >

image-20230721152717230

这里创建失败,为什么呢?因为windows系统的文件系统不支持管道文件(匿名管道没有管道文件)

第二章 Linux多进程开发:进程控制,进程通信和守护进程_第41张图片

在linux系统自己的本地文件夹当中创建

image-20230721152127780

观察发现fifo文件的大小为0,这是因为fifo管道文件的信息是存储在内核的缓冲区里面的,当程序结束之后便会清空,留给下一次使用

通过函数 mkfifo()

注意这里的路径也是linux本地文件夹的路径,否则就会被拒绝

// - 通过函数
 	#include 
	#include 

    int mkfifo(const char *pathname, mode_t mode);
// 参数:
//     pathname:管道名称的路径
//     mode:文件的权限 和 open 的 mode 一样,八进制数
// 返回值:
//     成功 返回0
//     失败 返回-1,并设施errno

代码:

#include 
using namespace std;
#include 
#include 
#include 

int main() {
    // 判断文件是否存在
    // access()函数可以获取文件的权限和查看是否存在
    int ret = access("/home/lzx0626/fuck/fifo", F_OK);
    if (ret == -1) {
        printf("管道不存在,创建管道\n");

        ret = mkfifo("/home/lzx0626/fuck/fifo", 0664);
        if (ret == -1) {
            perror("mkfifo");
            return -1;
        }
    }

    return 0;
}

现在我需要写两个进程并且通过有名管道来实现通信,实现write.cpp和read.cpp

// write.cpp
#include 
#include 
using namespace std;
#include 
#include 
#include 
#include 
#define _size 1024

// 向管道中写数据
int main() {
    // 判断管道是否存在,不存在则创建
    int ret = access("/home/lzx0626/fuck/fifo", F_OK);
    if (ret == -1) {
        printf("管道不存在,创建管道\n");

        ret = mkfifo("/home/lzx0626/fuck/fifo", 0664);
        if (ret == -1) {
            perror("mkfifo");
            return -1;
        }
    }

    // 打开管道,以只写的方式
    int fd = open("/home/lzx0626/fuck/fifo", O_WRONLY);
    if (fd == -1) {
        perror("open");
        return -1;
    }

    // 写数据
    for (int i = 0; i < 100; ++i) {
        char buf[_size];
        sprintf(buf, "hello, %d", i);
        printf("write data : %s\n", buf);
        write(fd, buf, strlen(buf));
        sleep(1);
    }

    close(fd);

    return 0;
}
// read.cpp
#include 
#include 
using namespace std;
#include 
#include 
#define _size 1024

// 向管道中读数据
int main() {
    // 打开管道文件,以只读的方式
    int fd = open("/home/lzx0626/fuck/fifo", O_RDONLY);
    if (fd == -1) {
        perror("open");
        return -1;
    }

    // 读取数据
    char buf[_size] = {0};
    while (1) {
        int len = read(fd, buf, sizeof(buf));
        if (len == 0) {
            printf("写端断开连接了...\n");
            break;
        }
        printf("recv buf : %s\n", buf);
        bzero(buf, sizeof(buf));
    }

    close(fd);

    return 0;
}

执行结果:(本来想写注意事项的,都在下面了)

第二章 Linux多进程开发:进程控制,进程通信和守护进程_第42张图片

第二章 Linux多进程开发:进程控制,进程通信和守护进程_第43张图片

有名管道的注意事项:

  • 一个为读而打开一个管道的进程会阻塞,直到另一个进程为写打开管道

  • 一个为写而打开一个管道的进程会阻塞,直到另一个进程为读打开管道
    (可见有名管道的实现还是非常严谨的,双方没有就位不开放)

    (所以测试程序当中先后打开两个进程先打开的进程会等待后打开的进程,这里阻塞就是因为这个,并且是阻塞在open()函数的位置)

读管道:

  • 管道中有数据,read会返回实际读取到的数据
  • 管道中无数据:
    • 管道写端被全部关闭,read返回0(相当于读到文件末尾)
    • 管道写端没有被完全关闭,read阻塞等待

写管道:

  • 管道读端被全部关闭,进程异常终止(收到 SIGPIPE信号)
  • 管道读端没有全部关闭:
    • 管道已经满了,write会阻塞
    • 管道没有满,write会将数据写入,并且返回实际写入的字节数
有名管道通信案例

实现一个简易聊天的功能,循环读写,我写你读,你写我读

思路

第二章 Linux多进程开发:进程控制,进程通信和守护进程_第44张图片

需要注意一点的就是两个进程是你来我往的,所以需要一方先写,另一方先读,否则就会导致阻塞

//一方的代码,另一方稍加修改即可
#include 
#include 
using namespace std;
#include 
#include 
#include 
#include 
#define _size 1024

int main() {
    // 判断有名管道文件1 2是否存在
    int ret = access("/home/lzx0626/FIFO/fifo1", F_OK);
    if (ret == -1) {
        printf("管道1不存在,创建相关的管道文件\n");
        ret = mkfifo("/home/lzx0626/FIFO/fifo1", 0664);
        if (ret == -1) {
            perror("mkfifo");
            return -1;
        }
    }

    ret = access("/home/lzx0626/FIFO/fifo2", F_OK);
    if (ret == -1) {
        printf("管道2不存在,创建相关的管道文件\n");
        ret = mkfifo("/home/lzx0626/FIFO/fifo2", 0664);
        if (ret == -1) {
            perror("mkfifo");
            return -1;
        }
    }

    // 以只写的方式打开管道1
    int fdw = open("/home/lzx0626/FIFO/fifo1", O_WRONLY);
    if (fdw == -1) {
        perror("open");
        return -1;
    }
    printf("打开管道fifo1成功,等待写入...\n");

    // 以只读的方式打开管道2
    int fdr = open("/home/lzx0626/FIFO/fifo2", O_RDONLY);
    if (fdr == -1) {
        perror("open");
        return -1;
    }
    printf("打开管道fifo2成功,等待读取...\n");

    char buf[_size] = {0};
    // 循环的写读数据
    while (1) {
        // 写数据
        bzero(buf, sizeof(buf));
        // 获取标准输入的数据
        fgets(buf, sizeof(buf) - 1, stdin);  // 包含了回车符号
        // 写数据
        int ret = write(fdw, buf, sizeof(buf) - 1);
        if (ret == -1) {
            perror("write");
            return -1;
        }

        // 读数据
        bzero(buf, sizeof(buf));
        int len = read(fdr, buf, sizeof(buf));
        if (len == -1) {
            perror("read");
            return -1;
        }
        if (len == 0)
            break;
        printf("buf : %s", buf);
    }

    // 关闭
    close(fdw);
    close(fdr);

    return 0;
}

问题来了,这个程序只能我写了你读,然后你写了我读,不能实现随意的交流,因为程序的逻辑就是写了之后读,读了之后写,如果写了之后再写,由于读是阻塞在那里的,所以写的东西会存在终端stdin的缓冲区当中,直到read()之后再刷新

现在我想让随时通信,随意的读写,这样不妨可以联想到可以让读和写的操作独立起来,可以用子进程的方式,父进程绑定读,子进程绑定写,这样就可以实现了

//一方的代码,另一方稍微修改一些就好了
#include 
#include 
using namespace std;
#include 
#include 
#include 
#include 
#include 
#define _size 1024

int main() {
    // 父进程写,子进程读
    int pid = fork();
    if (pid == -1) {
        perror("fork");
        return -1;
    }

    if (pid > 0) {
        // 负责写,绑定管道fifo1
        int ret = access("/home/lzx0626/FIFO/fifo1", F_OK);
        if (ret == -1) {
            printf("管道文件fifo1不存在,正在创建...\n");
            // 不存在则创建
            ret = mkfifo("/home/lzx0626/FIFO/fifo1", 0664);
            if (ret == -1) {
                perror("mkfifo");
                return -1;
            }
        }

        // 打开管道
        int fdw = open("/home/lzx0626/FIFO/fifo1", O_WRONLY);
        if (fdw == -1) {
            perror("open");
            return -1;
        }
        printf("父进程管道已打开,等待写入...\n");

        // 写数据
        char buf[_size] = {0};
        while (1) {
            bzero(buf, sizeof(buf));
            fgets(buf, sizeof(buf) - 1, stdin);
            printf("send : %s", buf);
            int len = write(fdw, buf, sizeof(buf) - 1);
            if (len == -1) {
                perror("write");
                return -1;
            }
        }
        // 关闭
        close(fdw);
        // 父进程回收子进程
        wait(NULL);
    } else if (pid == 0) {
        // 负责读,绑定管道fifo2
        int ret = access("/home/lzx0626/FIFO/fifo2", F_OK);
        if (ret == -1) {
            // 不存在则创建
            printf("管道文件fifo2不存在,正在创建...\n");
            ret = mkfifo("/home/lzx0626/FIFO/fifo2", 0664);
            if (ret == -1) {
                perror("mkfifo");
                return -1;
            }
        }

        // 打开管道
        int fdr = open("/home/lzx0626/FIFO/fifo2", O_RDONLY);
        if (fdr == -1) {
            perror("open");
            return -1;
        }
        printf("子进程管道已打开,等待读取...\n");

        // 读数据
        char buf[_size] = {0};
        while (1) {
            bzero(buf, sizeof(buf));
            int len = read(fdr, buf, sizeof(buf));
            if (len == -1) {
                perror("read");
                return -1;
            }
            if (len == 0)  // 读端全部关闭,相当于读到文件末尾
                break;
            printf("recv : %s", buf);
        }
        // 关闭
        close(fdr);
    }

    return 0;
}
2.2 内存映射

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

第二章 Linux多进程开发:进程控制,进程通信和守护进程_第45张图片

相关函数(!!!)

mmap()用来建立映射,munmap()用来取消映射

第二章 Linux多进程开发:进程控制,进程通信和守护进程_第46张图片

    #include 

    void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
// 作用:将一个文件或者设备的数据映射到内存中
// 参数:
//     void *addr:绝大多数情况传nullptr,表示由内核决定映射的内存地址在哪里
//     length:要映射的数据的长度,这个值不能为0,建议使用文件的长度
//         获取文件的长度:stat lseek
//         这个虚拟地址的应映射是以分页为管理的,如果映射的长度不满页的大小,会自动调整为一个分页的大小
//     prot:对我们申请的内存映射区的操作权限
//         - PROT_EXEC  可执行的权限
//         - PROT_READ  读的权限
//         - PROT_WRITE 写的权限
//         - PROT_NONE  没有权限
//         要操作映射区,必须要有读权限,一般给 读权限 或者 读权限和写权限都有 (按位或)
//     flags:
//         - MAP_SHARED 映射区的数据会自动和磁盘文件进行同步,进程间通信必须设置这个选项
//         - MAP_PROVATE 不同步,内存映射区的数据改变了,对原来的文件不会修改,会重新创建一个新的文件(copy on write)
//     fd:需要映射的文件的文件描述符
//         通过open()得到,open的是一个磁盘文件
//         注意,文件的大小不能为0;open指定的权限不能和prot参数的权限有冲突
//         比如,   prot:PROT_READ                    open:只读/读写
//                 prot:PROT_READ | PROT_WRITE       open:读写
//     offset:映射时候的偏移量,必须指定的是4K的整数倍,0表示不偏移
// 返回值:
//     成功 返回创建好的映射区的内存首地址
//     失败 返回 MAP_FAILED (void *)-1,并且设置errno

	int munmap(void *addr, size_t length);
// 功能:释放内存映射
// 参数:
//     addr:要释放的内存映射的首地址
//     length:要释放的内存大小,要和mmap()的length参数值一样
// 

// 
// 使用内存映射实现进程之间通信
// 1.有关系的进程,父子进程
//     在没有子进程的时候,通过唯一的父进程先通过一个大小不是0的磁盘文件创建内存映射区,有了之后再创建子进程,然后父子共享这个内存映射区
// 2.没有关系的进程间通信
//     准备一个大小不是0的磁盘文件
//     进程1通过磁盘文件得到一个内存映射区,得到一个操作这个内存的指针
//     进程2同理,得到一个指针
//     使用内存映射区进行通信

// 注意:内存映射区通信,不会阻塞

父子进程通过内存映射区通信的例子:

思路:通信?内存映射的本质是将文件映射到内存当中形成一块区域,和父子进程联系起来,不妨联想到了匿名管道,父进程在创建子进程之前就创建好内存映射区,然后fork()出子进程,这样父子进程就指向了同一块内存映射区了,就可以互相通信了

#include 
using namespace std;
#include 
#include 
#include 
#include 
#include 
#define _size 1024

int main() {
    // 打开1个文件
    int fd = open("test.txt", O_RDWR);
    if (fd == -1) {
        perror("open");
        return -1;
    }
    // 获取大小
    off_t size = lseek(fd, 0, SEEK_END);

    // 创建内存映射区
    void* ptr = mmap(nullptr, size, PROT_WRITE | PROT_READ, MAP_SHARED, fd, 0);
    if (ptr == MAP_FAILED) {
        perror("mmap");
        return -1;
    }

    // 创建子进程
    pid_t pid = fork();
    if (pid == -1) {
        perror("fork");
        return -1;
    }

    if (pid > 0) {   // 父进程
        wait(NULL);  // 等待子进程写入数据然后回收完毕再读取

        // 读数据
        char buf[_size] = {0};
        strcpy(buf, (char*)ptr);
        printf("read data : %s\n", buf);
    } else if (pid == 0) {  // 子进程
        // 写数据,注意是直接操作这个指针,和管道不一样,管道是通过文件描述符操作
        // 我写的字符串后面带有 '\0' 结束符,不用担心会和原来文件的数据冲突,因为我是从头开始覆盖,然后走到尾部自动补上 '\0',读操作也是一样的
        strcpy((char*)ptr, "nihao");
    }

    // 关闭内存映射区
    int ret = munmap(ptr, size);
    if (ret == -1) {
        perror("munmap");
        return -1;
    }

    // 关闭文件
    close(fd);

    return 0;
}

注意:'\0’就是 char(0),所以我才用 char buf[_size] = {0} 对字符串初始化!!!

不相关的进程之间通过内存映射通信

思路:写进程先打开文件,创建映射区,然后修改数据,然后读进程打开文件,创建映射区然后读取修改后的数据;我这里的设计两个进程不能并发执行,因为读进程不是阻塞的,这样读取的是文件中原本的数据,需要等待写进程写完数据之后再执行

// write.cpp
#include 
using namespace std;
#include 
#include 
#include 
#include 
#include 
#define _size 1024

int main() {
    // 打开文件
    int fd = open("test.txt", O_RDWR);
    if (fd == -1) {
        perror("open");
        return -1;
    }

    // 获取文件大小
    struct stat statbuf;
    int ret = stat("test.txt", &statbuf);
    if (ret == -1) {
        perror("stat");
        return -1;
    }
    off_t size = statbuf.st_size;

    // 创建内存映射区
    void* ptr = mmap(nullptr, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    if (ptr == MAP_FAILED) {
        perror("mmap");
        return -1;
    }

    // 操作这块内存
    char buf[_size] = {0};
    fgets(buf, sizeof(buf) - 1, stdin);  // 保证后面留有一个'\0'符号
    // 写数据
    strcpy((char*)ptr, buf);

    // 关闭内存映射区
    munmap(ptr, size);
    // 关闭文件
    close(fd);

    return 0;
}
// read.cpp
#include 
using namespace std;
#include 
#include 
#include 
#include 
#include 
#define _size 1024

int main() {
    // 打开文件
    int fd = open("test.txt", O_RDWR);
    if (fd == -1) {
        perror("open");
        return -1;
    }

    // 获取文件大小
    struct stat statbuf;
    int ret = stat("test.txt", &statbuf);
    if (ret == -1) {
        perror("stat");
        return -1;
    }
    off_t size = statbuf.st_size;

    // 创建内存映射区
    void* ptr = mmap(nullptr, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    if (ptr == MAP_FAILED) {
        perror("mmap");
        return -1;
    }

    // 操作这块内存
    char buf[_size] = {0};
    // 读数据
    strcpy(buf, (char*)ptr);
    printf("read data : %s", buf);

    // 关闭内存映射区
    munmap(ptr, size);
    // 关闭文件
    close(fd);

    return 0;
}

执行结果:

image-20230722155039444

但是要注意一点,写的数据不能比文件本身的大小大,不然就会超出内存的大小范围了,就会写不进去!!!

思考问题

1.如果对mmap的返回值(ptr)做++操作(ptr++), munmap是否能够成功?
void * ptr = mmap(…);
ptr++; 可以对其进行++操作
munmap(ptr, len); // 错误,要保存地址

2.如果open时O_RDONLY, mmap时prot参数指定PROT_READ | PROT_WRITE会怎样?
错误,返回MAP_FAILED
open()函数中的权限建议和prot参数的权限保持一致。

3.如果文件偏移量为1000会怎样?
偏移量必须是4K的整数倍,返回MAP_FAILED

4.mmap什么情况下会调用失败?
- 第二个参数:length = 0
- 第三个参数:prot
- 只指定了写权限
- prot PROT_READ | PROT_WRITE
第5个参数fd 通过open函数时指定的 O_RDONLY / O_WRONLY

5.可以open的时候O_CREAT一个新文件来创建映射区吗?
- 可以的,但是创建的文件的大小如果为0的话,肯定不行
- 可以对新的文件进行扩展
- lseek()
- truncate()

6.mmap后关闭文件描述符,对mmap映射有没有影响?
int fd = open(“XXX”);
mmap(,fd,0);
close(fd);
映射区还存在,创建映射区的fd被关闭,没有任何影响。

7.对ptr越界操作会怎样?
void * ptr = mmap(NULL, 100,);
映射出来会调整为4K
越界操作操作的是非法的内存 -> 段错误

示例

将english.txt文件拷贝一份为cpy.txt,保存在当前目录

思路:两个文件分别映射到内存当中,然后操纵内存进行复制即可

注意:新文件需要预分配大小,不能出现空文件

#include 
#include 
using namespace std;
#include 
#include 
#include 

// 将english.txt文件拷贝一份为cpy.txt,保存在当前目录
int main() {
    // 打开english.txt
    int fd_src = open("english.txt", O_RDONLY);
    if (fd_src == -1) {
        perror("open");
        return -1;
    }

    // 获取english.txt的大小
    off_t size = lseek(fd_src, 0, SEEK_END);

    // 创建内存映射区
    void* ptr_src = mmap(nullptr, size, PROT_READ, MAP_SHARED, fd_src, 0);
    if (ptr_src == MAP_FAILED) {
        perror("mmap");
        return -1;
    }

    // 创建cpy.txt
    int ret = access("copy.txt", F_OK);
    if (ret == 0)  // 存在把他删除
        unlink("cpy.txt");
    // 创建
    int fd_dest = open("cpy.txt", O_RDWR | O_CREAT, 0664);
    if (fd_dest == -1) {
        perror("open");
        return -1;
    }
    // 将空文件的大小修改为源文件的大小,防止出现空文件
    ret = truncate("cpy.txt", size);
    // ret = lseek(fd_dest, size, SEEK_END);
    // write(fd_dest, " ", 1);  // lseek扩展文件需要进行一次写的操作,truncate不需要!!!
    if (ret == -1) {
        perror("truncate");
        return -1;
    }

    // 创建内存映射区
    void* ptr_dest = mmap(nullptr, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd_dest, 0);
    if (ptr_dest == MAP_FAILED) {
        perror("mmap");
        return -1;
    }

    // 拷贝
    strncpy((char*)ptr_dest, (char*)ptr_src, size);

    // 关闭内存映射区
    munmap(ptr_src, size);
    munmap(ptr_dest, size);
    // 关闭文件
    close(fd_src);
    close(fd_dest);

    return 0;
}

这里我们得到了两种扩展文件内存的方式,分别使用truncate()和lseek()

  • truncate()中,size是想要扩展到的文件大小
  • lseek()中,length是在SEEK_END基础上的文件指针偏移量,最后文件指针会走到length+SEEK_END的位置,这是原来文件走不到的位置,因此文件就相应扩展了,length就是扩展了的文件大小
  • lseek扩展文件需要进行一次写的操作,truncate不需要!!!
truncate("cpy.txt", size);

lseek(fd_dest, length, SEEK_END);
write(fd_dest, " ", 1);  // lseek扩展文件需要进行一次写的操作,truncate不需要!!!
匿名映射

顾名思义,匿名映射,都匿名了,说明没有文件实体做支撑了吧,因此两个不相关的进程不适用于这个,因为找不到连接的接口,而父子进程恰好可以用这个来通信

flags参数当中,MAP_SHARED和MAP_PRIVATE参数是必选一个,然后其他的是可选项,MAP_ANONYMOUS就是一个可选项

#include 
using namespace std;
#include 
#include 
#include 
#include 
#include 
#define _size 1024

/*
匿名映射:不需要文件实体进行一个内存映射,只能在父子和有关系的进程之间通信,因为没有办法通过文件进行关联
- 修改flags参数,做匿名映射需要传入 MAP_ANONYMOUS,这样会忽略掉fd参数,一般我们传入-1
- flags参数当中,MAP_SHARED和MAP_PRIVATE参数是必选一个,然后其他的是可选项,MAP_ANONYMOUS就是一个可选项
*/

int main() {
    // 创建匿名内存映射区
    int length = 4096;
    void* ptr = mmap(nullptr, length, PROT_READ | PROT_WRITE, MAP_ANONYMOUS | MAP_SHARED, -1, 0);
    if (ptr == MAP_FAILED) {
        perror("mmap");
        return -1;
    }

    // 父子进程间通信
    pid_t pid = fork();
    if (pid == -1) {
        perror("fork");
        return -1;
    }

    if (pid > 0) {  // 父进程
        strcpy((char*)ptr, "helloworld");
        wait(nullptr);
    } else if (pid == 0) {  // 子进程
        sleep(1);           // 非阻塞,先睡一秒让父进程执行完写入操作
        printf("%s\n", (char*)ptr);
    }

    // 关闭内存映射区
    int ret = munmap(ptr, length);
    if (ret == -1) {
        perror("munmap");
        return -1;
    }

    return 0;
}
2.3 信号
概念

第二章 Linux多进程开发:进程控制,进程通信和守护进程_第47张图片

软件中断

处理紧急事务,完毕后回到保存的位置继续执行

第二章 Linux多进程开发:进程控制,进程通信和守护进程_第48张图片
目的

让进程知道了已经发生了某一个特定的事情;强迫进程执行他自己代码当中的信号处理程序

第二章 Linux多进程开发:进程控制,进程通信和守护进程_第49张图片
Linux信号列表

一共62个(32 33没有),1-31是常规信号,32-64是预定义好的信号,目前还没有使用,将来可能会使用,并且是实时信号

第二章 Linux多进程开发:进程控制,进程通信和守护进程_第50张图片

红色信号比较重要:

第二章 Linux多进程开发:进程控制,进程通信和守护进程_第51张图片

第二章 Linux多进程开发:进程控制,进程通信和守护进程_第52张图片

信号的5种默认处理动作

其中,Core是指终止进程并且生成一个Core文件,这个文件当中记载了程序异常终止时候保存的一些错误信息等等

第二章 Linux多进程开发:进程控制,进程通信和守护进程_第53张图片

查看并且使用Core文件

Core文件里面记录了程序异常退出的状态信息,可以让程序在异常退出的时候生成Core文件,方便我们查看和调试错误的信息

系统默认在进程异常退出的时候是不会产生Core文件的,通过 ulimit -a 命令查看:

可以看出 core file size 这一项默认是0

第二章 Linux多进程开发:进程控制,进程通信和守护进程_第54张图片

我们将其设置一下,可以设置为一个具体的大小或者不限制

第二章 Linux多进程开发:进程控制,进程通信和守护进程_第55张图片

现在就应该有了

还要注意一点:这个Core文件要想被记录下来,必须在Linux的本地目录当中操作,不能在和windows的共享文件夹或者windows文件夹当中操作,这样出来的Core文件是会生成,但是大小为0,没有用

源代码:

#include 
using namespace std;
#include 

int main() {
    char* buf;

    strcpy(buf, "hello");
    //这里显然会报错,因为buf指针没有被初始化,根本不知道操作的是哪一块内存,指向的字符串区域有多大,是个野指针

    return 0;
}
第二章 Linux多进程开发:进程控制,进程通信和守护进程_第56张图片

执行 a.out 之后,报了段错误,我们来查看下core文件的大小,不为0,可见记录了相关错误信息

第二章 Linux多进程开发:进程控制,进程通信和守护进程_第57张图片

怎么查看呢?可以通过gdb调试来查看,这也是为什么我们编译的时候加上 -g 调试参数的原因

通过gdb调试可执行文件a.out,里面有一个命令

core-file core #用来查看该可执行文件对应的core文件信息
第二章 Linux多进程开发:进程控制,进程通信和守护进程_第58张图片

可以看出程序的异常终止是因为这一行的问题,与我们的预期一致;

而发出的终止信号SIGSEGV的含义就是程序进行了无效的内存访问,也就是段错误

相关函数
kill(),raise(),abort()

第二章 Linux多进程开发:进程控制,进程通信和守护进程_第59张图片

    #include 

    int kill(pid_t pid, int sig);
// 作用:给任何的进程或者进程组pid,发送任何的信号sig
// 参数:
//     pid:
//         >  0  将信号发送给指定的进程
//         == 0  将信号发送给当前的进程组中所有的进程
//         == -1 将信号发送给每一个有权限接受这个信号的进程
//         <  -1 这个pid = 某个进程组的ID的相反数,给这个进程组中所有的进程发送信号

//     sig:需要发送的信号编号或者宏值,如果是0则表示不发送任何信号
// 返回值:成功 0 ; 失败 -1,并设置errno


    int raise(int sig);
// 作用:给当前进程发送信号
// 参数:sig:需要发送的信号编号或者宏值,如果是0则表示不发送任何信号
// 返回值: 成功 0 ; 失败 非 0
//     kill(getpid(),sig);


    void abort(void);
// 功能:发送一个SIGABRT信号给当前的进程,默认是杀死当前的进程
//     kill(getpid(),SIGABRT);

测试程序:

#include 
using namespace std;
#include 

int main() {
    pid_t pid = fork();
    if (pid == -1) {
        perror("fork");
        return -1;
    }

    if (pid > 0) {
        printf("parent process\n");
        sleep(2);
        printf("kill child process now\n");
        kill(pid, SIGINT);
    } else if (pid == 0) {
        for (int i = 0; i < 5; ++i) {
            printf("child process\n");
            sleep(1);
        }
    }

    return 0;
}

执行结果:

第二章 Linux多进程开发:进程控制,进程通信和守护进程_第60张图片 image-20230722193131543

有可能为2次也有可能为3次,因为父子进程是并发执行的,谁先运行要看抢占cpu,这里如果是父进程先执行就是2次,子进程先执行就会先偷偷打印一次,就是3次,这也体现了多进程程序执行结果的不确定性,这完全取决于操作系统对于进程的调度

alarm()

这里面请注意alarm()函数的返回值和设置参数为0时候的情况

定时器到时间之后程序会接受 SIGALARM 信号,然后终止

    #include 

    unsigned int alarm(unsigned int seconds);
// 作用:去设置定时器;函数调用,开始倒计时;
//     当倒计时为0的时候,函数会给当前的进程发送一个信号 SIGALARM
// 参数:
//     seconds:倒计时时长,单位秒,当参数为0的时候,计时器无效(不进行倒计时,也不发送信号)
//         取消一个定时器 alarm(0)
// 返回值:
//     - 之前没有定时器,返回0
//     - 之前有定时器,返回之前定时器剩余的时间

// - SIGALARM 信号:默认终止当前的进程,每一个进程都有且只有唯一的一个定时器
//     alarm(10); ->返回0
//     //过了一秒
//     alarm(5);  ->返回9

// 该函数不阻塞,设置之后会继续往下执行
#include 
using namespace std;
#include 

int main() {
    int seconds = alarm(5);             // 不阻塞
    printf("seconds : %d\n", seconds);  // 0

    sleep(2);
    seconds = alarm(2);
    printf("seconds : %d\n", seconds);  // 3

    while (1)
        ;

    return 0;
}

执行结果:

image-20230722200410797

案例:查看计算机一秒能输出多少个数

#include 
using namespace std;
#include 

// 1秒钟电脑能数多少个数
int main() {
    alarm(1);

    int i = 0;
    while (1)
        printf("%d\n", i++);

    return 0;
}

执行结果:

第二章 Linux多进程开发:进程控制,进程通信和守护进程_第61张图片

但是我们观察到输出完毕花的时间好像不止1秒,这是为什么呢?

注意到这里输出的数好多好多,而终端的输出是依靠内核当中的012文件描述符,0标准输入STDIN_FILENO,1标准输出STDOUT_FILENO,2标准错误STDERR_FILENO,然后要想输出到终端必须要经历特定的事件,比如程序终止或者遇到回车’\n’等等,这是因为终端有缓冲区的存在;之所以花了超过1秒是因为从缓冲区输出到屏幕上,还是输出了这么多的数据,花费了大量时间

但是这里只数了20万不到,感觉少太多了,这是因为往终端上输出的时候需要调用文件描述符,输出一次调用一次磁盘I/O,这样浪费时间,CPU没有百分百去执行数的这个操作,我们可以将其输出重定向到一个文本当中,这样只用调用一次文件I/O就可以把文件写在里面

image-20230722201704262

第二章 Linux多进程开发:进程控制,进程通信和守护进程_第62张图片

可见这样就多了很多

实际的时间 = 内核时间 + 用户时间 + 消耗的时间(比如I/O操作)

进行文件I/O操作的时候比较浪费时间

定时器,和进程的状态无关(自然定时法);无论进程处于什么状态,这个alarm()都会计时

settimer()

注意注释的介绍

    #include 

    int setitimer(int which, const struct itimerval *restrict new_value,
                struct itimerval *_Nullable restrict old_value);
// 作用:设置定时器;可以替代alarm函数。精度可以达到微秒,并且还可以实现周期性的定时
// 参数:
//     which:指定的是定时器以什么时间计时
//         - ITIMER_REAL:真实时间(包含内核+用户+消耗的时间(例如I/O)),时间到达发送 SIGALRM 常用
//         - ITIMER_VIRTUAL:用户时间,时间到达发送 SIGVTALRM
//         - ITIMER_PROF:以该进程在用户态和内核态所消耗的时间来计算,时间到达发送 SIGPROF

//     new_value:设置定时器属性

//         struct itimerval { //定时器的结构体
//             struct timeval it_interval;  // 每个阶段的时间,间隔时间
//             struct timeval it_value;     // 延迟多长时间执行定时器
//         };

//         struct timeval { //时间的结构体
//             time_t tv_sec;        // 秒数
//             suseconds_t tv_usec;  // 微秒
//         };

//         eg:过10秒(it_value)后,每隔2秒(it_interval)定时一次

//     old_value:记录上一次定时的时间参数,是一个传出参数,函数将上一次的状态心如进去,一般不使用,就指定nullptr就可以了
// 返回值:
//     成功 0
//     失败 -1,设置errno

代码:

#include 
using namespace std;
#include 

// 过3秒以后,每隔2秒定时一次
int main() {
    // 过三秒,会发送信号
    struct timeval _value;
    _value.tv_sec = 3;
    _value.tv_usec = 0;

    // 每隔两秒,会发送信号
    struct timeval _interavl;
    _interavl.tv_sec = 2;
    _interavl.tv_usec = 0;

    // itimerval结构体
    struct itimerval new_value;
    new_value.it_value = _value;
    new_value.it_interval = _interavl;

    // 设置定时器
    int ret = setitimer(ITIMER_REAL, &new_value, nullptr);  // 非阻塞
    printf("定时器开始了\n");                               // 立刻执行,表明是非阻塞的
    if (ret == -1) {
        perror("setitimer");
        return -1;
    }

    while (1)
        ;

    return 0;
}

3秒的延迟开始和2秒的定时间隔到了后都会发送信号,因此程序执行下来是在3秒之后就退出了,这是3秒的延迟开始的信号

而且"定时器开始了"这句话是立刻开始的,这就表明这个函数是非阻塞的,这个函数执行后,虽然定时器还没有开始,但是程序继续在执行

但是这样看起来没有办法实现每2秒一次的效果,这就需要捕捉信号,这样才能做我们自己的事情

信号捕捉函数

第二章 Linux多进程开发:进程控制,进程通信和守护进程_第63张图片

signal()

注意回调函数的函数格式定义是有要求的,就是 typedef 那一行,那是个函数指针的写法,要求我们传入的回调函数返回值必须为void,然后参数是int,这个记录的是捕捉到的信号的编号

    #include 

    typedef void (*sighandler_t)(int);
    sighandler_t signal(int signum, sighandler_t handler);
// 作用:设置某个信号的捕捉行为,
// 参数:
//     signum:要捕捉的信号
//         注意:SIGKILL 和 SIGSTOP 不能被捕捉,不能被忽略;
//             因为这两个信号都是带有强制性的杀死或者暂停进程,这个是需要保证权威的,否则强制性都解决不了就可以被不法分子利用了,比如制作病毒让进程一直运行消耗资源,这两个信号没有办法解决
//     hander:捕捉到信号要如何处理
//         SIG_IGN:忽略信号
//         SIG_DFL:用信号默认的行为
//         回调函数:这个函数是内核调用,程序员只负责写,捕捉到信号后如何去处理信号
//             回调函数需要程序员实现,提前准备好,函数的类型根据实际需求,看函数指针的定义
//             不是程序员调用的,而是当信号产生由内核调用
//             函数指针是实现回调的手段,函数实现后,将函数名放到函数指针的位置就可以了

// 返回值:
//     成功,返回上一次注册的信号处理函数的地址;第一次返回nullptr
//     失败,返回SIG_ERR,设置errno
#include 
using namespace std;
#include 
#include 

void myalarm(int num) {
    printf("捕捉到了信号的编号是: %d\n", num);
}

int main() {
    // 注册信号捕捉,需要提前注册,避免定时器开始执行后可能信号捕捉还没生效导致错过信号捕捉的情况
    // signal(SIGALRM, SIG_IGN);  // 信号产生后忽略信号,程序会一直执行
    // signal(SIGALRM, SIG_DFL);  // 按照默认的方式处理信号,程序延迟3秒的时候开始计时,发送信号然后终止

    // typedef void (*sighandler_t)(int); 函数指针的类型,int类型的参数表示捕捉到的信号的值
    sighandler_t ret = signal(SIGALRM, myalarm);
    if (ret == SIG_ERR) {
        perror("signal");
        return -1;
    }

    // 过三秒,会发送信号
    struct timeval _value;
    _value.tv_sec = 3;
    _value.tv_usec = 0;

    // 每隔两秒,会发送信号
    struct timeval _interavl;
    _interavl.tv_sec = 2;
    _interavl.tv_usec = 0;

    // itimerval结构体
    struct itimerval new_value;
    new_value.it_value = _value;
    new_value.it_interval = _interavl;

    // 设置定时器
    int rets = setitimer(ITIMER_REAL, &new_value, nullptr);  // 非阻塞
    printf("定时器开始了\n");                                // 立刻执行,表明是非阻塞的
    if (rets == -1) {
        perror("setitimer");
        return -1;
    }

    while (1)
        ;

    return 0;
}
信号集

位图机制:信号集是一堆信号的集合,那么怎么去表示这个信号集呢?我们知道信号是用一个整数的序号表示的(1-31 34-64),所以我们用类似于文件st_mode那个的形式,用每一位来表示一个信号,0 1 表示信号有无,这样信号集就相当于是一个整数,而想要添加一个信号进去就用这个信号(用信号集的格式表示)按位或就好了

信号三种状态:

产生:信号产生

未决:信号产生到信号被处理之前的这段时间

抵达:信号抵达

然后阻塞信号是指阻止信号被处理,而不是阻止信号产生;阻塞就是让系统保持信号,留着以后发送

我们可以设置阻塞信号集,表示要阻塞哪些信号;而不能修改或者设置未决信号集,我们不能阻止信号的产生;

系统PCB当中自带阻塞信号集和未决信号集,我们也不能直接操作,需要借助系统提供的API才能操作

第二章 Linux多进程开发:进程控制,进程通信和守护进程_第64张图片

阻塞信号集和未决信号集(在PCB当中)

  • 用户通过键盘 Ctrl + C,产生SIGINT信号,信号被创建
  • 信号产生,但是没有被处理,未决状态,以下是工作过程(也解释了为什么信号产生了不会被立即处理)
    • 在内核当中,将所有的没有被处理的信号存储在一个集合当中(未决信号集)
    • SIGINT信号,状态存储在第二个标志位,这个标志位的值为0说明信号不是未决状态,为1说明信号是未决状态
  • 这个未决状态的信号,需要被处理,处理之前需要和另一个信号集(阻塞信号集)对应的标志位进行比较
    • 阻塞信号集默认不阻塞所有信号
    • 如果想要阻塞某些信号,需要用户调用系统的API
  • 在处理的时候和阻塞信号集中的标志位查询,看是不是对该信号设置了阻塞
    • 没有阻塞,这个信号就会被处理
    • 如果阻塞了,这个信号就继续处于未决状态,直到阻塞解除,这个信号被处理

第二章 Linux多进程开发:进程控制,进程通信和守护进程_第65张图片

相关函数

第二章 Linux多进程开发:进程控制,进程通信和守护进程_第66张图片

前面五个,都是对自己定义的信号集进行操作,信号集的类型是 sigset_t,本质就是一个数组,下标对应信号signum,值代表是否信号状态

	#include 

// 以下的信号集相关的函数都是对自定义的信号集进行操作,我们不能直接修改系统当中的未决信号集和阻塞信号集!!!

	int sigemptyset(sigset_t *set);
// 功能:清空信号集中的数据,将信号集中的所有标志位置为0
// 参数:set,传出参数,需要操作的信号集
// 返回值:成功 0;失败 -1,修改errno

	int sigfillset(sigset_t *set);
// 功能:将信号集中的所有标志位置为1
// 参数:set,传出参数,需要操作的信号集
// 返回值:成功 0;失败 -1,修改errno

	int sigaddset(sigset_t *set, int signum);
// 功能:设置信号集中的某一个信号对应的标志位为1,表示阻塞这个信号
// 参数:set,传出参数,需要操作的信号集;signum:需要设置为阻塞的信号
// 返回值:成功 0;失败 -1,修改errno

	int sigdelset(sigset_t *set, int signum);
// 功能:设置信号集中的某一个信号对应的标志位为0,表示不阻塞这个信号
// 参数:set,传出参数,需要操作的信号集;signum:需要设置不为阻塞的信号
// 返回值:成功 0;失败 -1,修改errno

	int sigismember(const sigset_t *set, int signum);
// 功能:判断某个信号是否阻塞
// 参数:set,需要操作的信号集;signum:需要查看是否阻塞的信号
// 返回值:(与前面不一样!!!)
//     1 是成员,signum被阻塞;0 不是成员,不阻塞
//     -1 表示失败,修改errno

代码:

#include 
using namespace std;
#include 

void Judge(const sigset_t& set, const int& signum) {
    int ret = sigismember(&set, signum);
    if (ret == -1) {
        perror("sigismember");
        exit(-1);
    }

    if (ret == 1)
        printf("信号%d在set当中\n", signum);
    else if (ret == 0)
        printf("信号%d不在set当中\n", signum);
}

int main() {
    // 创建一个信号集
    sigset_t set;

    // 这么创建的数据一般是随机的,我们一般用系统的api清空
    int ret = sigemptyset(&set);
    if (ret == -1) {
        perror("sigemptyset");
        return -1;
    }

    // 判断SIGINT是否在信号集set中
    Judge(set, SIGINT);  // 2号信号不在

    // 添加几个信号
    ret = sigaddset(&set, SIGINT);
    if (ret == -1) {
        perror("sigaddset");
        return -1;
    }

    ret = sigaddset(&set, SIGQUIT);
    if (ret == -1) {
        perror("sigaddset");
        return -1;
    }

    // 判断是否在信号集set中
    Judge(set, SIGINT);   // 2号信号在
    Judge(set, SIGQUIT);  // 3号信号在

    // 删除一个信号
    ret = sigdelset(&set, SIGQUIT);
    if (ret == -1) {
        perror("sigdelset");
        return -1;
    }

    // 判断SIGQUIT是否在信号集set中
    Judge(set, SIGQUIT);  // 3号信号不在

    return 0;
}

sigprocmask()和sigpending()

调用之后就可以把我们自己设置的信号集设置到系统提供的阻塞信号集当中,这也是我们唯一能设置系统内核PCB中的信号集,未决信号集不能被设置或者处理,只能被读取

第二章 Linux多进程开发:进程控制,进程通信和守护进程_第67张图片
    #include 

	int sigprocmask(int how, const sigset_t *_Nullable restrict set,
                                   sigset_t *_Nullable restrict oldset);
// 功能:将自定义信号集中的数据设置到内核当中(设置阻塞,接触阻塞,替换)
// 参数:
//     how:如何对内核阻塞信号集进行处理
//         SIG_BLOCK:将用户设置的阻塞信号集添加到内核中,原来的数据不变
//             假设中内核中默认的阻塞信号集是mask,则 mask | set (添加的公式)
//         SIG_UNBLOCK:根据用户设置的数据,对内核中的数据进行接触阻塞
//             mask & = ~ set (去除的公式)
//             比如 mask 1 0 1 1 1 ,set 0 0 1 0 1,解除这两位的阻塞
//             那么就是 ~set 1 1 0 1 0 ,然后想与就得到 1 0 0 1 0
//         SIG_SETMASK:覆盖内核中原来的值

//     set:已经初始化好的用户自定义的信号集
//     oldset:保存的之前内核中的阻塞信号集的状态,传出参数,一般不使用,设置为nullptr即可
// 返回值:
//     成功 0
//     失败 -1,并且设置errno,有两个值:EFAULT,EINVAL

	int sigpending(sigset_t *set);
// 功能:获取内核中的未决信号集
// 参数:set,传出参数,保存的是内核中的未决信号集
// 返回值:
//     成功 0,失败 -1,设置errno

注意一点,就是在二进制数当中,添加位数为1和解除位数为1(变为0)的操作

mask | set //添加
mask & = ~set //解除

现在我们需要写一个程序,用来查看内核当中的未决信号集,并且设置某些信号阻塞,然后再次查看

#include 
using namespace std;
#include 
#include 

// 编写一个程序,把所有的常规信号(1-31)的未决状态打印到屏幕
// 设置某些信号是阻塞的,通过键盘产生这些信号
int main() {
    // 设置 2号信号 SIGINT(ctrl+C) 和 3号信号SIGQUIT(ctrl+\) 阻塞
    sigset_t set;
    // 清空
    int ret = sigemptyset(&set);
    if (-1 == ret) {
        perror("sigemptyset");
        return -1;
    }

    // 将2号和3号信号添加进去
    ret = sigaddset(&set, SIGINT);
    if (-1 == ret) {
        perror("sigaddset");
        return -1;
    }
    ret = sigaddset(&set, SIGQUIT);
    if (-1 == ret) {
        perror("sigaddset");
        return -1;
    }

    // 修改内核中的信号集
    ret = sigprocmask(SIG_BLOCK, &set, nullptr);
    if (-1 == ret) {
        perror("sigprocmask");
        return -1;
    }

    int count = 0;

    // 在循环当中获取未决信号集的数据
    while (1) {
        sigset_t pendingset;
        ret = sigemptyset(&pendingset);
        if (-1 == ret) {
            perror("sigemptyset");
            return -1;
        }

        sigpending(&pendingset);

        // 遍历前32位 即1-31号(0号没用)
        for (int i = 1; i < 32; ++i) {
            ret = sigismember(&pendingset, i);
            if (-1 == ret) {
                perror("sigismember");
                return -1;
            }

            if (1 == ret)
                printf("1");
            else if (0 == ret)
                printf("0");
        }
        puts("");

        // 为了防止只能通过kill -9 命令杀死该进程,现在我们计数,到10就接触阻塞
        if (count++ == 10) {
            printf("2号信号SIGINT和3号信号SIGQUIT已经解除阻塞\n");
            ret = sigprocmask(SIG_UNBLOCK, &set, nullptr);
            if (-1 == ret) {
                perror("sigprocmask");
                return -1;
            }
        }
        sleep(1);
    }

    return 0;
}

这里我们设置了10秒后就会解除阻塞,因为我们需要防止这个进程只能通过kill -9命令强制杀死,给自己留一条后路

执行结果:

可见,当我们输出 ctrl+c 和ctrl+\ 的时候,未决信号集里面添加了这两个信号,但是由于我们设置了阻塞,不会去立即处理,这种情况会持续到我解除他的阻塞才行,所以他很急,但是他没得选择。当我解除了这两个信号的阻塞后,马上就处理了,程序异常终止

这里输出一个空行是因为第一,字符串我输出了换行,第二,SIGINT信号和SIGQUIT信号执行后都会输出空行,这里是执行了SIGINT信号

第二章 Linux多进程开发:进程控制,进程通信和守护进程_第68张图片

补充:将程序挂到后台执行,加上 & 符号

./a.out &

程序到后台运行,所以我 ctrl+c 没有用,并且我可以执行我自己的命令,图中就执行了ls和kill -9

并且由于这个进程的输出是默认定向到终端的,所以终端会输出

第二章 Linux多进程开发:进程控制,进程通信和守护进程_第69张图片

切换到前台

fg

第二章 Linux多进程开发:进程控制,进程通信和守护进程_第70张图片

(续信号捕捉函数)sigaction()
    #include 

    int sigaction(int signum,
                        const struct sigaction *_Nullable restrict act,
                        struct sigaction *_Nullable restrict oldact);
// 作用:用来检查或者改变信号的处理,信号捕捉
// 参数:
//     signum:需要捕捉的信号的编号或者宏值
//     act:捕捉到信号之后相应的处理动作
//     oldact:上一次对信号捕捉的相关的设置,一般不使用,传递nullptr
// 返回值:
//     成功 0
//     失败 -1,设置errno

    struct sigaction {
            //函数指针,指向的函数就是信号捕捉到之后的处理函数
            void     (*sa_handler)(int);
            //函数指针,一般不使用
            void     (*sa_sigaction)(int, siginfo_t *, void *);
            //临时阻塞信号集,在信号捕捉函数执行过程中会临时阻塞某些信号,执行完之后恢复
            sigset_t   sa_mask;
            //指定是用第一个回调处理sa_handler还是第二个sa_sigaction,0表示第一个,SA_SIGINFO表示第二个,还有其他的值,但是用的少
            int        sa_flags;
            //被废弃掉了,不需要用,传入nullptr
            void     (*sa_restorer)(void);
	};

代码:

#include 
using namespace std;
#include 
#include 

void myalarm(int num) {
    printf("捕捉到了信号的编号是: %d\n", num);
}

int main() {
    struct sigaction act;
    act.sa_flags = 0;
    act.sa_handler = myalarm;
    sigemptyset(&act.sa_mask);  // 清空吧,表示不要临时阻塞任何信号

    int ret = sigaction(SIGALRM, &act, nullptr);
    if (-1 == ret) {
        perror("signal");
        return -1;
    }

    // 过三秒,会发送信号
    struct timeval _value;
    _value.tv_sec = 3;
    _value.tv_usec = 0;

    // 每隔两秒,会发送信号
    struct timeval _interavl;
    _interavl.tv_sec = 2;
    _interavl.tv_usec = 0;

    // itimerval结构体
    struct itimerval new_value;
    new_value.it_value = _value;
    new_value.it_interval = _interavl;

    // 设置定时器
    int rets = setitimer(ITIMER_REAL, &new_value, nullptr);  // 非阻塞
    printf("定时器开始了\n");                                // 立刻执行,表明是非阻塞的
    if (rets == -1) {
        perror("setitimer");
        return -1;
    }

    while (1)
        ;

    return 0;
}

执行结果和signal.cpp是一样的,延迟三秒后开始定时器发送信号,然后每隔两秒发送信号

比较二者

建议使用 sigaction()

  • signal()是ANSI C signal handling,是美国那边的标准,对其他的标准例如POSIX可能不匹配,所以有一定局限性
  • sigaction()是标准的,也可以说是改进过的函数,基本都能适配标准,并且功能更多
更好理解信号捕捉

第二章 Linux多进程开发:进程控制,进程通信和守护进程_第71张图片

要注意几点:

  • 在sigaction()中,处理信号的时候使用的是我们传递进去的临时阻塞信号集,当处理结束之后会回到PCB当中的阻塞信号集
  • 信号发出之后不会立即处理,先进入未决信号集,变为1,然后去找对应的阻塞信号集,不阻塞则处理,并且修改未决信号集相应为0,当信号在处理过程当中如果未处理完毕这时候收到一个对应的新的信号,不会处理,而是先填入未决信号集,然后等待处理结束然后处理
  • 如果查找阻塞信号集发现阻塞,则阻塞等待,这个时候如果收到新的信号,由于未决信号集相应位置都还是1,那么表示信号尚未被处理,新来的信号会被忽略,当然也不可能记录来了几个,到时候一起处理这种,因为只能存0 1,这也是忽略的原因

第二章 Linux多进程开发:进程控制,进程通信和守护进程_第72张图片

SIGCHLD信号

顾名思义,这是子进程给父进程发送的信号

产生的三种条件:

  • 子进程终止
  • 子进程收到SIGSTOP信号停止
  • 子进程处在停止态,收到SIGCONT唤醒

父进程接收到这个信号之后,默认处理是忽略这个信号

如果我们能接受这个信号,然后去回收子进程的资源,因为wait()函数是阻塞的,父进程不可能一直等待子进程等待结束然后回收,那么可以捕捉子进程结束时候(当然还有其他两种情况)发出的SIGCHLD信号,然后父进程中断去处理这个事情,回收子进程,这样就很好的避免了僵尸进程的问题

第二章 Linux多进程开发:进程控制,进程通信和守护进程_第73张图片

代码:

#include 
using namespace std;
#include 
#include 
#include 

/*
    SIGCHLD信号产生的三个条件
    - 子进程结束
    - 子进程暂停
    - 子进程从暂停状态继续运行
    都会给父进程发送该信号,父进程默认忽略该信号

    可以使用SIGCHLD信号解决僵尸进程的问题

*/

void myFunc(int num) {
    printf("捕捉到的信号 : %d\n", num);
    // 回收子进程PCB的资源
    // wait(nullptr);

    while (1) {
        int ret = waitpid(-1, nullptr, WNOHANG);
        if (ret > 0) {
            printf("chile die , pid = %d\n", getpid());
        } else if (0 == ret)
            // 说明还有子进程,这一次的循环捕捉回收没回收完毕
            break;
        else if (-1 == ret)
            // 说明没有子进程了
            break;
    }
}

int main() {
    // 创建子进程
    pid_t pid;
    for (int i = 0; i < 20; ++i) {
        pid = fork();
        if (0 == pid)
            break;
    }

    if (pid > 0) {
        // 父进程

        // 提前设置好阻塞信号集,阻塞SIGCHLD,因为子进程可能很快结束,父进程还没注册好
        sigset_t set;
        sigemptyset(&set);
        sigaddset(&set, SIGCHLD);
        sigprocmask(SIG_BLOCK, &set, nullptr);

        // 捕捉子进程死亡时发送的SIGCHLD信号
        struct sigaction act;
        act.sa_flags = 0;
        act.sa_handler = myFunc;
        sigemptyset(&act.sa_mask);

        sigaction(SIGCHLD, &act, nullptr);

        // 注册完信号捕捉之后解除阻塞
        sigprocmask(SIG_UNBLOCK, &set, nullptr);

        while (1) {
            printf("parent process pid : %d\n", getpid());
            sleep(2);
        }
    } else if (pid == 0) {
        // 子进程
        printf("child process pid : %d\n", getpid());
        // sleep(1);
    }

    return 0;
}

由于我们对子进程的设置,在运行中20个子进程结束的时间非常接近,waitpid(-1,…)是能识别所有的子进程,但是一次只能清理一个,这些子进程的SIGCHLD信号发送到未决信号集这里,当然未决信号集只能接受一个并且填入,然后交给阻塞信号集,其他的丢弃,所以我们需要while()循环来释放这些几乎同时结束的子进程;之所以设置非阻塞是因为可能个别子进程因为自己的原因,没有和上面的匹配,所以我们设置非阻塞,那个时候这个进程完了发送信号然后父进程去处理,这个时候的未决信号集肯定是写入(0)的,因为如果不可以写入,那必然这个进程就是和前面是一样的了

2.4 共享内存(效率最高)
概念

并不是完全没有内核介入,而是相比于其他通信的操作要少得多,因为没有经过内核和用户之间的切换操作或者说非常少,省去了这一大部分的时间,就是将数据从用户空间当中拷贝到内核当中的这一段时间,所以他的效率是最高的

第二章 Linux多进程开发:进程控制,进程通信和守护进程_第74张图片

使用步骤

创建共享内存,连接共享内存;分离,删除

第二章 Linux多进程开发:进程控制,进程通信和守护进程_第75张图片

相关函数

第二章 Linux多进程开发:进程控制,进程通信和守护进程_第76张图片

记得查man文档,太多太杂了!!!

	#include 

	int shmget(key_t key, size_t size, int shmflg);
// 作用:创建一个新的共享内存段或者获取一个既有的共享内存段的标识
//     新创建的内存段中的数据都会被初始化为0
// 参数:
//     key:key_t类型,是一个整形,通过这个找到或者创建一个共享内存
//         一般用16进制表示,并且是非0值,创建的时候可以随便给,给一个16进制的数或者10进制(会转化),找到的时候按照创建时候匹配就行
//     size:size_t类型,共享内存的大小,会自动调整为分页边界的整数倍(和内存映射是一样的)
//     shmflg:
//         共享内存的属性:用按位或连接
//             - 访问权限
//             - 附加属性(创建共享内存,判断共享内存是否存在,获取共享内存)

//                 创建:IPC_CREAT 加上 访问权限(比如0664)
//                 获取:IPC_CREAT(不加访问权限)
//                 判断:IPC_EXCL,需要和IPC_CREAT一起使用,用按位或连接
// 返回值:
//     成功 >0 返回共享内存引用的ID,后面操作共享内存使用这个标识
//     失败 -1.修改errno

	void *shmat(int shmid, const void *_Nullable shmaddr, int shmflg);
// 作用:和当前的进程进行关联
// 参数:
//     shmid:共享内存的标识,ID,由shmget()返回值获取
//     shmaddr:申请的共享内存的起始地址,指定为nullptr,让系统帮我们去分配
//     shmflg:
//         对共享内存的操作
//             - 读:SHM_RDONLY,而且必须要有读权限
//             - 读写:0,我们指定什么都不给,但是由于必须有读权限,系统会给我们加上读写的权限
// 返回值:
//     成功 返回共享内存的起始地址
//     失败 (void*)-1


	int shmdt(const void *shmaddr);
// 作用:解除当前进程和共享内存的关联
// 参数:
//     shmaddr:共享内存的首地址
// 返回值:
//     成功 0
//     失败 -1,修改errno

	int shmctl(int shmid, int cmd, struct shmid_ds *buf);
// 作用:对共享内存进行操作,比如可以删除,共享内存要删除才会消失;创建共享内存的进程被销毁了对这块共享内存没有任何影响,必须要手动删除才行
// 参数:
//     shmid:共享内存的id
//     cmd:要做的操作
//         IPC_STAT:获取共享内存当前的状态
//         IPC_SET:设置共享内存的状态
//         IPC_RMID:标记共享内存被销毁,之所以是标记是因为有很多个进程都连接了这个共享内存,我这一个进程并不能想删除就删除,而只是标记下来,当检测到连接数为0时,系统自会将这块共享内存删除
//     buf:需要设置或者获取的共享内存的属性信息
//         IPC_STAT:buf存储数据
//         IPC_SET:buf中需要初始化数据,设置到内核中
//         IPC_RMID:没有用,传递nullptr即可

    #include 

    key_t ftok(const char *pathname, int proj_id);
//作用:根据指定的路径名和int值,生成一个共享内存的key,我们可以不用自己指定
//参数:
    //pathname:指定一个存在的路径
    //proj_id:int类型的值,但是这系统调用只会使用其中的一个字节(8位)
        //返回:0-255,一般指定一个字符 'a'
示例

写两个程序进行通信

//write.cpp
#include 
#include 
using namespace std;
#include 
#include 
#define _size 1024

int main() {
    // 创建共享内存
    int shmid = shmget(100, 4096, 0664 | IPC_CREAT);
    if (-1 == shmid) {
        perror("shmget");
        return -1;
    }
    printf("shmid : %d\n", shmid);

    // 和当前进程进行关联
    void *ptr = shmat(shmid, nullptr, 0);
    if ((void *)-1 == ptr) {
        perror("shmat");
        return -1;
    }

    char str[_size] = {0};

    printf("请输入写入的字符串: ");
    fgets(str, sizeof(str), stdin);

    // 写数据
    memcpy(ptr, str, strlen(str) + 1);  // 为了保险,拷上字符串结束符

    printf("按任意键继续\n");
    getchar();

    // 解除关联
    int ret = shmdt(ptr);
    if (-1 == ret) {
        perror("shmdt");
        return -1;
    }

    // 删除共享内存
    shmctl(shmid, IPC_RMID, nullptr);

    return 0;
}
//read.cpp
#include 
#include 
using namespace std;
#include 
#include 

int main() {
    // 获得共享内存的标识,我们是用key标识的
    int shmid = shmget(100, 4096, IPC_CREAT);
    if (-1 == shmid) {
        perror("shmget");
        return -1;
    }
    printf("shmid : %d\n", shmid);

    // 绑定连接
    void* ptr = shmat(shmid, nullptr, 0);
    if ((void*)-1 == ptr) {
        perror("shmat");
        return -1;
    }

    // 读数据
    printf("data : %s", (char*)ptr);

    printf("按任意键继续\n");
    getchar();

    // 关闭关联
    int ret = shmdt(ptr);
    if (-1 == ret) {
        perror("shmdt");
        return -1;
    }

    // 标记删除
    shmctl(shmid, IPC_RMID, nullptr);

    return 0;
}

执行结果:

image-20230725102001298

image-20230725102008328

共享内存操作命令

注意,shmctl()执行的删除只是标记删除操作,执行到这一步后,这个共享内存的key修改为0,然后不再接受连接,其他程序再次执行shmctl()标记删除相当于什么也没做,但是可以执行不会报错,系统就监听其他进程对这个共享内存的解除连接操作,然后维护shm_nattach,记录关联数,当程序执行shmdt()手动解除或者程序结束的时候系统自动解除连接,当连接数为0之后系统就删除这块共享内存。之所以标记删除,是为了防止删除后还有其他进程在使用这块内存造成不必要的危险

第二章 Linux多进程开发:进程控制,进程通信和守护进程_第77张图片

注意

问题1:操作系统如何知道一块共享内存被多少个进程关联?

  • 共享内存维护了一个结构体 struct shmid_ds 这个结构体中有一个成员 shm_nattach
  • shm_nattach记录了关联的进程个数

问题2:可以不可以对共享内存多次删除 stmctl()

  • 可以,因为shmctl()只是标记删除共享内存,不是直接删除
  • 什么时候真正删除,当和共享内存关联的进程数为0的时候,就真正被删除
  • 当共享内存的key为0的时候,表示共享内存被标记删除,如果进程取消关联就不能继续操作这个共享内存,这种情况下也不能再次关联

问题3:共享内存和内存映射的区别

  • 共享内存可以直接创建,内存映射需要磁盘文件(匿名映射除外)
  • 共享内存效率更高
  • 内存:
    • 共享内存:所有的进程操作的是同一块共享内存
      • 内存映射:(父子进程除外)每个进程在自己的虚拟地址空间中有一个独立的内存
  • 数据安全
    • 进程突然退出,共享内存还存在,内存映射消失了
    • 运行进程的电脑死机了,数据存储在共享内存中就没有了,内存映射区的数据也没有了,但是他的数据已经同步给磁盘了
  • 生命周期
    • 内存映射区:进程退出,内存映射区销毁
    • 共享内存:进程退出,共享内存还在,标记删除(所有关联的进程数为0),或者关机
      如果进程退出,系统会自动和共享内存取消关联

3.守护进程

终端

第二章 Linux多进程开发:进程控制,进程通信和守护进程_第78张图片

进程组

第二章 Linux多进程开发:进程控制,进程通信和守护进程_第79张图片

会话

第二章 Linux多进程开发:进程控制,进程通信和守护进程_第80张图片

理解关系举例

第二章 Linux多进程开发:进程控制,进程通信和守护进程_第81张图片

操作函数

gid:进程组id;sid:会话的id

第二章 Linux多进程开发:进程控制,进程通信和守护进程_第82张图片
守护进程(Daemon进程,精灵进程)

后台服务进程,是一个生存期较长的进程,一般采用以d结尾的名字

第二章 Linux多进程开发:进程控制,进程通信和守护进程_第83张图片

创建步骤(!!!)

必须有的是的是前两步和最后一步

首先为什么要用子进程来创建会话,因为如果是父进程创建会话的话,一旦父进程是这个进程组的首进程,进程组号就是父进程的id,然后创建会话之后新会话中创建出来的进程组号也用的是这个,两个不同会话中存在同一个进程组号,这个显然是不可以的,所以我们用子进程创建,就避免了这个问题;然后父进程退出一是为了保证不出现僵尸进程(这是孤儿进程没有什么危险),而是避免子进程运行着时候父进程完了然后输出终端提示符,就是如下(突然冒出来很诡异)

image-20230725150200682

第二,为什么要创建一个新会话?因为如果不是新创建而是挪入其他的会话或者就用自身的会话,那么可能这个会话绑定了控制终端,能够接受信号处理信号这些,这显然与守护进程的初衷不符,所以我们要创建一个新会话,新会话默认是不绑定控制终端的,但是不代表没有终端,至少文件描述符012,标准输入输出错误是有的,言下之意就是可以向屏幕上输出数据,所以这就有了下面关闭文件描述符,然后重定向到 dev/null 的操作,当然这一步也不是必须的

第二章 Linux多进程开发:进程控制,进程通信和守护进程_第84张图片

示例

写一个守护进程,用来每两秒记录一次当前的时间并写到文本当中

就严格按照这几步来,创建子进程,子进程创建会话,设置umask(不必要),设置工作目录(不必要),关闭从父进程继承而来的文件描述符(不必要,这里没有),重定向文件描述符(不必要,这里有),核心业务逻辑(设置定时器,捕捉信号)

#include 
#include 
#include 
using namespace std;
#include 
#include 
#include 
#include 
#include 

// 写一个守护进程,每隔两秒获取系统时间,将这个时间写到磁盘文件中

void _deal(int num) {
    // 获取系统时间写入磁盘文件
    time_t _time = time(nullptr);
    // 将time()获得的距离计算机元年(1970-1-1 00:00:00)的秒数转化为当前的时间
    struct tm* _localtime = localtime(&_time);

    const char* str = asctime(_localtime);
    // 如果不存在则创建,存在则追加
    int ret = access("time.txt", F_OK);
    int fd = -1;
    if (-1 == ret)
        // 不存在
        fd = open("time.txt", O_RDWR | O_CREAT, 0664);
    else if (0 == ret)
        // 存在
        fd = open("time.txt", O_RDWR | O_APPEND);
    if (-1 == fd) {
        perror("open");
        exit(-1);
    }

    ret = write(fd, str, strlen(str));
    if (-1 == ret) {
        perror("write");
        exit(-1);
    }
}

int main() {
    // 创建子进程,退出父进程
    pid_t pid = fork();
    if (-1 == pid) {
        perror("fork");
        return -1;
    }

    if (pid > 0)
        // 父进程
        return 0;
    else if (0 == pid) {
        // 子进程

        // 如果存在time.txt,将其删除,准备工作
        int ret = access("time.txt", F_OK);
        if (0 == ret)
            unlink("time.txt");

        // 在子进程中重新创建一个会话,脱离原来的控制终端
        pid_t sid = setsid();
        if (-1 == pid) {
            perror("setsid");
            return -1;
        }

        // 设置umask
        umask(022);

        // 更改工作目录
        chdir("/mnt/d/Code/Cpp/深入学习/Linux方向/牛客网Linux网络课程/第2章-多进程开发/13");

        // 关闭,以及重定向文件描述符
        int fd = open("/dev/null", O_RDWR);
        dup2(fd, STDIN_FILENO);
        dup2(fd, STDOUT_FILENO);
        dup2(fd, STDERR_FILENO);

        // 业务逻辑

        // 注册信号捕捉器
        struct sigaction _act;
        _act.sa_flags = 0;
        _act.sa_handler = _deal;
        sigemptyset(&_act.sa_mask);
        sigaction(SIGALRM, &_act, nullptr);

        // 创建定时器
        itimerval _new;
        // 延迟时间
        _new.it_interval.tv_sec = 2;
        _new.it_interval.tv_usec = 0;
        // 周期时间
        _new.it_value.tv_sec = 2;
        _new.it_value.tv_usec = 0;

        ret = setitimer(ITIMER_REAL, &_new, nullptr);
        if (-1 == ret) {
            perror("setitimer");
            return -1;
        }

        // 不让进程结束,不然无法记录
        while (1)
            sleep(10);
    }

    return 0;
}

执行结果:

第二章 Linux多进程开发:进程控制,进程通信和守护进程_第85张图片

并且从文件大小不断变化可以看出是实时更新的

第二章 Linux多进程开发:进程控制,进程通信和守护进程_第86张图片

守护进程没有控制终端,所以没有办法接受控制终端发出的信号(例如SIGINT( ctrl+c )和SIGQUIT( ctrl+\ ) ),我们只能通过kill -9 强制杀死

第二章 Linux多进程开发:进程控制,进程通信和守护进程_第87张图片

你可能感兴趣的:(牛客Linux,linux,c++)