Linux 高并发服务器实战 - 2 Linux多进程开发

Linux 高并发服务器实战 - 2 Linux多进程开发

进程概述

概念1:
在这里插入图片描述

概念2:
在这里插入图片描述

在这里插入图片描述

微观而言,单CPU任意时刻只能运行一个程序

在这里插入图片描述

在这里插入图片描述

并发:两个队列交替使用一台咖啡机

并行:两个队列同时使用两台咖啡机

在这里插入图片描述

记录在PCB里:

在这里插入图片描述

进程状态转换

在这里插入图片描述

三态模型、五态模型

在这里插入图片描述

新建态 等待分配好资源才到就绪

在这里插入图片描述

在这里插入图片描述

PID 进程ID

PPID 父进程ID

PGID 进程组的ID

SID 会话ID

TTY 终端

STAT 状态

在这里插入图片描述

在这里插入图片描述

普通kill 无法自己杀死自己的终端(如果 kill -9 就可以 它强制杀死进程,这里的-9是信号 -SIGKILL)

运行程序时加 & 就可以在后台运行

./a.out &

还是可以输入shell的,可以再杀死

在这里插入图片描述

我们运行的进程的父进程一般都是当前终端这个进程

进程创建

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

fork 用于 创建子进程。一次是在父进程中 ,一次是在子进程中

fork 会执行两次、父进程子进程的for循环交替执行

for循环是父子进程都共享的代码, 从创建出子进程那开始,都可以执行

父进程和子进程会执行的代码

在这里插入图片描述

fork() 之后 clone 一个新的虚拟地址空间

子进程父进程的用户区数据是一样的

内核区里的进程的pid是不一样的

在各自的栈空间内 pidnum 的值是不一样的 一个是有值的 > 0 另一个是 = 0 的

注意子进程和父进程运行在不同的虚拟地址空间,fork之后clone了一个新的虚拟地址空间,它之前的用户区里的变量都复制过来了,而且是和父进程的虚拟地址空间是相互独立的,不会因为改了父(子)的子(父)的就变化。

在这里插入图片描述

直接复制一个虚拟地址空间出来其实是低效率的

实际在使用中不是复制一个一样的出来,而是

不是一fork的时候就立马拷贝出来,而是一开始读的时候是共享的

读时共享,写时才复制拷贝,并且映射到新的物理内存空间

/*
    #include <sys/types.h>
    #include <unistd.h>

    pid_t fork(void);
        函数的作用:用于创建子进程。
        返回值:
            fork()的返回值会返回两次。一次是在父进程中,一次是在子进程中。
            在父进程中返回创建的子进程的ID,
            在子进程中返回0
            如何区分父进程和子进程:通过fork的返回值。
            在父进程中返回-1,表示创建子进程失败,并且设置errno

        父子进程之间的关系:
        区别:
            1.fork()函数的返回值不同
                父进程中: >0 返回的子进程的ID
                子进程中: =0
            2.pcb中的一些数据
                当前的进程的id pid
                当前的进程的父进程的id ppid
                信号集

        共同点:
            某些状态下:子进程刚被创建出来,还没有执行任何的写数据的操作
                - 用户区的数据
                - 文件描述符表
        
        父子进程对变量是不是共享的?
            - 刚开始的时候,是一样的,共享的。如果修改了数据,不共享了。
            - 读时共享(子进程被创建,两个进程没有做任何的写的操作),写时拷贝。
        
*/

#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>

int main() {

    int num = 10;

    // 创建子进程
    pid_t pid = fork();

    // 判断是父进程还是子进程
    if(pid > 0) {
        // printf("pid : %d\n", pid);
        // 如果大于0,返回的是创建的子进程的进程号,当前是父进程
        printf("i am parent process, pid : %d, ppid : %d\n", getpid(), getppid());

        printf("parent num : %d\n", num);
        num += 10;
        printf("parent num += 10 : %d\n", num);


    } else if(pid == 0) {
        // 当前是子进程
        printf("i am child process, pid : %d, ppid : %d\n", getpid(),getppid());
       
        printf("child num : %d\n", num);
        num += 100;
        printf("child num += 100 : %d\n", num);
    }

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

    return 0;
}

/*
实际上,更准确来说,Linux 的 fork() 使用是通过写时拷贝 (copy- on-write) 实现。
写时拷贝是一种可以推迟甚至避免拷贝数据的技术。
内核此时并不复制整个进程的地址空间,而是让父子进程共享同一个地址空间。
只用在需要写入的时候才会复制地址空间,从而使各个进行拥有各自的地址空间。
也就是说,资源的复制是在需要写入的时候才会进行,在此之前,只有以只读方式共享。
注意:fork之后父子进程共享文件,
fork产生的子进程与父进程相同的文件文件描述符指向相同的文件表,引用计数增加,共享文件偏移指针。
*/

父子进程关系及GDB多进程调试

父子进程之间的关系:

​ 区别:

  1. fork() 函数的返回值不同

    ​ 父进程中:>0 返回子进程的ID

    ​ 子进程中:=0

    1. pcb中的一些数据

    ​ 当前进程的pid

    ​ 当前进程的父进程的id ppid

    ​ 信号集合

共同点:

​ 某些状态下,子进程刚被创建出来,还没有执行任何的写操作

​ 子进程关掉它的文件描述符,父进程是不受影响的

​ 父子进程对变量是不是共享的?

​ - 刚开始的时候,是一样的,共享的。如果修改了数据,不共享了。

​ - 读时共享(子进程被创建,两个进程没有做任何的写的操作),写时拷贝。

在这里插入图片描述

show follow-fork-mode 显示是调试父进程还是子进程 默认是父进程

可以set 设置默认跟踪哪个进程

show detach-on-fork 子进程是否脱离调试 可以set detach-on-fork 为 off 子进程就停留在fork那里

就只有父进程在调试

info inferiors 查看调试的进程 通过inferior id 切换调试的进程

exec函数族

C语言里没有函数重载 就是一系列功能相似的函数

在这里插入图片描述

调用进程的内部执行一个可执行文件,并替代掉了进程的内容

这是不合理的,一般来说我们是先fork出一个子进程,再替代子进程里的内容

在这里插入图片描述

把原先用户区替换成了 a.out对应的用户区、代码段 数据区什么的都变了

内核区没变、进程ID、父进程ID 都没变 但是灵魂变了

在这里插入图片描述

前6个是标准C库的函数,最后一个是linux系统的API,他们都是在它基础上封装的

用的比较多的:

​ execl、execlp

execl

execl调用失败返回-1,调用成功的话是没有返回值的,因为已经被替代了

/*  
    #include <unistd.h>
    int execl(const char *path, const char *arg, ...);
        - 参数:
            - path:需要指定的执行的文件的路径或者名称
                a.out /home/nowcoder/a.out 推荐使用绝对路径
                ./a.out hello world

            - arg:是执行可执行文件所需要的参数列表
                第一个参数一般没有什么作用,为了方便,一般写的是执行的程序的名称
                从第二个参数开始往后,就是程序执行所需要的的参数列表。
                参数最后需要以NULL结束(哨兵)

        - 返回值:
            只有当调用失败,才会有返回值,返回-1,并且设置errno
            如果调用成功,没有返回值。

*/
#include <unistd.h>
#include <stdio.h>

int main() {


    // 创建一个子进程,在子进程中执行exec函数族中的函数
    pid_t pid = fork();

    if(pid > 0) {
        // 父进程
        printf("i am parent process, pid : %d\n",getpid());
        sleep(1);
    }else if(pid == 0) {
        // 子进程
        execl("hello","hello",NULL);

        // 也可以execl shell命令
        // execl("/bin/ps", "ps", "aux", NULL);
        // perror("execl");
        // printf("i am child process, pid : %d\n", getpid());

    }

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


    return 0;
}

在这里插入图片描述

execlp

会到环境变量中查找指定的可执行文件,如果找到了就执行,找不到就执行不成功。

对于execl 中,如果 execl(“ps”, “ps”, “aux”, NULL); 相对路径是找不到这个ps的,绝对路径也没有,不会执行

如果execlp 执行 hello 是会报错的,因为环境变量里没这个

/*  
    #include <unistd.h>
    int execlp(const char *file, const char *arg, ... );
        - 会到环境变量中查找指定的可执行文件,如果找到了就执行,找不到就执行不成功。
        - 参数:
            - file:需要执行的可执行文件的文件名
                a.out
                ps

            - arg:是执行可执行文件所需要的参数列表
                第一个参数一般没有什么作用,为了方便,一般写的是执行的程序的名称
                从第二个参数开始往后,就是程序执行所需要的的参数列表。
                参数最后需要以NULL结束(哨兵)

        - 返回值:
            只有当调用失败,才会有返回值,返回-1,并且设置errno
            如果调用成功,没有返回值。


        int execv(const char *path, char *const argv[]);
        argv是需要的参数的一个字符串数组
        char * argv[] = {"ps", "aux", NULL};
        execv("/bin/ps", argv);

        int execve(const char *filename, char *const argv[], char *const envp[]);
        char * envp[] = {"/home/nowcoder", "/home/bbb", "/home/aaa"};


*/
#include <unistd.h>
#include <stdio.h>

int main() {


    // 创建一个子进程,在子进程中执行exec函数族中的函数
    pid_t pid = fork();

    if(pid > 0) {
        // 父进程
        printf("i am parent process, pid : %d\n",getpid());
        sleep(1);
    }else if(pid == 0) {
        // 子进程
        execlp("ps", "ps", "aux", NULL);

        printf("i am child process, pid : %d\n", getpid());

    }

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


    return 0;
}

execv

int execv(const char *path, char *const argv[]);
//argv是需要的参数的一个字符串数组
char * argv[] = {"ps", "aux", NULL};
//execv 是用字符串数组放进函数的
execv("/bin/ps", argv);

p -> 按PATH环境变量下的来执行

e -> 存有环境变量字符串地址的指针数组的地址

进程退出、孤儿进程、僵尸进程

在这里插入图片描述

exit 是标准C 库的函数

_exit 是linux 的函数

main 函数是由系统调用的

exit(NULL) 执行成功了的话,return 就不会执行了

/*
    #include <stdlib.h>
    void exit(int status);

    #include <unistd.h>
    void _exit(int status);

    status参数:是进程退出时的一个状态信息。父进程回收子进程资源的时候可以获取到。
*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main() {

    printf("hello\n");
    printf("world");

    // exit(0);
    _exit(0);
    
    return 0;
}

_exit() 没有刷新IO缓冲 exit() 刷新了

刷新IO缓冲:把数据刷到控制台

进程出现错误了,会调用exit() 退出进程

孤儿进程

  • 父进程运行结束,子进程还在运行(未运行结束),这样的子进程就成为孤儿进程

  • 每当出现一个孤儿进程的时候,内核就把孤儿进程的父进程设置为init(pid为1的进程,系统的第一个进程),而init进程会循环地 wait() 它的已经退出的子进程。当一个孤儿进程凄凉地结束了 其生命周期的时候,init进程就会善后

  • 因此孤儿进程并没有什么危害

让子进程进来之后先睡一秒钟,父进程早已经运行完了

int main() {

    // 创建子进程
    pid_t pid = fork();

    // 判断是父进程还是子进程
    if(pid > 0) {

        printf("i am parent process, pid : %d, ppid : %d\n", getpid(), getppid());

    } else if(pid == 0) {
        sleep(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;
}

在这里插入图片描述

子进程的父进程已经不是566 而是1

为什么子进程输出前面还有一个终端?

当前orphan 的父进程是当前终端326,当它的子进程开始运行时,它会切换到后台。子进程运行完毕后,会切换回前台。但是它没想到子进程还有一个子进程,所以在切换到前台后 还有输出

为什么子进程输出的也是在当前终端?

因为fork出的子进程和父进程读时共享统一虚拟地址空间,他们linux内核里文件描述符前三个是一样的,因此也是当前终端

僵尸进程

  • 每个进程结束之后 , 都会释放自己地址空间中的用户区数据,内核区的 PCB 没有办法自己释放掉,需要父进程去释放。

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

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

  • 这样就会导致一个问题,如果父进程不调用 wait() 或 waitpid() 的话,那么保留的那段信息就不会释放,其进程号就会一直被占用,但是系统所能使用的进程号是有限的,如果大量的产生僵尸进程,将因为没有可用的进程号而导致系统不能产生新的进程,此即为僵尸进程的危害,应当避免。

子进程都死了,父进程一直没有回收资源

用户区的数据虽然自己释放了,但是内核区的没释放,进程资源占用

// 判断是父进程还是子进程
    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());
       
    }

父进程一直循环

wait 函数

在这里插入图片描述

wait 是阻塞函数,如果调用了wait 子进程还没结束,那么就阻塞在这,直到子进程调用完毕

注意关于子进程的创建

fork();
fork();

这是一共有几个进程?4个!自己本身一个,第一个fork出来一个子,自己本身往下fork又出来一个。子进程往下fork又出来一个

所以如果想要创建5个子进程

for(int i = 0; i < 5; i++)
{
    pid = fork();
    if(pid == 0)
    {
        //说明它是我们产生的子进程,不需要它自己再产生子进程
        break;
    }
}

wait 一次只能回收一个,且会阻塞

不知道子进程退出状态时,可以wait(NULL)

可以建立一个int 变量,把那个变量的地址放到wait函数里,函数调用完之后,变量里的值被修改,再去判断它是怎么退出的

在这里插入图片描述

/*
    #include <sys/types.h>
    #include <sys/wait.h>
    pid_t wait(int *wstatus);
        功能:等待任意一个子进程结束,如果任意一个子进程结束了,次函数会回收子进程的资源。
        参数:int *wstatus
            进程退出时的状态信息,传入的是一个int类型的地址,传出参数。
        返回值:
            - 成功:返回被回收的子进程的id
            - 失败:-1 (所有的子进程都结束,调用函数失败)

    调用wait函数的进程会被挂起(阻塞),直到它的一个子进程退出或者收到一个不能被忽略的信号时才被唤醒(相当于继续往下执行)
    如果没有子进程了,函数立刻返回,返回-1;如果子进程都已经结束了,也会立即返回,返回-1.

*/
#include <sys/types.h>
#include <sys/wait.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>


int main() {

    // 有一个父进程,创建5个子进程(兄弟)
    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是正常退出
        exit(0);
    }

    return 0; // exit(0)
}

waitp函数

/*
    #include <sys/types.h>
    #include <sys/wait.h>
    pid_t waitpid(pid_t pid, int *wstatus, int options);
        功能:回收指定进程号的子进程,可以设置是否阻塞。
        参数:
            - pid:
                pid > 0 : 某个子进程的pid
                pid = 0 : 回收当前进程组的所有子进程    
                pid = -1 : 回收所有的子进程,相当于 wait()  (最常用) 无论是不是同组的
                pid < -1 : 某个进程组的组id的绝对值,回收指定进程组中的子进程
            - options:设置阻塞或者非阻塞
                0 : 阻塞
                WNOHANG : 非阻塞
            - 返回值:
                > 0 : 返回子进程的id
                = 0 : options=WNOHANG, 表示还有子进程或者
                = -1 :错误,或者没有子进程了
*/
#include <sys/types.h>
#include <sys/wait.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

int main() {

    // 有一个父进程,创建5个子进程(兄弟)
    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());
            sleep(1);

            int st;
            // int ret = waitpid(-1, &st, 0);
            int ret = waitpid(-1, &st, WNOHANG);

            if(ret == -1) {
                break;
            } else if(ret == 0) {
                // 说明还有子进程存在
                continue;
            } else if(ret > 0) {

                if(WIFEXITED(st)) {
                    // 是不是正常退出
                    printf("退出的状态码:%d\n", WEXITSTATUS(st));
                }
                if(WIFSIGNALED(st)) {
                    // 是不是异常终止
                    printf("被哪个信号干掉了:%d\n", WTERMSIG(st));
                }

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

进程间通信

在这里插入图片描述

一边下载视频,一遍播放视频 是两个进程

在这里插入图片描述

同一主机间进程通信:

  • 匿名管道
  • 有名管道
  • 信号
  • 消息队列
  • 共享内存
  • 信号量

匿名管道

linux中终端也是文件

fd 0 是标准输入

fd 1 是标准输出

在这里插入图片描述

在这里插入图片描述

管道是在内核内存中的缓冲器 —— 没有文件实体

有名管道是有文件实体的,但是是不存储数据的!

管道是字节流,没有消息/消息边界的概念

从管道读取的字节顺序和被写入管道的顺序是一样的 线性队列

单工:你走的时候我不能走,我走的时候你不能走

半双工:数据也是单向的流动,但是可以两边一边进一边出

在这里插入图片描述

匿名管道只能在具有公共祖先的进程(父子、兄弟、有亲缘关系)之间使用

注意他们的空间,读和写是在用户空间里的,管道缓冲区是在内核空间

在这里插入图片描述

子进程fork之后,和父进程共享文件描述符,5指向管道的写端,6指向管道的读端

父进程可写可读,子进程也是可写可读

管道的数据结构

在这里插入图片描述

环形队列

读完了就没有了

(不是说在内存中是环形,而是用逻辑设计的)

从数据的末尾开始写,从数据的开始读,读完了的再写就覆盖了

匿名管道的实现

在fork之前建立管道,否则他们进程间没法通信,文件描述符都不一样

子进程写到管道里,

父进程从读端读取数据并输入到buff里

注意当管道里没有数据时,管道是阻塞的,会等有了数据再读出

管道默认是阻塞的,如果管道中没有数据,read阻塞,如果管道满了,write阻塞

!!!子进程和父进程都要实现读和写,双方都要读写,记得把read和write岔开,否则一开始如果两个都是read,会阻塞

/*
    #include <unistd.h>
    int pipe(int pipefd[2]);
        功能:创建一个匿名管道,用来进程间通信。
        参数:int pipefd[2] 这个数组是一个传出参数。
            pipefd[0] 对应的是管道的读端
            pipefd[1] 对应的是管道的写端
        返回值:
            成功 0
            失败 -1

    管道默认是阻塞的:如果管道中没有数据,read阻塞,如果管道满了,write阻塞

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

// 子进程发送数据给父进程,父进程读取到数据输出
#include <unistd.h>
#include <sys/types.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

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());
 
        // 从管道的读取端读取数据
        char buf[1024] = {0};
        while(1) {
            int len = read(pipefd[0], buf, sizeof(buf));
            printf("parent recv : %s, pid : %d\n", buf, getpid());
            
            //向管道中写入数据
            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());
        
        char buf[1024] = {0};
        while(1) {
            // 向管道中写入数据
            char * str = "hello,i am child";
            write(pipefd[1], str, strlen(str));
            sleep(1);

            int len = read(pipefd[0], buf, sizeof(buf));
            printf("child recv : %s, pid : %d\n", buf, getpid());
            // bzero(buf, 1024);
        }
        
    }
    return 0;
}

查看管道缓冲大小:ulimit -a

8块,一块512字节 一共是4096 4K

用C库函数的话,fpathconfig 获取管道大小

匿名管道通信案例

注意下面的代码,在子/父进程里,如果read和write之间没有sleep, 那么会导致父进程读了父进程写入管道中的,子进程读了子进程写入管道中的

如果加了sleep,子进程写的就被父进程读了

但是在开发的时候,不会写sleep、在开发的时候,一般只会固定一个写端 一个读端

//bzero是清空buf里的值

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

       // 从管道的读取端读取数据
       char buf[1024] = {0};
       while(1) {
           int len = read(pipefd[0], buf, sizeof(buf));
           printf("parent recv : %s, pid : %d\n", buf, getpid());
           bzero(buf, 1024);
           
           // 向管道中写入数据
           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());

       char buf[1024] = {0};
       while(1) {
           // 向管道中写入数据
           char * str = "hello,i am child";
           write(pipefd[1], str, strlen(str));
           sleep(1);

           int len = read(pipefd[0], buf, sizeof(buf));
           printf("child recv : %s, pid : %d\n", buf, getpid());
           bzero(buf, 1024);
       }

那如果父进程就是读的,那么我们就关闭了它的写端

那如果子进程就是写的,那么我们就关闭了它的读端

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

        // 关闭写端
        close(pipefd[1]);
        
        // 从管道的读取端读取数据
        char buf[1024] = {0};
        while(1) {
            int len = read(pipefd[0], buf, sizeof(buf));
            printf("parent recv : %s, pid : %d\n", buf, getpid());
        }

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

案例

/*
    实现 ps aux | grep xxx 父子进程间的通信

    子进程: ps aux, 子进程结束后, 将数据发给父进程
    父进程:获取到数据,过滤

    pipe()      ->      管道
    execlp()    ->      ps aux 默认执行完后是发送到子进程对应的当前终端
                        需要把子进程将标准输出重定向到管道的写端
                        利用dup2(重定向文件描述符)
*/


#include <unistd.h>
#include <sys/types.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <wait.h>

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);
            //输出完后清空buf里的值
            memset(buf, 0, 1024);
        }

        //回收子进程的资源
        wait(NULL);

    }else if(pid == 0){

        //子进程

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

        //先将文件描述符重定向
        //stdout_fileno -> fd[1]
        dup2(fd[1], STDOUT_FILENO);
        execlp("ps", "ps", "aux", NULL);
        perror("execlp");
        exit(0);

    }else{
        perror("fork");
        exit(0);
    }

    return 0;
}

管道的读写特点

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

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

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

4.如果有指向管道读端的文件描述符没有关闭(管道的读端引用计数大于0),而持有管道读端的进程
也没有从管道中读数据,这时有进程向管道中写数据,那么在管道被写满的时候再次write会阻塞,
直到管道中有空位置才能再次写入数据并返回。

总结:
读管道:
管道中有数据,read返回实际读到的字节数。
管道中无数据:
写端被全部关闭,read返回0(相当于读到文件的末尾)
写端没有完全关闭,read阻塞等待
写管道:
管道读端全部被关闭,进程异常终止(进程收到SIGPIPE信号)

​ 管道读端没有全部关闭:

​ 管道已满,write阻塞
管道没有满,write将数据写入,并返回实际写入的字节数

将read设置为非阻塞

给fd[0] 设置,它就变成非阻塞

int flags = fcntl(fd[0], F_GETFL);  //获取原来的flag
flag |= O_NONBLOCK;
fntcl(fd[0], F_SETFL, flags)
#include <unistd.h>
#include <sys/types.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>

/*
    设置管道非阻塞
    int flags = fcntl(fd[0], F_GETFL);  //获取原来的flag
    flag |= O_NONBLOCK;
    fntcl(fd[0], F_SETFL, flags)
*/

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;
        fcntl(pipefd[0], F_SETFL, flags);

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



非阻塞,还是会执行,buf现在是空的,会照常打印出来

在这里插入图片描述

有名管道

匿名管道由于没有名字,只能用于亲缘关系的进程间通信。

因此提出了有名管道FIFO、也成为命名管道、FIFO文件

有名管道是有文件实体的,它是以FIFO的文件形式存在于文件系统中的

即使与FIFO的创建进程不存在亲缘关系,只要可以访问该路径,就可以彼此通过FIFO相互通信

在这里插入图片描述

有名管道和匿名管道的区别:

  1. FIFO在文件系统中作为一个特殊文件存在,但是FIFO中的内容存放在内存中
  2. 当使用FIFO的进程退出后,FIFO文件将继续保存在文件系统中以便以后使用
  3. FIFO有名字,不相关的进程可以通过打开有名管道相互通信

在这里插入图片描述

注意:

  • 只运行写程序时,写端是不会往里写数据的,因为我们还没运行读端呢
  • 只运行读端时,也是会阻塞的,因为写端还没有开始
  • 当写端关闭时,程序给读端的read返回0,就断开了,读端也不读了
  • 当读端关闭时,写端也立刻关闭了,因为收到了信号,(防止撑爆管道)

有名管道的注意事项:
1.一个为只读而打开一个管道的进程会阻塞,直到另外一个进程为只写打开管道
2.一个为只写而打开一个管道的进程会阻塞,直到另外一个进程为只读打开管道

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

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

write

#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>

//向管道中写数据
int main()
{
    //1、判断文件是否存在
    int ret = access("fifo", F_OK);
    if(ret == -1)
    {
        //2、管道不存在,创建管道
        ret = mkfifo("fifo", 0664);
        if(ret == -1)
        {
            perror("mkfifo");
            exit(0);
        }
    }

    //3、 打开管道(注意这里是写进程,我们以只写的方式打开管道)
    int fd = open("fifo", O_WRONLY);
    if(fd == -1)
    {
        perror("open");
        exit(0);
    }

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

    // 5、关闭
    close(fd);

    return 0;
} 

read

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>

// 从管道中读取数据
int main() {

    // 1.打开管道文件
    int fd = open("fifo", 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) {
            // 注意,当写端结束时,read的返回值就变成了0
            printf("写端断开连接了...\n");
            break;
        }
        printf("recv buf : %s\n", buf);
    }

    close(fd);

    return 0;
}

有名管道实现简单版聊天

这个简单版指的是,我给你发送一条、你给我发送一条。

不能一起发,一起发的话都在等待另一方读,都是阻塞的

在这里插入图片描述

为什么A B两个进程在这两句会阻塞呢?因为fifo文件是读端和写端都被打开,才继续往下,否则会阻塞

int fdw = open("fifo1", O_WRONLY);
int fdr = open("fifo2", O_RDONLY);

并不是说上来就打开两个管道,是一个打开,另一个才打开

实现传统的聊天方式:

把写管道和读管道放在两个不同的进程里,你阻塞了并不影响我继续写/读

chatA

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <stdlib.h>
#include <fcntl.h>
#include <string.h>

int main() {

    // 1.判断有名管道文件是否存在
    int ret = access("fifo1", F_OK);
    if(ret == -1)
    {
        //文件不存在
        printf("管道不存在,创建对应的有名管道\n");
        ret = mkfifo("fifo1", 0664);
        if(ret == -1)
        {
            perror("mkfifo");
            exit(0);
        }
    }


    ret = access("fifo2", F_OK);
    if(ret == -1)
    {
        //文件不存在
        printf("管道不存在,创建对应的有名管道\n");
        ret = mkfifo("fifo2", 0664);
        if(ret == -1)
        {
            perror("mkfifo");
            exit(0);
        }
    }

    //2. 以只写的方式打开fifo1
    int fdw = open("fifo1", O_WRONLY);
    if(fdw == -1)
    {
        perror("open");
        exit(0);
    }
    printf("打开fifo1成功, 等待写入数据\n");

    //3. 以只读的方式打开fifo2
    int fdr = open("fifo2", O_RDONLY);
    if(fdr == -1)
    {
        perror("open");
        exit(0);
    }
    printf("打开fifo2成功, 等待读取数据\n");

    char buf[128];
    
    while(1){
        //4. 循环地写数据
        memset(buf, 0, 128);
        //获取标准输入的数据
        fgets(buf, 128, stdin);
        //写数据
        int ret = write(fdw, buf, strlen(buf));
        if(ret == -1)
        {
            perror("write");
            exit(0);
        }

        //5. 循环地读数据
        memset(buf, 0, 128);
        ret = read(fdr, buf, 128);
        if(ret <= 0)
        {
            perror("read");
            exit(0);
        }
        printf("buf: %s\n", buf);
    }

    //6. 关闭文件描述符
        close(fdr);
        close(fdw);

    return 0;
}

chatB

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <stdlib.h>
#include <fcntl.h>
#include <string.h>

int main() {

    // 1.判断有名管道文件是否存在
    int ret = access("fifo1", F_OK);
    if(ret == -1) {
        // 文件不存在
        printf("管道不存在,创建对应的有名管道\n");
        ret = mkfifo("fifo1", 0664);
        if(ret == -1) {
            perror("mkfifo");
            exit(0);
        }
    }

    ret = access("fifo2", F_OK);
    if(ret == -1) {
        // 文件不存在
        printf("管道不存在,创建对应的有名管道\n");
        ret = mkfifo("fifo2", 0664);
        if(ret == -1) {
            perror("mkfifo");
            exit(0);
        }
    }

    // 2.以只读的方式打开管道fifo1
    int fdr = open("fifo1", O_RDONLY);
    if(fdr == -1) {
        perror("open");
        exit(0);
    }
    printf("打开管道fifo1成功,等待读取...\n");
    // 3.以只写的方式打开管道fifo2
    int fdw = open("fifo2", O_WRONLY);
    if(fdw == -1) {
        perror("open");
        exit(0);
    }
    printf("打开管道fifo2成功,等待写入...\n");

    char buf[128];

    // 4.循环的读写数据
    while(1) {
        // 5.读管道数据
        memset(buf, 0, 128);
        int ret = read(fdr, buf, 128);
        if(ret <= 0) {
            printf("???r\n");
            perror("read");
            break;
        }
        printf("buf: %s\n", buf);

        memset(buf, 0, 128);
        // 获取标准输入的数据
        fgets(buf, 128, stdin);
        // 写数据
        ret = write(fdw, buf, strlen(buf));
        if(ret == -1) {
            printf("???w\n");
            perror("write");
            exit(0);
        }
    }

    // 6.关闭文件描述符
    close(fdr);
    close(fdw);

    return 0;
}

内存映射

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

在这里插入图片描述

效率比较高,因为是直接对内存操作的

mmap: 将一个文件或者设备的数据映射到内存中

参数:

​ - void *addr: NULL, 由内核指定 (映射那块内存的首地址,我们是不知道的,由内核指定

​ - length : 要映射的数据的长度,这个值不能为0。建议使用文件的长度。

获取文件的长度:stat lseek

Linux系统里是有内存的分页的,如果没有达到分页的大小,是给分配分页整数倍的大小的

​ - p rot : 对申请的内存映射区的操作权限

​ -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

munmap:

int munmap(void *addr, size_t length);

​ - 功能:释放内存映射

​ - 参数:

​ - addr : 要释放的内存的首地址

​ - length : 要释放的内存的大小,要和mmap函数中的length参数的值一样。

使用内存映射实现进程间通信:

1.有关系的进程(父子进程)

​ - 还没有子进程的时候

​ - 通过唯一的父进程,先创建内存映射区 (类似于管道pipe的操作)

​ - 有了内存映射区以后,创建子进程

​ - 父子进程共享创建的内存映射区

2.没有关系的进程间通信

​ - 准备一个大小不是0的磁盘文件

​ - 进程1 通过磁盘文件创建内存映射区

​ - 得到一个操作这块内存的指针

​ - 进程2 通过磁盘文件创建内存映射区

​ - 得到一个操作这块内存的指针

​ - 使用内存映射区通信

注意:内存映射区通信,是非阻塞。

父子进程之间的内存映射做通信

lseek 返回从0到末尾的偏移量,也就是fd的文件大小

#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.创建内存映射区
    // 注意内存映射区的权限要和open时候的权限保持一致
    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 放前面,进来先阻塞了,等子进程运行完释放了资源再继续父。
        //子进程去写数据,父进程去读数据。
        wait(NULL);
        // 父进程
        char buf[64];
        //通过strcpy 去拷贝内容
        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(-1)
open()函数中的权限建议和prot参数的权限保持一致。

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

4.mmap什么情况下会调用失败?
- 第二个参数:length = 0
- 第三个参数:prot
- 只指定了写权限 (prot如果没有读权限,是没法操作的)
- prot PROT_READ | PROT_WRITE
第5个参数fd 通过open函数时指定的 O_RDONLY / O_WRONLY

5.可以open的时候O_CREAT一个新文件来创建映射区吗?
- 可以的,但是创建的文件的大小如果为0的话,肯定不行 -> 映射到内存的数据也为0了

​ 可以对新的文件进行扩展

​ - lseek() 用移动偏移量进行扩展

​ - truncate()

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

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

​ int fd = open(“XXX”);
​ mmap(,fd,0);
​ close(fd);

7.对ptr越界操作会怎样?

偏移量是4K的整数倍

​ void * ptr = mmap(NULL, 100,);
​ 4K
​ 越界操作操作的是非法的内存 -> 段错误

在这里插入图片描述

通过内存完成文件复制(非常快的)

一般不用内存映射来做文件拷贝。假设有1G的文件,内存都不一定放得下

// 使用内存映射实现文件拷贝的功能
/*
    思路:
        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* ptr1 =  mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED, fd,  0);
    void* ptr2 =  mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED, fd1, 0);

    if(ptr1 == MAP_FAILED)
    {
        perror("mmap");
        exit(0);
    }

    if(ptr2 == MAP_FAILED)
    {
        perror("mmap");
        exit(0);
    }

    //4. 内存拷贝
    //第一个是目的地址,第二是原地址
    memcpy(ptr2, ptr1, len);

    //5. 释放资源
    //后打开的先释放!!!!!!!!(有可能先后之间有依赖关系)
    munmap(ptr2, len);
    munmap(ptr1, len);

    close(fd1);
    close(fd);

    return 0;
}

匿名映射

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

    没文件实体,不同进程之间不能通过文件进行内存映射的关联共享,所以只能在父子进程间

    匿名映射是把mmap的flag参数改为 MAP_ANONYMOUS
*/

#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 <wait.h>

int main()
{   
    //1. 创建匿名内存映射区
    int len = 4096;
    // MAP_SHARED | MAP_ANONYMOUS 匿名映射 fd 设置为-1
    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)
        sleep(1);
        printf("%s\n", (char*)ptr);
        
    }

    //释放内存映射区
    int ret = munmap(ptr, len);

    if(ret == -1)
    {
        perror("munmap");
        exit(0);
    }

    return 0;
}

信号概述

在这里插入图片描述

信号:导致一个正在运行的进程中断,去处理某一个突发事件

Ctrl + C -> 2号信号

产生信号的四种情况(上面)

在这里插入图片描述

查看系统定义的信号:kill - l

1~31是操作系统的标准信号

34~64是预定义好的实时信号

在这里插入图片描述


在这里插入图片描述
在这里插入图片描述

收到信号后,默认做5种处理动作中的一种

生成了Core文件后,异常写在了Core文件里,可以查看异常产生的原因

在这里插入图片描述

信号有3个值一般是不同的架构,我们目前用的Intel X86是中间的

core的使用

使用ulimit -a

可以看到core文件大小规定为0 (默认)

我们可以用 ulimit -c 1024 设置core文件大小为1024

以gcc core.c -g 的形式进行编译

在gdb模式中 输入 core-file core(core文件名) ->可以看到错误信息

kill、raise、abort函数

int kill(pid_t pid, int sig);

​ - 功能:给任何的进程或者进程组pid, 发送任何的信号 sig

​ - 参数:

​ - pid :

​ > 0 : 将信号发送给指定的进程

​ = 0 : 将信号发送给当前的进程组

​ = -1 : 将信号发送给每一个有权限接收这个信号的进程

​ < -1 : 这个pid=某个进程组的ID取反 (-12345) 表示给12345发

​ - sig : 需要发送的信号的编号或者是宏值,0表示不发送任何信号

使用宏值是更好的,不同架构下的编号是不一样的,但是宏值是一样的

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

alarm 函数

unsigned int alarm(unsigned int seconds);

​ - 功能:设置定时器(闹钟)。函数调用,开始倒计时,当倒计时为0的时候,

​ 函数会给当前的进程发送一个信号:SIGALARM

​ - 参数:

​ seconds: 倒计时的时长,单位:秒。如果参数为0,定时器无效(不进行倒计时,不发信号)。

取消一个定时器,通过alarm(0)。

​ - 返回值:

​ - 之前没有定时器,返回0

​ - 之前有定时器,返回之前的定时器剩余的时间

- SIGALARM :默认终止当前的进程,每一个进程都有且只有唯一的一个定时器。

​ alarm(10); -> 返回0

​ 过了1秒

​ alarm(5); -> 返回9 (直接覆盖了上面的alarm),重新开始计时

alarm(100) -> 该函数是不阻塞的、还能继续往下运行

#include <stdio.h>
#include <unistd.h>

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

1秒能数多少数?

// 1秒钟电脑能数多少个数?
#include <stdio.h>
#include <unistd.h>

/*
    实际的时间 = 内核时间 + 用户时间 + 消耗的时间
    进行文件IO操作的时候比较浪费时间

    定时器,与进程的状态无关(自然定时法)。无论进程处于什么状态,alarm都会计时。
*/

int main() {    

    alarm(1);

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

    return 0;
}

我们看起来好像不止一秒,其实程序是一秒结束的,但是从buffer到终端显示出来用了一些时间,那个不算

可以把直接打印出来改为重定向到a.txt里

./alarm >> a.txt

省去了很多IO

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

进行文件IO操作的时候比较浪费时间

调用alarm,切换到内核(因为是系统调用),内核调用完又切换到用户区 也浪费时间

定时器,与进程的状态无关(自然定时法)。无论进程处于什么状态(运行、就绪、终止。。。),alarm都会计时。

setitimer 周期性定时

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

没有看到定时的实现,于是现在我们要实现一下 信号捕捉

3秒之后发出了SIGALRM 信号,就把进程终止了,没有看到周期性的执行

signal 信号捕捉函数

typedef void (*sighandler_t)(int);

sighandler_t signal(int signum, sighandler_t handler);

​ - 功能:设置某个信号的捕捉行为

​ - 参数:

​ - signum: 要捕捉的信号

​ - handler: 捕捉到信号要如何处理

​ - SIG_IGN : 忽略信号

​ - SIG_DFL : 使用信号默认的行为

​ - 回调函数 : 这个函数是内核调用,程序员只负责写,捕捉到信号后如何去处理信号

​ 回调函数:

​ - 需要程序员实现,提前准备好的,函数的类型根据实际需求,看函数指针的定义

​ - 不是程序员调用,而是当信号产生,由内核调用

​ - 函数指针是实现回调的手段,函数实现之后,将函数名放到函数指针的位置就可以了。

​ - 返回值:

​ 成功,返回上一次注册的信号处理函数的地址。第一次调用返回NULL

​ 失败,返回SIG_ERR,设置错误号

SIGKILL SIGSTOP不能被捕捉,不能被忽略。

进入程序一开始,就要定义好信号捕捉,这样后面才能捕捉到

SIG_IGN -> 忽略了这个信号了

SIG_DFL -> 什么都没做, 如果是捕捉SIGALRM 那就停下了

回调函数的参数 int 类型 -> 表示捕捉到的信号的值

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

信号集及相关函数

PCB信号集 每一个进程都有一个pcb进程控制块,用来控制进程的信息,同时信号在pcb中有两个队列去维护他,一个是未决信号集,每一位对应一个信号的状态,0,1,1表示未决态,另一个是信号屏蔽字(阻塞信号集),也就 0,1(1代表阻塞),

在这里插入图片描述

阻塞信号集,用位图机制来实现的 -> 64个为 1表示阻塞、0表示不阻塞

未决 -> 信号还没有被处理

阻塞

在这里插入图片描述

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

2.信号产生但是没有被处理 (未决)

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

3.这个未决状态的信号,需要被处理处理之前需要和另一个信号集(阻塞信号集),进行比较

- 阻塞信号集默认不阻塞任何的信号**(人为设置阻塞某信号)**
    - 如果想要阻塞某些信号需要用户调用系统的API **未决信号集要和阻塞信号集里的信号进行比较,如果没有阻塞,那就可以去处理,未决信号集变为0**

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函数

只有通过sigprocmask才能改变内核当中的信号集

int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);

​ - 功能:将自定义信号集中的数据设置到内核中(设置阻塞,解除阻塞,替换)

​ - 参数:

​ - how : 如何对内核阻塞信号集进行处理

SIG_BLOCK: 将用户设置的阻塞信号集添加到内核中,内核中原来的数据不变

​ 假设内核中默认的阻塞信号集是mask, mask | set 其实就是只加了set里的1

​ 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);
        if(num == 10) {
            // 解除阻塞
            sigprocmask(SIG_UNBLOCK, &set, NULL);
        }

    }

    return 0;
}

信号产生但是没有被处理 (未决)

按了ctrl + C 2号信号的未决信号集就会变成1

因为现在的信号集是阻塞状态,所以信号没法被处理,当解除阻塞后,信号才能被处理,终止进程

等解除了阻塞,未决就会被处理

在这里插入图片描述

再按 ctrl + q 3号新号的未决信号集就也会变成1

在这里插入图片描述

以后台方式运行,加&

sigaction 信号捕捉函数

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

};

推荐使用sigaction -> 它的版本比较一致

#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;
    //置为0,使用sa_handler处理
    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;
}

在这里插入图片描述

在调用回调函数的时候,会使用临时阻塞的信号集,调用完回去之后,会使用内核中的信号集

信号产生之后,都是先改变未决信号集中的状态为1,然后去和阻塞信号集里去做比较,再执行

SIGCHLD信号

在这里插入图片描述

SIGSTOP是暂停

能不能在父进程中捕捉这个SIGCHILD,当子进程结束了,再执行wait(),而不是一直阻塞的等它

SIGCHLD信号产生的3个条件:

​ 1.子进程结束

​ 2.子进程暂停了

​ 3.子进程继续运行

​ 都会给父进程发送该信号,父进程默认忽略该信号。

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

kill -l 查看 SIGCHILD是17号信号

注意:未决信号集只能记录一个信号,它没有buffer去做缓冲

20个子进程有3个同时死亡, 17号变成1后,执行回调函数,在处理的时候,产生的很多SIGCHILD都被舍弃了

如果使用wait(NULL) 导致最终很多子进程没有被回收

所以我们使用waitpid()并且套一个while在外面重复的去清理(为什么不while套wait?因为wait会阻塞,waitpid可以设置非阻塞)

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

有没有可能信号注册还没成功,子进程就运行完了?

所以我们先阻塞掉信号集的17号,等信号集注册完之后再去解除阻塞

子进程发送给父进程SIGCHILD信号,父进程是会默认忽略的,信号就从未决状态转变为结束状态了

如果阻塞一下,能让它先停在未决,最后注册完后再处理他

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

注册完信号捕捉之后,接触阻塞

// 捕捉子进程死亡时发送的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);

waitpid 参数为-1回收所有(执行完的)子进程

整体代码:

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

共享内存(效率高于进程映射)

因为内存映射有一个文件,要同步到里面

在这里插入图片描述

在这里插入图片描述

使用shmat()来附上共享内存段、是在用户区的共享库里

多个进程共享一个内存段,只有当所有附加内存段都分离了,内存段才会销毁

共享内存相关的函数
#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'

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

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

问题1:操作系统如何知道一块共享内存被多少个进程关联?
- 共享内存维护了一个结构体struct shmid_ds 这个结构体中有一个成员 shm_nattch
- shm_nattach 记录了关联的进程个数

问题2:可不可以对共享内存进行多次删除 shmctl
- 可以的
- 因为shmctl 标记删除共享内存,不是直接删除
- 什么时候真正删除呢?
当和共享内存关联的进程数为0的时候,就真正被删除
- 当共享内存的key为0的时候,表示共享内存被标记删除了,当所有进程和他取消关联,共享内存才真正地删除
如果一个进程和共享内存取消关联,那么这个进程就不能继续操作这个共享内存。也不能进行关联。

共享内存和内存映射的区别
1.共享内存可以直接创建,内存映射需要磁盘文件(匿名映射除外)
2.共享内存效果更高
3.内存
    所有的进程操作的是同一块共享内存。
    内存映射,每个进程在自己的虚拟地址空间中有一个独立的内存。(用文件的方式关联起来)
4.数据安全
    - 进程突然退出
        共享内存还存在
        内存映射区消失
    - 运行进程的电脑死机,宕机了
        数据存在在共享内存中,没有了
        内存映射区的数据 ,由于磁盘文件中的数据还在,所以内存映射区的数据还存在。

5.生命周期
    - 内存映射区:进程退出,内存映射区销毁
    - 共享内存:进程退出,共享内存还在,标记删除(所有的关联的进程数为0),或者关机
        如果一个进程退出,会自动和共享内存进行取消关联。

在这里插入图片描述

守护进程

在这里插入图片描述

echo $$ 查看当前终端的进程号

终端可以理解为一个设备,例如 dev/pts/1 然后这个设备是运行了.bash 进程

在这里插入图片描述

进程组的ID和组长的ID是一样的,组长不需要最后一个离开

在这里插入图片描述

会话首进程创建会话,新进程继承会话ID

一个终端最多可能成为一个会话的控制终端

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

前台进程组、后台进程组

控制终端链接建立起来后,会话首进程成为终端控制进程**(控制终端是有控制进程的,它是会话的首进程)**

bash 是会话首进程

在这里插入图片描述

任意时刻只有一个前台进程组

在这里插入图片描述

在这里插入图片描述

守护进程是后台进程,控制终端不能对他进行操作,它没有控制终端

在这里插入图片描述

为什么父进程退出?

(1)确保它死了之后,终端不会弹出让我们输入命令的那一行 提示符 就是一执行,它立马就死了,弹出终端提示符,而不是走到一半了再弹出来的

(2)父进程是进程组的首进程,子进程不会和进程组ID重合 (这样才能调用setid)

产生一个新的会话,默认是没有任何控制终端连接他的。所以没人控制它。但是对于bash进程,它内部会和控制终端建立连接。

如果用父进程去setsid开启一个新会话,那么新会话的id又会和原来会话的id重合,所以要用子进程创建

改成根目录是因为根目录不会卸载掉,如果你在U盘里运行,它可能会卸载的

关闭继承来的文件描述符(你不能一直占用着,如果是U盘里的文件,我要是卸载都没法卸载,说他已经打开了)

为什么创建新的会话?————> 脱离原先的控制终端

改变 标准输入/输出/错误 重定向到 /dev/null

它就不会输出到当前终端,因为它一直在运行,不能一直让他往终端输出啊,我们一般输出到文件里

写一个守护进程,实现每隔2秒输出一次当前时间(到文件里)

重点!

/*
    写一个守护进程,每隔2s获取一下系统时间,将这个时间写入到磁盘文件中。
*/

#include <stdio.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/time.h>
#include <signal.h>
#include <time.h>
#include <stdlib.h>
#include <string.h>

void work(int sigid)
{
    time_t tm = time(NULL);
    struct tm* loc = localtime(&tm);

    char* str = asctime(loc);
    int fd = open("text.txt", O_CREAT | O_RDWR | O_APPEND, 0664);
    write(fd, str, strlen(str));
    close(fd);
}

int main()
{
    //1.建立一个子进程并杀死父进程
    pid_t pid = fork();
    if(pid > 0)
    {
        exit(0);
    }

    //2.子进程调用setsid开启新的会话
    setsid();

    //3.清除umask(设置掩码)
    umask(022);

    //4.修改当前工作目录,通常为根目录
    chdir("root/gjcc/");

    //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;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值