2.10~2.29 进程间通信(下)

简介: 2.10~2.29 进程间通信

三、内存映射


10 内存映射



11 内存映射相关系统调用



/*
    #include <sys/mman.h>
    void *mmap(void *addr, size_t length, int prot, int flags,int fd, off_t offset);
        - 功能:将一个文件或者设备的数据映射到内存中
        - 参数:
            - void *addr: NULL, 由内核指定
            - length : 要映射的数据的长度,这个值不能为0。建议使用文件的长度。
                    获取文件的长度:stat lseek
            - prot : 对申请的内存映射区的操作权限
                -PROT_EXEC :可执行的权限
                -PROT_READ :读权限
                -PROT_WRITE :写权限
                -PROT_NONE :没有权限
                要操作映射内存,必须要有读的权限。
                PROT_READ、PROT_READ|PROT_WRITE
            - flags :
                - MAP_SHARED : 映射区的数据会自动和磁盘文件进行同步,进程间通信,必须要设置这个选项
                - MAP_PRIVATE :不同步,内存映射区的数据改变了,对原来的文件不会修改,会重新创建一个新的文件。(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
    int munmap(void *addr, size_t length);
        - 功能:释放内存映射
        - 参数:
            - addr : 要释放的内存的首地址
            - length : 要释放的内存的大小,要和mmap函数中的length参数的值一样。
*/
/*
    使用内存映射实现进程间通信:
    1.有关系的进程(父子进程)
        - 还没有子进程的时候
            - 通过唯一的父进程,先创建内存映射区
        - 有了内存映射区以后,创建子进程
        - 父子进程共享创建的内存映射区
    2.没有关系的进程间通信
        - 准备一个大小不是0的磁盘文件
        - 进程1 通过磁盘文件创建内存映射区
            - 得到一个操作这块内存的指针
        - 进程2 通过磁盘文件创建内存映射区
            - 得到一个操作这块内存的指针
        - 使用内存映射区通信
    注意:内存映射区通信,是非阻塞。
*/
#include <stdio.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <sys/types.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <wait.h>
// 作业:使用内存映射实现没有关系的进程间的通信。
int main() {
    // 1.打开一个文件
    int fd = open("test.txt", O_RDWR);
    int size = lseek(fd, 0, SEEK_END);  // 获取文件的大小
    // 2.创建内存映射区
    void *ptr = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    if(ptr == MAP_FAILED) {
        perror("mmap");
        exit(0);
    }
    // 3.创建子进程
    pid_t pid = fork();
    if(pid > 0) {    //父进程读数据
        wait(NULL);  //回收子进程
        char buf[64];
        strcpy(buf, (char *)ptr);
        printf("read data : %s\n", buf);
    }
    else if(pid == 0) {  // 子进程写数据
        strcpy((char *)ptr, "nihao a, son!!!");
    }
    // 关闭内存映射区
    munmap(ptr, size);
    return 0;
}



内存映射完成文件拷贝


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
越界操作操作的是非法的内存 -> 段错误


// 使用内存映射实现文件拷贝的功能
/*
    思路:
        1.对原始的文件进行内存映射
        2.创建一个新文件(拓展该文件)
        3.把新文件的数据映射到内存中
        4.通过内存拷贝将第一个文件的内存数据拷贝到新的文件内存中
        5.释放资源
*/
#include <stdio.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
int main() {
    // 1.对原始的文件进行内存映射
    int fd = open("english.txt", O_RDWR);
    if(fd == -1) {
        perror("open");
        exit(0);
    }
    // 获取原始文件的大小
    int len = lseek(fd, 0, SEEK_END);
    // 2.创建一个新文件(拓展该文件)
    int fd1 = open("cpy.txt", O_RDWR | O_CREAT, 0664);
    if(fd1 == -1) {
        perror("open");
        exit(0);
    }
    // 对新创建的文件进行拓展
    truncate("cpy.txt", len);
    write(fd1, " ", 1);
    // 3.分别做内存映射
    void * ptr = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    void * ptr1 = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED, fd1, 0);
    if(ptr == MAP_FAILED) {
        perror("mmap");
        exit(0);
    }
    if(ptr1 == MAP_FAILED) {
        perror("mmap");
        exit(0);
    }
    // 内存拷贝
    memcpy(ptr1, ptr, len); //第一个参数是往哪拷贝  第二个参数是从哪拷贝
    // 释放资源
    munmap(ptr1, len);
    munmap(ptr, len);
    close(fd1);
    close(fd);
    return 0;
}
//整体实现的功能是从copy.txt拷贝到cpy.txt


匿名映射


/*
    匿名映射:不需要文件实体进程一个内存映射
    可以用来父子间文件通信
*/
#include <stdio.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <sys/wait.h>
int main() {
    // 1.创建匿名内存映射区
    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(0);
    }
    // 父子进程间通信
    pid_t pid = fork();
    if(pid > 0) {
        // 父进程
        strcpy((char *) ptr, "hello, world");
        wait(NULL);
    }else if(pid == 0) {
        // 子进程
        sleep(1);  //先写再读
        printf("%s\n", (char *)ptr);
    }
    // 释放内存映射区
    int ret = munmap(ptr, len);
    if(ret == -1) {
        perror("munmap");
        exit(0);
    }
    return 0;
}


四、信号


01 信号的概念





02 linux信号





03 信号的五种默认处理动作



04 信号相关的函数



/*  
    #include <sys/types.h>
    #include <signal.h>
    int kill(pid_t pid, int sig);
        - 功能:给任何的进程或者进程组pid, 发送任何的信号 sig
        - 参数:
            - pid :
                > 0 : 将信号发送给指定的进程
                = 0 : 将信号发送给当前的进程组
                = -1 : 将信号发送给每一个有权限接收这个信号的进程
                < -1 : 这个pid=某个进程组的ID取反 (-12345)
            - sig : 需要发送的信号的编号或者是宏值,0表示不发送任何信号
        kill(getppid(), 9);
        kill(getpid(), 9);
    int raise(int sig);
        - 功能:给当前进程发送信号
        - 参数:
            - sig : 要发送的信号
        - 返回值:
            - 成功 0
            - 失败 非0
        kill(getpid(), sig);   
    void abort(void);
        - 功能: 发送SIGABRT信号给当前的进程,杀死当前进程
        kill(getpid(), SIGABRT);
*/
#include <stdio.h>
#include <sys/types.h>
#include <signal.h>
#include <unistd.h>
int main() {
    pid_t pid = fork();
    if(pid == 0) {
        // 子进程
        int i = 0;
        for(i = 0; i < 5; i++) {
            printf("child process\n");   //子进程每隔一秒打印一句话
            sleep(1);
        }
    } else if(pid > 0) {
        // 父进程
        printf("parent process\n");
        sleep(2);
        printf("kill child process now\n");  //父进程各两秒杀死子进程
        kill(pid, SIGINT);  //杀死子进程
    }
    return 0;
}
//多进程抢夺CPU资源,不能确定父子谁先执行



alarm


/*
    #include <unistd.h>
    unsigned int alarm(unsigned int seconds);
        - 功能:设置定时器(闹钟)。函数调用,开始倒计时,当倒计时为0的时候,
                函数会给当前的进程发送一个信号:SIGALARM
        - 参数:
            seconds: 倒计时的时长,单位:秒。如果参数为0,定时器无效(不进行倒计时,不发信号)。
                    取消一个定时器,通过alarm(0)。
        - 返回值:
            - 之前没有定时器,返回0
            - 之前有定时器,返回之前的定时器剩余的时间
    - SIGALARM :默认终止当前的进程,每一个进程都有且只有唯一的一个定时器。
        alarm(10);  -> 返回0
        过了1秒
        alarm(5);   -> 返回9
    alarm(100) -> 该函数是不阻塞的
*/
#include <stdio.h>
#include <unistd.h>
int main() {
    int seconds = alarm(5);   //5秒终止程序
    printf("seconds = %d\n", seconds);  // 0
    sleep(1);
    seconds = alarm(1);    // 不阻塞
    printf("seconds = %d\n", seconds);  // 3
    while(1) {
    }
    return 0;
}


// 1秒钟电脑能数多少个数?
#include <stdio.h>
#include <unistd.h>
/*
    实际的时间 = 内核时间 + 用户时间 + 消耗的时间
    进行文件IO操作的时候比较浪费时间
    定时器,与进程的状态无关(自然定时法)。无论进程处于什么状态,alarm都会计时。
*/
int main() {    
    alarm(1);   //一秒终止
    int i = 0;
    while(1) {
        printf("%i\n", i++);
    }
    return 0;
}


alarm返回值为还剩余多少时间


多次调用alarm只会有一个生效


下一个替代上一个


setitimer


/*
    #include <sys/time.h>
    int setitimer(int which, const struct itimerval *new_value,
                        struct itimerval *old_value);
        - 功能:设置定时器(闹钟)。可以替代alarm函数。精度微妙us,可以实现周期性定时
        - 参数:
            - which : 定时器以什么时间计时
              ITIMER_REAL: 真实时间,时间到达,发送 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;    //  微秒    
                };
            过10秒后,每个2秒定时一次
            - old_value :记录上一次的定时的时间参数,一般不使用,指定NULL
        - 返回值:
            成功 0
            失败 -1 并设置错误号
*/
#include <sys/time.h>
#include <stdio.h>
#include <stdlib.h>
// 过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;
}


05 信号捕捉函数



signal


/*
    #include <signal.h>
    typedef void (*sighandler_t)(int);
    sighandler_t signal(int signum, sighandler_t handler);
        - 功能:设置某个信号的捕捉行为
        - 参数:
            - signum: 要捕捉的信号
            - handler: 捕捉到信号要如何处理
                - SIG_IGN : 忽略信号
                - SIG_DFL : 使用信号默认的行为
                - 回调函数 :  这个函数是内核调用,程序员只负责写,捕捉到信号后如何去处理信号。
                回调函数:
                    - 需要程序员实现,提前准备好的,函数的类型根据实际需求,看函数指针的定义
                    - 不是程序员调用,而是当信号产生,由内核调用
                    - 函数指针是实现回调的手段,函数实现之后,将函数名放到函数指针的位置就可以了。
        - 返回值:
            成功,返回上一次注册的信号处理函数的地址。第一次调用返回NULL
            失败,返回SIG_ERR,设置错误号
    SIGKILL SIGSTOP不能被捕捉,不能被忽略。
*/
#include <sys/time.h>
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
void myalarm(int num) {
    printf("捕捉到了信号的编号是:%d\n", num);
    printf("xxxxxxx\n");
}
// 过3秒以后,每隔2秒钟定时一次
int main() {
    // 注册信号捕捉
    // signal(SIGALRM, SIG_IGN);
    // signal(SIGALRM, SIG_DFL);
    // void (*sighandler_t)(int); 函数指针,int类型的参数表示捕捉到的信号的值。
    signal(SIGALRM, myalarm);
    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;
}



sigaction


/*
    #include <signal.h>
    int sigaction(int signum, const struct sigaction *act,
                            struct sigaction *oldact);
        - 功能:检查或者改变信号的处理。信号捕捉
        - 参数:
            - signum : 需要捕捉的信号的编号或者宏值(信号的名称)
            - act :捕捉到信号之后的处理动作
            - oldact : 上一次对信号捕捉相关的设置,一般不使用,传递NULL
        - 返回值:
            成功 0
            失败 -1
     struct sigaction {
        // 函数指针,指向的函数就是信号捕捉到之后的处理函数
        void     (*sa_handler)(int);
        // 不常用
        void     (*sa_sigaction)(int, siginfo_t *, void *);
        // 临时阻塞信号集,在信号捕捉函数执行过程中,临时阻塞某些信号。
        sigset_t   sa_mask;
        // 使用哪一个信号处理对捕捉到的信号进行处理
        // 这个值可以是0,表示使用sa_handler,也可以是SA_SIGINFO表示使用sa_sigaction
        int        sa_flags;
        // 被废弃掉了
        void     (*sa_restorer)(void);
    };
*/
#include <sys/time.h>
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
void myalarm(int num) {
    printf("捕捉到了信号的编号是:%d\n", num);
    printf("xxxxxxx\n");
}
// 过3秒以后,每隔2秒钟定时一次
int main() {
    //给结构体初始化
    struct sigaction act;   //sigaction是结构体
    act.sa_flags = 0;
    act.sa_handler = myalarm;
    sigemptyset(&act.sa_mask);  // 清空临时阻塞信号集
    // 注册信号捕捉
    sigaction(SIGALRM, &act, NULL);
    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();
    while(1);
    return 0;
}


06 信号集



07 阻塞信号集和未决信号集



08 信号集相关的函数



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


/*
    以下信号集相关的函数都是对自定义的信号集进行操作。
    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 : 失败
*/
#include <signal.h>
#include <stdio.h>
int main() {
    // 创建一个信号集
    sigset_t set;
    // 清空信号集的内容
    sigemptyset(&set);
    // 判断 SIGINT 是否在信号集 set 里
    int ret = sigismember(&set, SIGINT);
    if(ret == 0) {
        printf("SIGINT 不阻塞\n");  //..
    } else if(ret == 1) {
        printf("SIGINT 阻塞\n");
    }
    // 添加几个信号到信号集中
    sigaddset(&set, SIGINT);
    sigaddset(&set, SIGQUIT);
    // 判断SIGINT是否在信号集中
    ret = sigismember(&set, SIGINT);
    if(ret == 0) {
        printf("SIGINT 不阻塞\n");
    } else if(ret == 1) {
        printf("SIGINT 阻塞\n");  //..
    }
    // 判断SIGQUIT是否在信号集中
    ret = sigismember(&set, SIGQUIT);
    if(ret == 0) {
        printf("SIGQUIT 不阻塞\n");
    } else if(ret == 1) {
        printf("SIGQUIT 阻塞\n");  //..
    }
    // 从信号集中删除一个信号
    sigdelset(&set, SIGQUIT);
    // 判断SIGQUIT是否在信号集中
    ret = sigismember(&set, SIGQUIT);
    if(ret == 0) {
        printf("SIGQUIT 不阻塞\n");  //..
    } else if(ret == 1) {
        printf("SIGQUIT 阻塞\n");
    }
    return 0;
}



上面改变的是自定义的信号集


sigprocmask


/*
    int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
        - 功能:将自定义信号集中的数据设置到内核中(设置阻塞,解除阻塞,替换)
        - 参数:
            - how : 如何对内核阻塞信号集进行处理
                SIG_BLOCK: 将用户设置的阻塞信号集添加到内核中,内核中原来的数据不变
                    假设内核中默认的阻塞信号集是mask, mask | set
                SIG_UNBLOCK: 根据用户设置的数据,对内核中的数据进行解除阻塞
                    mask &= ~set
                SIG_SETMASK:覆盖内核中原来的值
            - set :已经初始化好的用户自定义的信号集
            - oldset : 保存设置之前的内核中的阻塞信号集的状态,可以是 NULL
        - 返回值:
            成功:0
            失败:-1
                设置错误号:EFAULT、EINVAL
    int sigpending(sigset_t *set);
        - 功能:获取内核中的未决信号集
        - 参数:set,传出参数,保存的是内核中的未决信号集中的信息。
*/
// 编写一个程序,把所有的常规信号(1-31)的未决状态打印到屏幕
// 设置某些信号是阻塞的,通过键盘产生这些信号
#include <stdio.h>
#include <signal.h>
#include <stdlib.h>
#include <unistd.h>
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);  //1秒打印一次内核中前31个状态
        if(num == 10) {
            // 解除阻塞
            sigprocmask(SIG_UNBLOCK, &set, NULL);
        }
    }
    return 0;
}


09 内核实现信号捕捉的过程



10 sigchld信号



子进程给父进程发送信号


/*
    SIGCHLD信号产生的3个条件:
        1.子进程结束
        2.子进程暂停了
        3.子进程继续运行
        都会给父进程发送该信号,父进程默认忽略该信号。
    使用SIGCHLD信号解决僵尸进程的问题。
*/
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <signal.h>
#include <sys/wait.h>
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;
}


五、共享内存


01 共享内存



效率最高,无需要内核介入


02 共享内存使用步骤



共享内存相关的函数
#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);
    - 功能:创建一个新的共享内存段,或者获取一个既有的共享内存段的标识。
        新创建的内存段中的数据都会被初始化为0
    - 参数:
        - key : key_t类型是一个整形,通过这个找到或者创建一个共享内存。
                一般使用16进制表示,非0值
        - size: 共享内存的大小
        - shmflg: 属性
            - 访问权限
            - 附加属性:创建/判断共享内存是不是存在
                - 创建:IPC_CREAT
                - 判断共享内存是否存在: IPC_EXCL , 需要和IPC_CREAT一起使用
                    IPC_CREAT | IPC_EXCL | 0664
        - 返回值:
            失败:-1 并设置错误号
            成功:>0 返回共享内存的引用的ID,后面操作共享内存都是通过这个值。
void *shmat(int shmid, const void *shmaddr, int shmflg);
    - 功能:和当前的进程进行关联
    - 参数:
        - shmid : 共享内存的标识(ID),由shmget返回值获取
        - shmaddr: 申请的共享内存的起始地址,指定NULL,内核指定
        - shmflg : 对共享内存的操作
            - 读 : SHM_RDONLY, 必须要有读权限
            - 读写: 0
    - 返回值:
        成功:返回共享内存的首(起始)地址。  失败(void *) -1
int shmdt(const void *shmaddr);
    - 功能:解除当前进程和共享内存的关联
    - 参数:
        shmaddr:共享内存的首地址
    - 返回值:成功 0, 失败 -1
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
    - 功能:对共享内存进行操作。删除共享内存,共享内存要删除才会消失,创建共享内存的进行被销毁了对共享内存是没有任何影响。
    - 参数:
        - shmid: 共享内存的ID
        - cmd : 要做的操作
            - IPC_STAT : 获取共享内存的当前的状态
            - IPC_SET : 设置共享内存的状态
            - IPC_RMID: 标记共享内存被销毁
        - buf:需要设置或者获取的共享内存的属性信息
            - IPC_STAT : buf存储数据
            - IPC_SET : buf中需要初始化数据,设置到内核中
            - IPC_RMID : 没有用,NULL
key_t ftok(const char *pathname, int proj_id);
    - 功能:根据指定的路径名,和int值,生成一个共享内存的key
    - 参数:
        - pathname:指定一个存在的路径
            /home/nowcoder/Linux/a.txt
            / 
        - proj_id: int类型的值,但是这系统调用只会使用其中的1个字节
                   范围 : 0-255  一般指定一个字符 'a'
问题1:操作系统如何知道一块共享内存被多少个进程关联?
    - 共享内存维护了一个结构体struct shmid_ds 这个结构体中有一个成员 shm_nattch
    - shm_nattach 记录了关联的进程个数
问题2:可不可以对共享内存进行多次删除 shmctl
    - 可以的
    - 因为shmctl 标记删除共享内存,不是直接删除
    - 什么时候真正删除呢?
        当和共享内存关联的进程数为0的时候,就真正被删除
    - 当共享内存的key为0的时候,表示共享内存被标记删除了
        如果一个进程和共享内存取消关联,那么这个进程就不能继续操作这个共享内存。也不能进行关联。
    共享内存和内存映射的区别
    1.共享内存可以直接创建,内存映射需要磁盘文件(匿名映射除外)
    2.共享内存效果更高
    3.内存
        所有的进程操作的是同一块共享内存。
        内存映射,每个进程在自己的虚拟地址空间中有一个独立的内存。
    4.数据安全
        - 进程突然退出
            共享内存还存在
            内存映射区消失
        - 运行进程的电脑死机,宕机了
            数据存在在共享内存中,没有了
            内存映射区的数据 ,由于磁盘文件中的数据还在,所以内存映射区的数据还存在。
    5.生命周期
        - 内存映射区:进程退出,内存映射区销毁
        - 共享内存:进程退出,共享内存还在,标记删除(所有的关联的进程数为0),或者关机
            如果一个进程退出,会自动和共享内存进行取消关联。


03 共享内存操作函数



write_shm.c


#include <stdio.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <string.h>
int main() {    
    // 1.创建一个共享内存
    int shmid = shmget(100, 4096, IPC_CREAT|0664);  
    printf("shmid : %d\n", shmid);
    // 2.和当前进程进行关联
    void * ptr = shmat(shmid, NULL, 0);   //返回共享内存的首地址
    char * str = "helloworld";
    // 3.写数据
    memcpy(ptr, str, strlen(str) + 1);
    printf("按任意键继续\n");
    getchar();
    // 4.解除关联
    shmdt(ptr);
    // 5.删除共享内存
    shmctl(shmid, IPC_RMID, NULL);
    return 0;
}


read_shm.c


#include <stdio.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <string.h>
int main() {    
    // 1.获取一个共享内存
    int shmid = shmget(100, 0, IPC_CREAT);  //第一个参数要对上  第二个参数要小于4096
    printf("shmid : %d\n", shmid);
    // 2.和当前进程进行关联
    void * ptr = shmat(shmid, NULL, 0);
    // 3.读数据
    printf("%s\n", (char *)ptr);
    printf("按任意键继续\n");
    getchar();
    // 4.解除关联
    shmdt(ptr);
    // 5.删除共享内存
    shmctl(shmid, IPC_RMID, NULL);
    return 0;
}


要打开两个终端实现,一个写数据,一个读数据


04 共享内存操作命令


目录
相关文章
|
7月前
|
消息中间件 存储 Unix
进程间通信和线程间通信总结
写在前面 面试的时候一定不要疲劳战,比如上午面了一个,然后中午不休息直接赶到另外一个相距比较远的公司,影响状态。 面试的时候一定不要紧张,不管对方有几个人,总之面试的时候做好充分准备,休息好,放松心态。 好了,言归正传,开始总结。
67 0
|
5月前
|
消息中间件 负载均衡 安全
进程间通信
数据传输:一个进程需要将它的数据发送给另一个进程资源共享:多个进程之间共享同样的资源。通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。进程间通信的本质:让不同的进程先看到同一份资源,但是这份资源不能由双方进程提供,而通常是由操作系统所提供!
39 9
进程间通信
|
存储 消息中间件 Linux
IPC(进程间通信)(上)
IPC(进程间通信)
76 0
|
API
IPC(进程间通信)(下)
IPC(进程间通信)(下)
57 0
|
Unix
进程间通信(一)
进程间通信
71 0
|
消息中间件
进程间通信(非常实用)
进程间通信(非常实用)
|
Linux
2.10~2.29 进程间通信(上)
2.10~2.29 进程间通信
90 0
2.10~2.29 进程间通信(上)
|
开发者
进程间通信 | 学习笔记
快速学习进程间通信,介绍了进程间通信系统机制, 以及在实际应用过程中如何使用。
进程间通信——共享内存
进程间通信——共享内存
217 0
进程间通信——共享内存