Linux高并发服务器开发(二)Linux多进程开发

2.1 进程概述

五态模型

程序是包含一系列信息的文件,这些信息描述了如何在运行时创建一个进程:

进程 是正在运行的程序的实例。是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。是操作系统动态执行的基本单元。

单道程序 -- 在计算机中只允许一个的程序运行;多道程序 -- 在计算机内存中同时存放几道相互独立的程序,该技术引入的根本目的是为了提高CPU的利用率。

时间片timeslice是操作系统分配给每个正在运行的进程微观上的一段CPU时间。时间片由操作系统内核的调度程序分配给每个进程。

并行parallel指的是在同一时刻,有多条指令在多个处理器上同时执行并发concurrency是指在同一时刻只能有一条指令执行,但多个进程指令被快速的轮换执行,使得在宏观上具有多个进程同时执行的效果,但在微观上并不是同时执行的,只是把时间分成若干段,使多个进程快速交替执行。

PCB(Processing Control Block)-- 为了管理进程,内核必须对每个进程所做的事情进行清楚的描述。内核为每个进程分配一个PCB进程控制块,维护进程相关的信息。Linux内核的进程控制块是task_struct结构体,在/usr/src/linux-headers-xxx/include/linux/sched.h文件中可以查看truct task_struct结构体定义

关于进程的Linux实用指令:

        ulimit -a --> 显示当前系统的一些资源上限

2.2 进程状态转换

进程状态反应进程执行过程的变化。三态模型(就绪态、运行态、阻塞态),五态模型(新建态、终止态、就绪态、运行态、阻塞态)

三态模型
与进程相关的Linux下命令:

        ps -aux / ajx --> 查看进程【a:显示终端上的所有进程,包括其他用户的进程;u:显示进程的详细信息;x:显示没有控制终端的进程;j:列出与作业控制相关的信息

        top --> 实时显示进程动态【top -d 来指定显示信息更新的时间间隔,在top命令执行后,可以按以下按键对显示的结果进行排序👇】

        kill [-signal] pid --> 杀死进程【-l:列出所有信号;- SIGKILL 进程ID 或者 -9 进程ID:-9是9号信号(SICKILL)强制杀死进程; killall 根据进程名杀死进程】

        运行一个进程的时候可以加&符号(比如,./a.out &)则该进程将会在后台运行,不会阻塞其他进程

进程号和相关函数

        每个进程都由进程号来标识,其类型为pid_t(整型),进程号的范围:0~32767。进程号总是唯一的,但可以重用。当一个进程终止后,其进程号就可以再次使用。

        任何进程(除init进程)都是由另一个进程创建,该进程称为被创建进程的父进程,对应的进程号称为父进程号(PPID)

        进程组是一个或多个进程的集合。他们之间相互关联,进程组可以接收同一终端的各种信号,关联的进程有一个进程组号(PGID)。默认情况下,当前的进程号会当作当前的进程组号

        pid_t getpid(void); //获取当前进程的进程号;pid_t getppid(void); //获取当前进程的父进程号;pid_t getpgid(pid_t pid); //获取当前进程的进程组号;

2.3 进程创建

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

#include <sys/types.h>
#include <unistd.h>
pid_t fork(void); //函数的作用:用于创建子进程
/*
    返回值:
        成功:子进程中返回0,父进程中返回子进程ID
        失败:返回-1
            在父进程中返回-1,表示创建子进程失败,并设置errno
        //
        fork()的返回值会返回两次。一次是在父进程中,一次是在子进程中;
        在父进程中返回创建的子进程的ID,在子进程中返回0;
        如何区分父进程和子进程:通过fork的返回值。

    失败的两个主要原因:
        1.当前系统的进程数已经达到了系统规定的上限,这时errno的值被设置为EAGAIN;
        2.系统内存不足,这时errno的值被设置为ENOMEM;
*/
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>

int main(){

    //创建子进程
    pid_t pid = fork(); //fork()函数会返回一个pid_t类型

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

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

运行结果如下(父进程即为当前终端(-bash)):

2.4 父子进程虚拟地址空间情况

调用fork()函数以后,子进程的用户区数据和父进程一样。内核区也会拷贝过来。但是pid不同,且之后再调用栈区的临时保存的数据时互不影响。【子进程和父进程运行在分别的内存空间中,在fork()调用的时候,两者的内存空间有相同的内容。当我们写的时候,会通过mmap(2)保证它们之间互不影响】        

        更准确说,Linux的fork() 使用的是写实拷贝(copy- on-write)实现。写实拷贝是一种可以推迟甚至避免拷贝数据的技术。内核此时并不复制整个进程的地址空间,而是让父子进程共享同一个地址空间。只用在需要写入的时候才会复制地址空间,从而使各个进行拥有各自的地址空间。也就是说,资源的复制是在需要写入时才会进行,在此之前,只有以只读的方式共享。

        注意:fork之后父子进程共享文件。【fork产生的子进程与父进程相同的文件 文件描述符指向相同的文件表,引用计数增加,共享文件偏移指针】 【即 读时共享,写时拷贝

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

2.5.1 父子进程之间的关系

        区别:

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

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

                        子进程中:=0

                2. pcb中的一些数据

                        当前的进程的id【pid】;

                        当前的进程的父进程的id 【ppid】;

                        信号集

        共同点:

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

                                -用户区的数据

                                -文件描述符表

        读时共享(子进程被创建,两个进程没有做任何的修改时),写时拷贝【刚开始的时候,父子进程对变量是共享的。如果修改了数据,则不共享了】

2.5.2 GDB多进程调试

        使用GDB调试的时候,GDB默认只能跟踪一个进程,可以在fork函数调用之前,通过指令设置GDB调试工具跟踪父进程或者是跟踪子进程。默认跟踪父进程。

        设置调试父进程或者子进程: set follow-fork-mode [parent (默认) | child]

        设置调试模式: set detach-on-fork [on | off]

                默认为on,表示调试当前进程的时候,其他的进程继续运行;如果为off

,调试当前进程的时候,其他进程被GDB挂起。

        查看调试的进程:info inferiors

        切换当前调试的进程:inferior id

                要再按 一个c(continue)

        使进程脱离GDB调试:detach inferiors id

2.6 exec函数族

        类似C++的函数重载;一系列功能相同或相似的函数

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

        exec函数族的函数执行成功后不会返回,因为调用进程的实体,包括代码段,数据段和堆栈等都已经被新的内容取代。只留下进程ID等一些表面上的信息仍保持原样。只有调用失败了,它们才会返回-1,从原程序的调用点接着往下执行。

exec函数族

2.6.1 execl

int execl (const char *path, const char *arg, ... /* (char *) NULL */);

标准C库中

#incoude <unistd.h> //头文件

int execl( const char *path, const char *arg, ...); //可变参数

        参数:

                path --> 需要指定的执行的文件的路径或名称(建议写绝对路径)

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

        返回值:

                只有调用失败才会有返回值(-1),并且设置errno

例:

#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);//此处用的相对路径

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

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

}

程序不会执行到“i am child process”这句话,因为在execl函数后其他的代码都会被execl中的hello可执行文件替换掉。进程只会执行父进程中的输出i循环和子进程中的hello可执行文件

2.6.2 execlp

int execlp (const char *file, const char *arg, ... /* (char *) NULL */ );

        回到环境变量中查找指定的可执行文件,如果找到了就执行,找不到就执行不成功(不指定绝对路径,但是它能帮我们去环境变量中查找)

        参数:

                file --> 需要执行的可执行文件的文件名

        返回值:

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


一个补充:(引用,没仔细看)

execve部分的运行情况感觉和老师您描述得有些不太一致,望老师帮忙检查下我的理解。 

提问区有好几位同学都无法运行,我仔细查了下。 

我认为问题原因可能在于:第三个参数envp数组参数不是用来查找可执行程序的,而是为可执行程序运行期间增加新的环境变量因此在第二个参数中argv数组中,第一个元素一定需要把路径写对。至于第一个参数filename,确实是可执行程序的程序名。但不写路径的原因不是envp,而是第二个参数中argv中第一个元素就已经可以查找到需要执行的可执行程序。

我按照这种理解方式重新写了三个文件测试了一下,应该是符合这个理解。 

这是第一个文件hello.c的代码,主要是输出hello world

#include <stdio.h>

int main() {    
    printf("hello, world\n");
    return 0;
}

这是第二个文件execlp.c的代码:主要是使用execlp(代码中直接使用了文件名,且该可执行程序所在目录不在系统的环境变量中),执行上面的hello。

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

int main() {
    printf("execlp\n");
    //这里直接使用文件名
    execlp("hello", "hello", NULL);
    return 0;
}

这是第三个文件execve.c的代码:主要是使用execve,执行上面的execlp

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

int main() {

    pid_t pid = fork();

    if(pid > 0) {
        printf("i am parent process, pid : %d\n",getpid());
        sleep(1);
    }else if(pid == 0) {
        
        //args中第一个参数一定要把路径写对
        char *args[] = {"./execlp", "execlp", (char *)0};
        
        //env_args为我们执行的execlp新增了环境变量,这样excelp中可以找到hello这个可执行程序
        char *env_args[] = {"PATH=/home/yc/Documents/Linux/code/lesson19",(char*)0};
        
        execve("execlp", args, env_args);
        
        printf("i am child process, pid : %d\n", getpid());
    }
    return 0;
}

最后实际的运行结果:

i am parent process, pid : 7790
execlp
hello, world

调用fork函数会创建一个子进程,共享用户区的数据(内核区不相同:有自己的编号 等),会降低内存的使用。

采用fork函数机制(读时共享 写时复制)创建子进程,再在子进程中使用exec函数族中的函数(把原先用户区的数据用新的数据进行替换)。若没有用fork的话,就会将父进程中的虚拟地址空间拷贝过来,会浪费时间与内存。(?) 

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

2.7.1 进程退出

右图为_exit()和exit()运行区别:exit()函数多两个步骤

参数status为进程状态;

/*
    #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");//标准C库中的“\n”会刷新缓冲区
    printf("world");//若使用情况2,则这个“world”会放在缓冲区中,程序结束销毁,不会被printf出来

    //情况1
    exit(0);//标准C库的exit
    //情况2
    _exit(0);
    
    return 0;
}

进程中出现某些错误时,可以使用exit()退出子进程。

2.7.2 孤儿进程

        父进程运行结束,但子进程还在运行(未运行结束),这样的子进程就称为孤儿进程(Orphan Process)

        每当出现一个孤儿进程时,内核就把孤儿进程的父进程设置为 init,而 init 进程会循环地 wait() 它已经退出的子进程。

        孤儿进程并不会有什么危害。

测试(使用之前fork()函数的代码进行更改):

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

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);//休息1s,父进程会结束
        printf("i am child process, pid: %d, ppid: %d\n", getpid(), getppid());
        //此时输出结果会有一个终端提示,再打印这句话,ppid(父进程)=1(孤儿进程会被分配到进程号为1的进程,由该进程回收子进程)
    }

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

父进程结束后,会有一段终端显示:因为父进程有输入状态的时候,界面会切换到后台输出打印,当父进程结束时,父进程的ppid会知道父进程什么时候结束,那么会让界面切换回前台。

运行结果会停留在一个 好像程序还在等待运行 的状态,但实际上可以输入其他指令:当ppid知道父进程结束后,界面切换回前台。但是子进程还有内容要输出,所以子进程的内容会继续输出。

2.7.3 僵尸进程

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

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

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

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

2.8 wait函数

2.8.1 进程回收

        在每个进程退出时,内核释放该进程所有的资源,包括打开的文件、占用的内存等。但是仍然为其保留一定的信息,这些信息主要指进程控制块PCB的信息(包括进程号、退出状态、运行时间等)

        父进程可以通过调用wait或waitpid得到它的退出状态,同时彻底清除掉这个进程。

        wait和waitpid函数的功能一样,区别在于,wait函数会阻塞,waitpid可以设置不阻塞,wiatpid还可以指定哪个子进程结束。

        注意:一次wait或waitpid调用只能清理一个子进程,清理多个子进程应使用循环。

2.8.2 wait()函数

        查询wait函数:man 2 wait //查看wait函数的man文档

/*
    #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>

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

    //⚠️只创建五个子进程
    for(int i = 0; i < 5; i++){
        pid = fork();
        if(pid == 0){
            //pid == 0 说明之前fork成功创建了一个子进程,并且正在这个子进程中
            break;//防止在子进程中再fork出一个孙子进程
        }
    }

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

            int ret = wait(NULL);//子进程没有死的时候,它会在这里阻塞(挂起)。若子进程死掉,则它会不阻塞,往下执行。
            
            if(ret == -1){
                break;
            }

            printf("child die, pid = %d\n",ret);
            //若打印-1,说明没有子进程
            //若杀死子进程成功了,则会返回子进程的pid

            sleep(1);//父进程等待,此时子进程结束,但是还有残留资源在PCB中
        }
    }else if(pid == 0){
        //子进程
        //用while指令,可以让子进程不一运行完死掉,可以手动通过kill指令杀死子进程
        while(1){
            print("child, pid = %d\n",getpid());
            sleep(1);//睡眠1秒,不然速度太快了
        }//5个子进程将会不断循环打印
    }

    return 0;
}

pid_t wait (int *wstatus); 中子进程状态的获取

2.9 waitpid函数

/*
    #include <sys/types.h>
    #include <sys/wait.h>
    pid_t waitpid(pid_t pid, int *wstatus, int options);
        功能:回收指定进程号的子进程,可以设置是否阻塞。(wait函数是默认阻塞状态的)
        参数:
            pid --
                pid > 0 :回收某个子进程
                pid = 0 :回收当前进程组的所有子进程
                pid = -1:回收所有子进程,相当于调用wait函数(最常用)
                pid < -1:回收某个进程组的组id(进程组号为pid的绝对值)
            options -- 设置阻塞/非阻塞
                0:阻塞
                WNOHANG:非阻塞
        返回值:
            > 0:返回子进程的id
            = 0:options=WNOHANG(在非阻塞的情况下才会返回0的值),表示还有子进程活着(没有退出)
            = -1:错误,或者没有子进程了
*/

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

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

    
    for(int i = 0; i < 5; i++){
        pid = fork();
        if(pid == 0){
            //pid == 0 说明之前fork成功创建了一个子进程,并且正在这个子进程中
            break;//防止在子进程中再fork出一个孙子进程
        }
    }

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

            int st;
            //int ret = waitpid(-1, &st, 0);//阻塞,相当于wait函数
            int ret = waitpid(-1, &st, WNOHANG);//非阻塞
            
            if(ret == -1){
                break;
            }

            else if(ret == -1){
                //说明还有子进程存在,继续调用waitpid等待子进程结束去回收
                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);
                //若打印-1,说明没有子进程
                //若杀死子进程成功了,则会返回子进程的pid
            }

        }
    }else if(pid == 0){
        //子进程
        //用while指令,可以让子进程不一运行完死掉,可以手动通过kill指令杀死子进程
        while(1){
            print("child, pid = %d\n",getpid());
            sleep(1);//睡眠1秒,不然速度太快了
        }//5个子进程将会不断循环打印
        exit(0);
    }

    return 0;
}

非阻塞的好处是父进程不用一直在这里挂起,可以继续往下去做其他的业务。

2.10 进程间的通信简介

        进程是一个独立的资源分配单元,不同进程(通常指的是用户进程)之间的资源是独立的、没有关联的,不能在一个进程中直接访问另一个进程的资源。

        但是进程并孤立,不同进程需要进行信息的交互和状态的传递等,因此需要进程间通信(IPC: Inter Processes Communication)

        进程间通信的目的:

                数据传输:一个进程需要将它的数据发送给另一个进程

                通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如 进程终止时需要通知父进程)

                资源共享:多个进程之间共享同样的资源。为了做到这一点,需要内核提供互斥和同步机制

                进程控制:有些进程希望完全控制另一个进程的执行(如 Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。

2.11 匿名管道概述

2.11.1 匿名管道概述

        管道也叫无名(匿名)管道,它是 UNIX 系统 IPC(进程间通信)的最古老形式,所有的UNIX系统都支持这种通信机制

        统计一个目录中文件的数目命令:ls | wc -l(“|”为管道符),为了执行该命令,shell 创建了两个进程来分别执行 ls 和 wc

 ls 原先是把标准输出stdout对应到终端,现在是对应到管道写入端,wc 进程启动以后,标准输入stdin对应到管道的读取端。

2.11.2 管道的特点

        管道其实是一个在内核内存中维护的缓冲器,这个缓冲器的存储能力是有限的,不同的操作系统大小不一定相同。

        管道拥有文件的特质:读操作、写操作,(匿名管道和有名管道的区别)匿名管道没有文件实体,有名管道有文件实体,但不存储数据。可以按照操作文件的方式对管道进行操作。

        一个管道是一个字节流,使用管道时不存在消息或者消息边界的概念,从管道读取数据的进程可以读取任意大小的数据块,而不管写入进程写入管道的数据块的大小是多少。

        通过管道传递的数据是顺序的,从管道中读取出来的字节的顺序和它们被写入管道的顺序是完全一样的。

        在管道中的数据传递方向是单向的,一端用于写入,一端用于读取,管道是半双工的。

        从管道读数据是一次性操作,数据一旦被读走,它就从管道中被抛弃,释放空间以便写入更多的数据,在管道中无法使用 lseek() 来随机访问数据。

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

2.11.3 为什么可以使用管道进行进程间通信

如图:左边为父进程的文件描述符表A,fork出一个右边的进程(是和A有关系的进程或者是子进程),文件描述符表B。此时它们共享文件描述符表。A中文件描述符3 对应文件A的话,B中的3也对应文件A……管道相当于文件,所以若A中的5对应管道的写端,B中的5也会对应文件的写端;A中的6对应读端,B中的6也会对应读端。

为什么可以进行进程间的通信:主要的原因是A fork 出来的B 与A共享文件描述符。可以进行有关系的进程之间的通信。

2.11.4 管道的数据结构

        一般来说,管道的数据结构其实是队列,一般是用数组实现的。

        传统的队列通常会有一些弊端:删掉的空间不能重复使用

        所以管道一般会设计成环形队列。这样写完后可以覆盖之前的数据,不会浪费内存空间。

2.11.5 管道的使用

创建匿名管道
    #include <unistd.h>
    int pipe(int piprfd[2]);

查看管道缓冲大小
    ulimit -a

查看管道缓冲大小函数
    #include <unistd.h>
    long fpathconf(int fd, int name);

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

2.12.1 创建匿名管道

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

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

// 子进程发送数据给父进程,父进程读取到数据输出
#include <unistd.h>
#include <sys/type.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};
        int len = read(pipefd[0], buf, sizeof(buf)); //read的返回值读取到的是字节个数
        printf("parent recv : %s ,pid: &d\n", buf, gerpid());
    }else if(pid == 0){
        //子进程
        printf("i am child process, pid : %d\n", getpid());
        //写入数据
        char * str = "hello, i am child";
        write(pipefd[1], str, strlen(str));        
    }

    //
    pipe(pipefd);

    return 0;
}

2.12.2 查看管道缓冲大小 

        命令:ulimit -a

        函数:

                #include <unistd.h>
                long fpathconf(int fd, int name);

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

int main(){

    int pipefd[2];

    int ret = pipe(pipefd);

    //获取管道的大小
    long size = fpathconf(pipefd[0], _PC_PIPE_BUF);//name是一个宏值,可以去man文档中查看

    printf("pipe size: %ld\n", size);

    return 0;
}

2.13 匿名管道通信案例

1. (2.12遗留问题:若关闭sleep(1),父子进程同时读写会出现问题。??)

2. 案例:用函数实现一个管道功能

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

    子进程:ps aux ,子进程结束后,将数据发送给父进程
    父进程:获取到数据,过滤(此处不实现过滤的功能)

    pipe() //创建命令管道
    execlp() //默认把进程发送给当前终端
    子进程将标准输出 stdout_fileno 重定向到管道的写端  dup2()

*/

#include <unistd.h>
#include <sys/type.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){
        //创建管道失败
        peeror("pipe");
        exit(0);
    }

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

    if(pid > 0){
        //父进程
        //关闭写端
        close(fd[1]);

        //从管道中读取
        char buf[1024] = {0};

        int len = -1; 
        
        //"-1"是因为有一个字符串的结束符占1个字节
        while((len = read(fd[0, buf sizeof(buf) - 1)) > 0){
            //有数据
            //过滤数据输出
            printf("%s", buf);
            memset(buf, 0, 1024);
        }
        
    }else if(pid == 0){
        //子进程
        
        //关闭读端
        close(fd[0]);

        //文件描述符的重定向:如果直接执行ps aux的话,就会直接执行到终端 stdout_fileno -> fd[1]
        dup2(fd[1], STDOUT_FILENO);
        
        //写管道:执行 ps aux命令
        execlp("ps", "ps", "aux", NULL);
        peeror("execlp");
        exit(0);

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

    }else{
        //错误
        perror(" fork");
        exit(0);
    }


    return 0;
}

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

2.14.1 管道读写的特点

        使用管道时需要注意以下特殊情况(默认阻塞状态下):

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

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

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

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

总结:

2.14.2 设置管道非阻塞 

        == 设置文件描述符非阻塞;

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

/*
    设置管道非阻塞
    int flags = fcntl(fd[0], GETFL); //获取原来文件描述符的状态flag
    flags |= O_NONBLOCK; //修改flag -- 设置非阻塞
    fcntl(fd[0], F_SETDL, flags); //设置新的flag
*/

int main(){
    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]);

        //new!
        int flags = fcntl(pipefd[0], GETFL); //获取原来文件描述符的状态flag
        flags |= O_NONBLOCK; //修改flag -- 设置非阻塞
        fcntl(pipefd[0], F_SETDL, flags); //设置新的flag

        //从管道中读取数据
        char buf[1024] = {0};
        while(1){
            printf("len : %d", len);
            int len = read(pipefd[0], buf, sizeof(buf));
            memset(buf, 0, 1024);//清空buf
            printf("parent recv: %s, pid: %d\n", buf, getpid());
            sleep(1);
        }
    
    }
    else if(pid == 0){
        //子进程
        printf("i am child process, pid = %d\n", getpid());

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

        //向管道中写数据
        while(1){
            char *str = "hello ,i am child";
            write(pipefd[1], str, strlen(str));
            sleep(5);//若睡眠5秒,则父进程很明显读数据会阻塞。-->设置不阻塞
        }
    }


    return 0;
}

 子进程休眠5秒(写),父进程休眠1s(读)。在设置不阻塞的情况下,打印4次父进程才会打印1次子进程

2.15 有名管道介绍及使用

2.15.1 FIFIO

        匿名管道,由于没有名字,只能用于亲缘关系的进程间通信。为了克服这个缺点,提出了有名管道(FIFO),也叫命名管道、FIFO文件。

        有名管道(FIFO)不同于匿名管道之处在于它提供了一个路径名与之关联,以 FIFIO 的文件形式存在于文件系统中,并且其打开方式与打开一个普通文件是一样的,这样即使与 FIFO 的创建进程不存在亲缘关系的进程,只要可以访问该路径,就能够彼此通过FIFO相互通信,因此,通过FIFO不相关的进程也能交换数据。

        一旦打开了FIFO,就能在它上面使用与操作匿名管道和其他文件的系统调用一样的I/O系统调用了(如read()、write()和close())。与管道一样,FIFO 也有一个写入端和读取端,并且从管道中读取数据的顺序与写入的顺序是一样的。FIFO的名称也由此而来:先入先出。

        有名管道(FIFO)和匿名管道(pipe)有一些特点是相同的,不一样的地方在于:

                1、FIFO 在文件系统中作为一个特殊文件存在,但 FIFO 中的内容却存放在内存中。

                2、当使用 FIFO 的进程退出后, FIFO 文件将继续保存在文件系统中以便以后使用。

                3、FIFO 有名字,不相关的进程可以通过打开有名管道进行通信。

2.15.2 有名管道的使用

通过命令创建有名管道:

        mkfifo 名字

通过函数创建有名管道:

        #include <sys/types.h>

        #include <sys/stat.h>

        int mkfifo (const char *pathname, mode_t mode);

一旦使用 mkfifo 创建了一个 FIFO,就可以使用 open 打开它,常见的文件I/O函数都可以用于FIFO。如:close、read、write、unlink等。

FIFO 严格遵循先进先出,对管道及FIFO的读总是从开始处返回数据,对它们的写则是把数据添加到末尾。它们不支持 lseek() 等文件定位(因为FIFO读数据后数据就会没有)操作。

step1.用函数创建一个有名管道(mkfifo.c)
/*
    通过函数创建fifo
    #include <sys/types.h>
    #include <sys/stat.h>
    int mkfifo(const char *pathname, mode_t mode);

        参数:
            pathname -- 管道名称的路径
            mode -- 文件的权限(和open的mode是一样的),是一个八进制的数
        fifo的mode也会和umask取反做与运算(mode & ~umask)
        返回值:
            0 -- 成功
            -1 -- 失败,并设置错误号
*/


#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#include <stdlib.h> //exit()
#include <unistd.h>


int main(){

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

    return 0;
}
step2. 往管道里写数据(write.c)
#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#include <stdlib.h> //exit()
#include <unistd.h>
#include <fcntl.h>
#include <string.h>

//向管道中写数据
/*
    有名管道的注意事项:
        1. 一个为只读而打开一个管道的进程会阻塞,直到另一个进程为只写打开管道。
        2. 一个为只写而打开一个管道的进程会阻塞,直到另一个进程为只读打开管道。
    读管道:
        管道中有数据,read返回实际读到的字节数
        管道中无数据:
            管道写端被全部关闭,read返回0,(相当于读到文件末尾)
            写端未被全部关闭,read阻塞等待
    写管道:
        管道读端被全部关闭,进行异常终止(收到一个SIGPIPE信号)
        管道读端没有全部关闭:
            管道已经满了,write会阻塞
            管道没有满,write将数据写入并返回实际写入的字节数
*/
int main(){

    //1 判断文件是否存在
    int ret = access("test", F_OK);
    if(ret == -1){
        printf("管道不存在,创建管道\n");
        
        //2 创建管道文件
        ret = mkfifo("test", 0664);

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

    //3 以只写的方式打开管道
    int fd = open("test", O_WRONLY);
    if(fd == -1){
        perror("open");
        exit(0);
    }

    //写数据
    for(int i = 0; i < 100; i++){
        char buf[1024];
        sprintf(buf, "hello, %d\n", i); //C标准库中,往buf中写入数据“……”
        printf("write data: %s\n", buf); //打印刚刚写入的数据
        write(fd, buf, strlen(buf)); //写入有名管道
        sleep(1);
    }
            
    close(fd);

    return 0;
}
step3. 从管道中读取数据(read.c)
#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#include <stdlib.h> //exit()
#include <unistd.h>
#include <fcntl.h>
#include <string.h>

//从管道中读取数据

int main(){

    //1 打开管道文件
    int fd = open("test", O_RDONLY);
    if(fd == -1){
        //若打开文件失败,则直接退出
        perror("open");
        exit(0);
    }

    // 读数据
    while(1){
        char buf[1024] = {0};
        read(fd, buf, sizeof(buf));
        if(ret == 0){
            printf("写端断开连接了……\n");
            break;
        }
        printf("recv buf : %s\n", buf);
    }
    
    close(fd);
    

    return 0;
}

若只打开读/写端(在step2中有具体注释):

        若没有读端只有写端,write会阻塞。等读端运行写端也会开始运行。等读端关闭,写端如果不关闭的话会导致管道破裂,所以会收到信号SIGPAi立刻终止进程;

        若没有写端只有读端,read函数会阻塞。等写端开始运行读端也会开始运行。等写端关闭,ret会变为0,读端执行close(fd)关闭文件,结束进程。

2.16 有名管道实现简单版聊天功能

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

int main(){

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

    ret = access("fifo2",F_OK);
    
    if(ret == -1){
        printf("未找到fifo2,创建管道\n");
        ret = mkfifo("fifo2", 0664);
        if(ret == -1){
            perror("mkfifo");
            exit(0);
        }
    }


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



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

    char buf[128];    

    //4. 循环写读数据
    while(1){
        memset(buf, 0, 128); //清空原先的数据
        //获取标准输入的数据
        fgets(buf, 128, stdin);
        //写数据
        ret = write(few, 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");
            break;
        }
        printf("buf: %s\n",buf);
    }

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

    return 0;

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

int main(){

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

    ret = access("fifo2",F_OK);
    
    if(ret == -1){
        printf("未找到fifo2,创建管道\n");
        ret = mkfifo("fifo2", 0664);
        if(ret == -1){
            perror("mkfifo");
            exit(0);
        }
    }


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



    //3. 以只读的方式打开管道2
    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);
        ret = read(fdr, buf, 128);
        if(ret <= 0){
            perror("read");
            break;
        }
        printf("buf: %s\n",buf);


        memset(buf, 0, 128); //清空原先的数据

        //获取标准输入的数据
        fgets(buf, 128, stdin);

        //写数据
        ret = write(few, buf, strlen(buf));
        if(ret == -1){
            perror("write");
            exit(0);
        }
    }

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

    return 0;

}

该代码只能实现“发送一次接受一次”的功能,如果A发2条信息的话,是无法收到第二条的(保存在缓冲区中),因为read在阻塞。等B发送完信息后,A的第二条会和第三条一起发送给B。

2.17 内存映射(1)

2.17.1 内存映射概述

        内存映射(Memory-mapped I/O)是将磁盘文件的数据映射到内存,用户通过修改内存就能修改磁盘文件

左边为进程的虚拟地址空间,虚拟地址空间会对应实际内存。文件映射到内存当中时,可以设置只映射某一部分去内存中去:off = offset偏移量,len为长度

 2.17.2 内存映射相关系统调用

#include <sys/mman.h>

void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset); //映射一个文件到内存当中

int munmap(void *addr, size_t length); //释放映射内存

2.17.3 案例:完成进程间的通信

1. 创建一个txt文件

        如,创建一个"test.txt"文件,内容为“hello,world”

2. mmap-parent-child-ipc.c

/*
    #include <sys/mman.h>

    void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset); //映射一个文件到内存当中
        功能:映射一个文件到内存当中
        参数:
            -void *addr:NULL,由内核决定
            -size_t length:要映射的数据的长度,这个值不能为0。建议使用文件的长度(会给一个分页的整数倍)
                获取文件长度: stat、lseek
            -int prot:对申请的内存映射区的操作权限
                PROT_EXEC 可执行的权限
                PROT_READ 读权限
                PROT_WRITE 写权限
                PROT_NONE 没有权限
                要操作映射内存,必须要有读的权限
            -int flags: 
                MAP_SHARED 映射区的数据会自动和磁盘文件进行同步,进程间通信必须要设置这个选项
                MAP_PRIVATE 不同步,内存映射区的数据改变了,对原来的文件不会修改,会重新创建一个新的文件(copy on write)
            -int fd:需要映射的那个文件的文件描述符
                通过open得到,open是一个磁盘文件
                    注意:文件的大小不能为0,并且open指定的权限不能和prot参数有冲突
                         prot的权限必须要小于open的权限
            -off_t offset:偏移量,一般不用。必须指定的是4K的整数倍。0表示不偏移(从开头开始操作)
        返回值:
            成功 -- 创建好的内存的首地址
            失败 -- 返回MAP_FAILED(其实就是(void *)-1)


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

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

    注意: 内存映射区是不会阻塞的(非阻塞)
*/

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


int main(){

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

    //2. 创建内存映射区
    void *ptr = mmap(NULL, size, PROT_READ | PROT_WRITE< MAP_SHARE, fd, 0);
    if(ptr == MAP_FAILED){
        perror("mmap");
        exit(0);
    }

    //3. 创建子进程
    pid_d pid = fork();
    if(pid > 0) {
        wait(NULL);
        //父进程写数据
        char buf[64];
        strcpy(buf, (vhar *)ptr);
        printf("read data : %s\n", buf);
        
    }else if(pid == 0){
        //子进程读数据
        strcpy((char *)ptr, "nihao a, son!!!");
    }

    // 关闭内存映射区
    munmap(ptr, size);

    return 0;
}

2.18 内存映射(2)

2.18.1 内存映射的注意事项:

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

//可以进行++操作
void * ptr = mmap(...);
ptr++;
//但是不建议,因为之后还要用munmap释放
munmap(ptr, len); //此时会报错;除非保存了mmap的地址再释放

2. 如果open时 O_RDONLY,mmap时prot参数指定PROT_READ | PROT_WRITE会怎样?

        错误,会返回宏MAP_FAILED(-1转换成(void *))

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

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

        偏移量必须是4K的整数倍,返回MAP_FAILED。

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

        - 第二个参数:length = 0

        - 第三个参数:prot

                - 只定义了写权限

                - prot PROT_READ | PROP_WRITE

                        第五个参数fd 通过open函数指定的 O_RDONLY 或者 O_WRONLY

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

        可以。但是创建的文件的大小如果为0的话,肯定不行

        可以对新的文件进行拓展

                - lessk()

                - truncate()

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

        映射区在关闭创建映射区的文件描述符fd后还是存在的(close(fd));

        通过munmap去释放

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

如,
    void *ptr = mmap(NULL, 100, ..., ...); //创建一个映射区,映射100个数据
    //不会只映射100个字节。会以分页的大小去分配,每个系统都不同:若本系统为4K
越界操作 操作的是非法的内存(野内存) -> 段错误

2.18.2 使用内存映射实现文件拷贝的功能 

// 使用内存映射实现文件拷贝的功能
/*
    1. 对原始的文件进行内存映射
    2. 创建一个新文件(拓展该文件)
    3. 把新文件的数据映射到内存中
    4. 通过内存拷贝将第一个文件的内存数据拷贝到新的文件
    5. 释放资源

*/

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


int main(){
    //1.对原始文件进行映射(如,此处用的english.txt)
    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(fd == -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;
}

        内存映射不适合拷贝太大内存的数据。因为会占用一个很大的空间。所以内存映射一般不用于拷贝数据。

        2.18.3 匿名映射

mmap_anon.c

/*
    匿名映射:不需要文件实体进行一个内存映射
    只能做父子进程之间的通信
    只需要将mmap函数的flag参数更改
*/
#include <stdlib.h>
#include <stdio.h>
#include <sys/stat.h>
#include <unistd.h>
#include <sys/type.h>
#include <string.h>
#include <sys/mman.h>
#include <fcntl.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;
}

2.19 信号概述

2.19.1 信号的概念

        信号是Linux进程间通信的最古老的方式之一,是事件发生时对进程的通知机制,有时也称之为软件中断,它是在软件层次上对中断机制的一种模拟,是一种异步通信的方式。信号可以导致一个正在运行的进程被另一个正在运行的异步进程中断,转而处理某一个突发事件。

        发往进程的诸多信号,通常都是源于内核。引发内核为进程产生信号的各类事件如下:

                -对于前台进程,用户可以通过输入特殊的终端字符来给它发送信号。比如输入Ctrl+C通常会给进程发送一个中断信号。

                -硬件发生异常,即硬件检测到一个错误条件并通知内核,随即再由内核发送相应信号给相关进程。比如执行一条异常的机器语言指令,诸如被0除,或者引用了无法访问的内存区域。

                -系统状态变化,比如alarm定时器到期将引起SIGALRM信号,进程执行的CPU时间超限,或者该进程的某个子进程退出。

                -运行kill命令或调用kill函数。

        使用信号的两个主要目的:

                1. 让进程知道已经发生了一个特定的事情。

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

        信号的特点:

                -简单

                -不能携带大量信息

                -满足某个特定条件才能发送

                -优先级比较高

        查看系统定义的信号列表:

                kill -l

        前31个信号为常规信号,其余为实时信号

2.19.2 Linux信号一览表

2.19.3 信号的5种默认处理动作

        查看信号的详细信息:man 7 signal

        信号的5种默认处理动作:

                Term -- 终止进程

                Ign -- 当前进程忽略掉这个信号

                Core -- 终止进程,并生成一个Core文件:为了对程序的错误进行调试

#include <stdio.h>
#include <string.h>

int main(){

    char * buf;//未指向合法内存(初始化),是一个野内存
    
    strcpy(buf, "hello");

    return 0;
}

在Linux终端中,运行该程序会显示“段错误(核心已转储)”。
但是并没有生成core文件。需要设置:
输入ulimit -a  //-a参数可以展示出详细的参数,即我们可以对什么资源做限制
将core file size    (blocks, -c) 0
中的大小0改成1024: ulimit -c 1024

再重新运行.c文件,会显示“段错误(核心已转储)”,ls查看,并生成core文件
ll后会发现有一个a.out*的文件。
gdb a.out去调试该程序
core-file core 可以查看错误

                Stop -- 暂停当前进程

                Cont -- 继续执行当前被暂停的进程

        信号的几种状态:产生、未决、递达

        SIGKILL 和 SIGSTOP 信号不能被捕捉、阻塞或者忽略,只能执行默认动作

2.20 kill、raise、abort函数

#include <sys/type.h>

#include <signal.h>

int kill (pid_t pid, int sig); 

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

        参数:

                -pid:需要发送给进程的id

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

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

                        =-1 -- 将信号发送给每一个接受这个信号的进程

                        <-1 -- 这个pid=某个进程组的id取反

                -sig:需要发送的信号的编号/宏值,若为0则表示不发送任何信号

int raise (int sig);

        功能:给当前进程发送信号

        参数:

                sig -- 要发送的信号

        返回值:

                0 -- 成功

                -1(非0) -- 失败

        相当于 kill(getpid(), sig);

void abort (void);

        功能:发送SIGABRT信号给当前进程,杀死当前进程

        相当于 kill(getpid(), SIGABRT);

unsigned int alarm (unsigned int seconds);

2.21 alarm函数

#include <unistd.h>

unsigned int alarm (unsigned int seconds);

        功能:设置定时器(闹钟)。函数调用,开始倒计时,当倒计时为0的时候,函数会给当前的进程发送一个信号:SIGALARM

        参数:

                seconds -- 倒计时时长,单位:秒。

                                    如果参数为0,则定时器无效(不进行倒计时,也不会发送信号)

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

        返回值:

                之前没有定时器,返回0

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

//伪代码
alarm(10); -- 返回0(因为之前没有定时器)
过了1s……
alram(5); -- 刷新定时器,返回之前的定时器剩余的时间9

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

alram(100);  -->  该函数是不阻塞的
100s后会终止当前进程

案例:计算计算机一秒中能数多少个数

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

int main() {
    
    alarm(1);

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

    return 0;
}

该案例中:

        实际的时间 = 内核时间(系统调用) + 用户时间 + 消耗的时间(I/O等)

        进行文件的I/O操作是非常浪费时间的。

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

alarm和setitimer共享同一个定时器,即alarm的定时时间包含的是用户+系统内核的运行时间。

2.22 setitimer定时器函数

#include <sys/time.h>

int getitimer(int which, const struct itimerval *new_value, struct itimerval *old_value);

        功能:设置定时器(闹钟),可以去替代alarm函数。精度比alarm高(微秒),并能实现周期性的定时

        参数:

                witch -- 定时器是以什么标准的时间计时

                        ITIMER_REAL -- 真实时间。时间到达发送SIGALRM   (常用)

                        ITIMER_VIRTUAL -- 虚拟时间。保证用户执行时间。时间到达,发送SIGVTALR

                        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; //微秒
}

                *old_value -- 记录上一次的定时的时间参数。一般用不到就写NULL

        返回值:

                0 -- 成功

                -1 -- 失败,设置错误号

案例: 过3秒以后,每隔2秒定时一次 //没有设置信号捕捉,不太好发现信号

#include <stdio.h>
#include <sys/time.h>
#include <stdlib.h>

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("定时器开始了……");

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

    getchar(); //获取键盘录入,不然程序直接执行结束,看不出定时效果

    return 0;
}

2.23 signal信号捕捉函数 

#include <signal.h>

typedef void (*sighandler_t)(int);

sighandler_t signal (int signum, sighandler_t handler);

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

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

        参数:

                signum -- 要捕捉的信号

                handler -- 捕捉到信号要如何处理(处理动作)

                        SIG_IGN -- 忽略信号

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

                        回调函数 -- 内核(系统)调用,程序员只负责写,捕捉到信号后如何去处理信号

【回调函数】是需要程序员实现,提前准备好的,函数的类型根据实际需求,看函数指针的定义;不是程序员调用,而是当信号产生由内核调用;函数指针是实现回调的手段,函数实现之后,将函数名放到函数指针的位置就可以了。

        返回值:

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

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

        说明:SIGKILL SIGSTOP不能被捕捉,不能被忽略

案例改进(2.22):使信号能够捕捉

#include <stdio.h>
#include <sys/time.h>
#include <stdlib.h>
#include <signal.h>

//回调函数设置
void myalarm(int num) {
    printf("捕捉到了信号的标号是:%d\n",num);
    printf("xxxxxxxxxx\n");
}

//延迟3秒后,每隔2秒定时一次
int main() {

    //注册信号捕捉 -- 在信号开始前先注册
    //signal(SIGALRM, SIG_IGN);//忽略该信号捕捉
    //signal(SIGALRM, SIG_DFL);//默认终止进程
    //设置回调函数 typedef void (*sighandler_t)(int);函数指针 int类型的参数表示的捕捉到的信号的值
    sighandler_t ret = signal(SIGALRM, myalarm);//回调函数

    if(ret == SIG_ERR) {
        perror("signal");
        exit(0);
    }

    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("定时器开始了……");

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

    getchar(); //获取键盘录入,不然程序直接执行结束,看不出定时效果

    return 0;
}

2.24 信号集及相关函数

2.24.1 信号集

        许多信号相关的系统调用都需要能表示一组不同的信号,多个信号可使用一个称之为信号集的数据结构来表示,其系统数据类型为 sigset_t

        在 PCB 中有两个非常重要的信号集。一个称之为“阻塞信号集”,另一个称之为“未决信号集”。这两个信号集都是内核使用位图机制来实现的。但操作系统不允许我们直接对这两个信号集进行位操作。而需要自定义另一个集合,借助信号集操作函数来对 PCB 中的这两个信号集进行修改

        未决信号集 不能被我们设置,只能读;阻塞信号集可以被我们设置

        信号的“未决”是一种状态,指的是从信号的产生到信号被处理前的这一段时间(未递达)

        信号的“阻塞”是一个开关动作,指的是阻止信号被处理,但不是阻止信号产生(开关动作)

        信号的阻塞就是让系统暂时保留信号留待以后发送。由于另外有办法让系统忽略信号,所以一般情况下信号的阻塞只是暂时的,只是为了防止信号打断敏感的操作

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

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

2. 信号产生没有被处理(未决状态)

        - 在内核中将所有未被处理的信号存储在一个集合中(未决信号集)

        - SIGINT信号状态被存储在第二个标志位

                0 -- 信号不是未决状态(已处理)

                1 -- 信号处于未决状态(未处理)

3. 未决状态的信号需要被处理,处理之前需要和另一个信号集(阻塞信号集)进行比较:若阻塞信号集对应标志位值为0,则能够去处理;若阻塞信号集对应标志位为1,则说明要阻塞,信号会被阻塞在这里不被处理,继续处于未决状态,直到阻塞接触

        - 阻塞信号默认不阻塞任何信号

        - 如果想要阻塞某些信号需要用户调用系统的API 

4. 在处理的时候和阻塞信号集中的标志位进行查询,看是否对该信号设置阻塞了

2.24.3 信号集相关的函数

以下信号集相关的函数都是对自定义的信号集进行操作:

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, sigset_t *oldset);

        功能:判断某个信号是否阻塞

        参数:

                set -- 需要操作的信号集

                signum -- 需要判断的信号

        返回值:

                1 -- 表示signum被阻塞

                0 -- signum不阻塞

                -1 -- 失败

对系统中的信号集进行操作的函数:

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

int sigpending (sigset_t *set);

#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是否在信号集中
    int ret = sigismember(&set, SIGINT);
    if(ret == 0) {
        printf("SIGINT, 不阻塞\n");
    } else if(ret == 1) {
        printf("SIGINT, 阻塞\n");
    }

    //从信号中删除一个信号
    sigdelset(&set, SIGINT);

    //判断SIGQUIT是否在信号集中
    int ret = sigismember(&set, SIGQUIT);
    if(ret == 0) {
        printf("SIGQUIT, 不阻塞\n");
    } else if(ret == 1) {
        printf("SIGQUIT, 阻塞\n");
    }

    return 0;
}

 运行结果如下:

2.25 sigprocmask函数使用

对系统中的信号集进行操作的函数:

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

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

        参数:

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

                        SIG_BLOCK -- 将用户设置的阻塞信号集添加到内核中,内核中原来的数据不变(假设内核中默认的阻塞信号集是mask,此处相当于 mask | set)

                        SIG_UNBLOCK -- 根据用户设置的数据,对内核中的数据进行解除阻塞(解除设置1,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);

    int num = 0; // 结束计时

    // 修改内核中的阻塞信号集
    sigprocmask(SIG_BLOCK, &set, NULL);

    while(1) {
        num++;
        // 获取当前未决信号集的数据
        sigset_t pendingset;
        sigemptyset(&pengdingset);
        sigpending(&pendingset);

        // 遍历前32位
        for(int i = 1; i <= 32; i++) {
            if(sigismember(&pendingset, i) == 1) {
                //未决
                printf("1");
            } else if(sigismember(&pendingset, i) == 0) {
                //非未决
                printf("1");
            } else {
                perror("sigismember");
                exit(0);
            }
        }

        printf("\n");
        sleep(1);

        if(num == 10) {
            //解除阻塞
            sigprocmask(SIG_UNBLOCK, &set, NULL);
        }
    }
    return 0;
}

在执行指令时输入&,则可以在后台运行(如:./a &)  -- 前台进程转后台进程 

2.26 sigaction信号捕捉函数

2.26.1 sigaction函数

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

        功能:检查或者改变信号的处理,信号捕捉。

        参数:

                signum -- 需要捕捉的信号的编号或者宏值

                act -- 捕捉到信号之后的处理动作

                oldact -- 上一次对信号捕捉相关的设置。一般不适用,NULL

        返回值:

                0 -- 成功

                1 -- 失败

sigaction结构体:

struct sigaction {
    void (*sa_handler)(int);//函数指针,指向的函数就是信号捕捉到之后的处理函数
    void (*sa_sigaction)(int, siginfo_t *, void *);//不常用
    sigset_t sa_mask;//信号集类型,设置临时阻塞信号集
    int sa_flags;//使用哪一个信号处理 对捕捉到的信号进行处理
        /*
            0 -- 表示使用sa_handler
            SA_SIGINFO -- 表示使用sa_sigaction
        */
    void (*sa_restorer)(void);//被废弃,使用NULL即可
}
#include <stdio.h>
#include <sys/time.h>
#include <stdlib.h>
#include <signal.h>

//回调函数设置
void myalarm(int num) {
    printf("捕捉到了信号的标号是:%d\n",num);
    printf("xxxxxxxxxx\n");
}

//延迟3秒后,每隔2秒定时一次
int main() {

    struct sigaction act;
    act.sa_flags = 0;// 表示用sa_handler处理
    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("定时器开始了……");

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

    //getchar(); //获取键盘录入,不然程序直接执行结束,看不出定时效果
    while(1){
        //死循环
    }

    return 0;
}

2.26.2 signal 和 sigaction

        尽量避免使用signal(ANSI C标准)

        sigaction在很多标准下都可以使用(POSIX C标准)

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

2.27 SIGCHLD信号

SIGCHLD信号产生的条件

        子进程终止时(最常出现)

        子进程接收到SIGSTOP信号时 -- 暂停

        子进程处在停止态,接受到SIGCONT后唤醒时 -- 继续运行

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

案例:用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的资源
    wait(NULL);
}

int main() {

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

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

        //捕捉子进程死亡时发送的SIGCHID信号
        struct sigaction act;
        act.sa_flags = 0;
        act.sa_handler = myFun;
        sigemptyset(&act.sa_mask);
        sigaction(SIGCHID, &act, 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;
}

        此时用该函数运行后还是会有很多僵尸进程,因为未决信号集在阻塞信号集中该信号为阻塞的时候(标志位=1),无法记录更多的未决信号(标志=1),所以会导致这个情况。解决办法为:在回调函数那里加入死循环来回收

无法处理所有的僵尸进程
能处理僵尸进程
但是死循环时父进程阻塞

将死循环中的wait函数改成waitpid函数
设置状态为WNOHANG,当==0时说明还有子进程活着

但是这样运行很容易产生错误:因为可能出现子进程已经运行完了,但是信号还没来得及注册(??????) -- 需要设置阻塞

改进后的代码如下 

#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){
        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() {

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

    //注册完信号捕捉以后,解除阻塞
    sigpromask(SIG_UNBLOCK, &set, NULL);

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

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

        //捕捉子进程死亡时发送的SIGCHID信号
        struct sigaction act;
        act.sa_flags = 0;
        act.sa_handler = myFun;
        sigemptyset(&act.sa_mask);
        sigaction(SIGCHID, &act, 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;
}

2.28 共享内存(1)

        共享内存允许两个或多个进程共享物理内存的同一块区域(通常被称为段)。由于一个共享内存段会成为一个进程用户空间的一部分,因此这种 IPC(进程间通信) 机制无需内核介入。所有需要做的就是让一个进程将数据复制进共享内存中,并且这部分数据会对其他所有共享同一段的进程可用。

        与管道等要求发送进程将数据从用户空间的缓冲区复制进内核内存和接受进程将数据从内核内存复制进用户空间的缓冲区的做法相比,这种 IPC 技术的速度更快。

2.28.1 共享内存的使用步骤

        调用 shmget() 创建一个新共享内存段 或 取得一个既有共享内存段标识符(即由其他进程创建的共享内存段)。这个调用将返回后续调用中需要用到的共享内存标识符。

        使用 shmat() 来附上共享内存段,即使该段成为调用进程的虚拟内存的一部分。

        此刻程序中可以像对待其他可用内存那样对待这个共享内存段。为引用这块共享内存,程序需要使用由 shmat() 调用返回的 addr 值,它是一个指向进程的虚拟地址空间中该共享内存段的起点的指针。

        调用 shmdt() 来分离共享内存段。在这个调用之后,进程就无法再引用这块共享内存了。这一步是可选的,并且在进程终止时会自动完成这一步。

        调用 shmctl() 来删除共享内存段。只有当当前所有附加内存段的进程都与之分离之后内存段才会销毁。只有一个进程需要执行这一步。

2.28.2 共享内存操作函数

#include <sys/ipc.h>

#include <sys/shm.h>

int shmget(key_t key, size_t size, int shmflg);

        功能:创建一个新的共享内存段,或者获取一个既有的共享内存段的标识

                新创建的内存段中的数据都会被初始化为0

        参数:

                key -- ket_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 shmad(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 -- 指定一个存在的路径

                proj_id --int类型的值,但是系统调用只会使用其中的一个字节

                        范围: 0-255 一般指定一个字符'a'

        返回值:

2.29 共享内存(2)

案例 -- 实现进程间通信:

        1创建共享内存;2获取共享内存

write_shm.c

#include <sys/ipc.h>
#include <sys/shm.h>
#include <stdio.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 = "hello world";

    // 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 <sys/ipc.h>
#include <sys/shm.h>
#include <stdio.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);

    char * str = "hello world";

    // 3.读数据
    printf("%s\n",(char *)ptr);

    //让程序停在第三步
    printf("按任意键继续\n");
    getchar();

    // 4.解除关联
    shmdt(ptr);

    // 5.删除共享内存
    shmctl(shmid, IPC_RMID, NULL); 

    return 0;
}

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

        共享内存维护了一个结构体struct shmid_ds 这个结构体中有一个成员 shm_nattach

        shm_nattatch 记录了关联的进程个数

2. 可不可以对共享内存进行多次删除 shmctl

        可以

        因为 shmctl 标记删除共享内存,不是直接删除

        什么时候真正删除?

                当和共享内存关联的进程数为0的时候,就被真正删除

        当共享内存为0的时候,标识共享内存被标记删除了

                如果一个进程和共享内存取消关联,那么这个进程就不能继续操作这个共享内存,也不能再次关联

3. 共享内存和内存映射的区别

        1)共享内存可以直接创建,内存映射需要磁盘文件(匿名映射除外)

        2)共享内存效果更高

        3)内存

                所有的进程操作的是同一块共享内存

                内存映射,每个进程在自己的虚拟地址空间中有一个独立的内存

        4)数据安全

                进程突然退出:共享内存还存在;内存映射区会消失

                运行进程的电脑死机,宕机了:共享内存中的数据存储在共享内存中,会没有了;内存映射区的数据由于磁盘文件中的数据还在,所以内存映射区的数据还存在

        5)生命周期

                内存映射区:进程退出,内存映射区销毁

                共享内存:进程退出,共享内存还在,标记删除(所有的关联的进程数为0),或者关机

                        如果一个进程退出,会自动和共享内存进行取消关联

2.30 守护进程(1)

2.30.1 终端

        在 UNIX 系统中,用户通过终端登录系统后得到一个 shell 进程,这个终端成为 shell 进程的控制终端(Contorlling Terminal),进程中,控制终端是保存在 PCB 中的信息,而 fork() 会复制 PCB 中的信息,因此 shell 进程启动的其它进程的控制终端也是这个终端。

        默认情况下(没有重定向),每个进程的标准输入、标准输出和标准错误输出都指向控制终端,进程从标准输入读也就是读用户的键盘输入,进程往标准输出或标准错误输出也就是输出到显示器上。

        在控制终端输入一些特殊的控制键可以给前台进程发信号,例如 Ctrl+C 会产生 SIGINT 信号,Ctrl+\ 会产生 SIGQUIT 信号。

2.30.2 进程组

        进程组和会话在进程之间形成了一种两级层级关系:进程组是一组相关进程的集合,会话是一组相关进程组的集合。进程组和会话是为了支持 shell 作业控制而定义的抽象概念,用户通过 shell 能够交互式地在前台或后台运行命令。

        进行组由一个或多个共享同一个进程标识符(PGID)的进程组成。一个进程组拥有一个进程组首进程,该进程是创建该组的进程,其进程ID 为该进程组的ID,新进程会继承其父进程所属的进程组ID。

        进程组拥有一个生命后期,其开始时间为首进程创建组的时刻,结束时间为最后一个成员进程退出组的时刻。一个进程可能会因为终止而退出进程组,也可能会因为加入了另外一个进程组而退出进程组。进程组首进程无需是最后一个离开进程组的成员。

2.30.3 会话

        会话是一组进程组的集合。会话首进程是创建该新会话的进程,其进程ID会成为会话ID。新进程会继承其父进程的会话ID。

        一个会话中的所有进程共享单个控制终端。控制终端会在会话首进程首次打开一个终端设备时被建立。一个终端最多可能会成为一个会话的控制终端。

        在任一时刻,会话中的其中一个进程组会成为终端的前台进程组,其他进程会成为后台金层组。只有前台进程组中的进程才能从控制终端读取输入。当用户在控制终端中输入终端字符生成信号后,该信号会被发送到前台进程组中的所有成员。

2.30.4 进程组、会话、控制终端之间的关系

案例:两个命令分别在计算机中开辟了哪些位置放他们

2.30.5 进程组、会话操作函数

pid_t getpgrp(void); // 获取当前进程组的组id

pid_t getpgid(pid_t pid); // 获取传入的pid的进程的进程组

int setpgid(pid_t pid, pid_t pgid); // 创建该进程组的id

pid_t getsid(pid_t pid); // 获取指定进程的会话id

pid_t setsid(void); // 设置会话id

2.30.6 守护进程

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

        守护进程具备下列特征:

                -生命周期长。守护进程会在系统启动的时候被创建并一直运行直至系统被关闭。

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

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

2.31 守护进程(2)-- 写守护进程

2.31.1 守护进程的创建步骤

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

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

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

清除进程的当前工作目录,通常会改为根目录( / );

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

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

核心业务逻辑

 2.30.2 创建一个守护进程

daemon.c

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

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

void work(int num) {
    // 捕捉到信号之后,获取系统时间,写入磁盘文件
    time_t tm = time(NULL);
    struct tm = localtime(&tm);
    /*
    方式1
    char buf[1024];
    
    sprintf(buf, "%d-%d-%d %d:%d:%d\n", loc->tm_year, loc->tm_mon, loc->tm_mday, loc->tm_hour, loc->tm_min, loc->tm_sec);

    printf("%s\n", buf); 
    */
    // 方式2:此时需要将步骤4的目录改成用户目录
    char * str = asctime(loc);// 会返回一个字符串格式
    int fd = open("time.txt", O_RDWR | O_CREAT | O_APPEND, 0664);
    write(fd, str, strlen(str));
    close(fd);    
}

int main() {
    
    // 1.创建子进程,退出父进程
    pid_t pid = fork();
    
    if(pid > 0){
        exit(0);
    }

    // 2.将子进程重新创建一个会话
    setsid();

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

    // 4.更改工作目录
    chdir("/");

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

    //6. 业务逻辑:每隔2s获取一下系统时间,将这个时间写入到磁盘文件中
    
    //捕捉定时信号
    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;
}

  • 43
    点赞
  • 43
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值