Linux多进程开发

一、进程概述

1、程序与进程

程序是编译过的、可执行的二进制代码;进程是指正在运行的程序,可以用一个程序来创建多个进程。

2、单道、多道程序设计

单道程序:计算机内存中只允许一个程序运行

多道程序:同时运行多个程序,提升CPU的利用率。宏观上CPU上同时运行,但是微观上,任意时刻CPU上运行的程序只有一个

3、时间片

4、并行和并发

并行:指同一时刻,有多条指令在多个处理器上同时执行

并发:指同一时刻只能有一条指令执行,但多个进程指令快速的轮换执行,是的在宏观上具有多个进程同时执行的效果。

 5、进程控制块(PCB)

为了管理进程,内核为每个进程分配了一个PCB进程控制块维护进程相关信息,LInux内核的进程块是task_struct结构体。内部成员如下:

 二、进程状态转换

1、进程状态

进程状态反映进程执行过程的变化,其随着进程的执行和外界条件的变化而转换。在三态模型中为:就绪态、运行态、阻塞态,在五态模型中:新建态、就绪态、运行态、阻塞态、终止态

 

 2、进程相关命令

ps aux / ajx

-a:显示终端上的所有进程

-u:显示进程的详细信息

-x:显示没有控制终端的进程

-j:列出与作业控制相关的信息

 top

实时显示进程动态,可以使用按键对显示的结果进行排序

 kill

杀死进程

 3、进程号和相关函数

每个进程都由进程号来表示,其类型为pid_t整型

任何进程都是有另一个进程创建的,该进程称为被创建进程的父进程

进程号相关的函数

 三、进程创建

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

pid_t fork(void);

作用:用于创建子进程,在父进程中执行

返回值:fork的返回值会返回两次,一次是在父进程中,一次是在子进程中。在父进程中返回创建子进程的id,在子进程中返回0 。可以通过fork的返回值来区分父子进程。

在父进程中返回-1表示创建子进程失败,并且设置errno

创建进程失败的原因:1)进程数达到上限,2)系统内存不足

示例代码:

#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) {//当前是子进程
        printf("i am child process, pid : %d, ppid : %d\n", getpid(), getppid());
    }

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

    return 0;
}

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

调用fork函数之后,创建的子进程的用户区数据和父进程一样,内核区的数据也一样,除了pid的值

 写时复制:其核心思想是,如果有多个调用者(callers)同时请求相同资源(如内存或磁盘上的数据存储),他们会共同获取相同的指针指向相同的资源,直到某个调用者试图修改资源的内容时,系统才会真正复制一份专用副本(private copy)给该调用者,而其他调用者所见到的最初的资源仍然保持不变。

父子进程的关系:

区别:1)fork()函数的返回值不同:父进程中:pid>0返回的子进程的id,子进程中为0

           2)pcb中的一些数据:进程id,信号集

共同点:某些状态下:子进程刚被创建出来,没有赋值操作时,用户区的数据,文件描述符表等一样

父子进程的变量是否共享?

刚开始的时候是共享的,如果有赋值操作则不共享了(读时共享,写时拷贝)

五、GDB多进程调试

GDB默认调试父进程的代码

查看调试是跟踪父进程还是子进程: show follow-fork-mode

设置调试子进程:set follow-fork-mode child

设置调试模式:set detach-on-fork on/off,设置为on表示调试当前进程时,其他进程继续运行;设置为off时,表示调试当前进程时,其他进程被GDB挂起

查看调试的进程:info inferiors

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

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

六、exec函数族

作用:根据只当的文件名或者路径知道可执行文件,并用它来取代调用进程的内容,换句话说就是在调用进程内部执行一个可执行程序 。

exec函数执行成功后不会返回,因为调用进程的实体已被新的内容取代,只留下进程id等保持原样。调用失败返回-1,从源程序的调用点接着往下执行。

#include <unistd.h>
int execl(const char *pathname, const char *arg, .../* (char  *) NULL */);

参数:pathname:需要制定的执行的文件的路径或者名称,推荐使用绝对路径

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

返回值:只有当调用失败时才会有返回值-1,并设置errno

示例代码

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


int main()
{
    pid_t pid = fork();
    if (pid > 0) {
        //这是父进程
        printf("Parent process, pid:%d\n", getpid());
        sleep(1);
    } else if (pid == 0) {
        //这是子进程
        printf("Child process, pid:%d\n", getpid());
        execl("hello", "hello", NULL);
        printf("Child process, pid:%d\n", getpid());
    }

    for (int i = 0; i < 3; i++) {
        printf("i = %d, pid = %d\n", i, getpid());
    }
    return 0;
}
#include <unistd.h>
int execlp(const char *file, const char *arg, .../* (char  *) NULL */);

execlp函数会到环境变量中查找指定的可执行文件,如果找到了就执行,找不到就执行失败

参数:file:可执行文件的文件名

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

返回值:只有当调用失败时才会有返回值-1,并设置errno。

示例代码:

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


int main()
{
    pid_t pid = fork();
    if (pid > 0) {
        //这是父进程
        printf("Parent process, pid:%d\n", getpid());
        sleep(1);
    } else if (pid == 0) {
        //这是子进程
        printf("Child process, pid:%d\n", getpid());
        //execl("hello", "hello", NULL);
        //需要指定路径
        //execl("/bin/ps", "ps", "a", "u", "x", NULL);
        //不需要指定路径
        execlp("ps", "ps", "aux", NULL);
        printf("Child process, pid:%d\n", getpid());
    }

    for (int i = 0; i < 3; i++) {
        printf("i = %d, pid = %d\n", i, getpid());
    }
    return 0;
}
#include <unistd.h>
int execv(const char *path, const char *arg, .../* (char  *) NULL */);

参数:path:文件路径

           argv:需要的参数的一个字符串数组

示例代码:

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


int main()
{
    pid_t pid = fork();
    if (pid > 0) {
        //这是父进程
        printf("Parent process, pid:%d\n", getpid());
        sleep(1);
    } else if (pid == 0) {
        //这是子进程
        printf("Child process, pid:%d\n", getpid());
        //execl("hello", "hello", NULL);
        //需要指定路径
        //execl("/bin/ps", "ps", "a", "u", "x", NULL);
        //不需要指定路径
        //execlp("ps", "ps", "aux", NULL);
        //参数为字符串数组
        char * argv[] = {"ps", "ps", "aux", NULL};
        execv("/bin/ps", argv);
        printf("Child process, pid:%d\n", getpid());
    }

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

总得来说,exec函数族主要区别是末尾得字母得区别:

l(list):参数地址列表,以空指针结尾

v(vector):存有各参数地址的指针数组的地址

p(path):按PATH环境变量指定的目录搜索可执行文件

e(environment):存有环境变量字符串地址的指针数组的地址

七、进程控制

1、进程退出,一般使用exit()函数

#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);//会打印hello world
    _exit(0);//只会打印hello

    return 0;
}

2、孤儿进程

父进程运行结束时,但子进程还在运行(未运行结束),这样的进程称为孤儿进程(orphan process),内核就把孤儿进程的父进程设置为init,而init进程会循环地wait()它的已经退出的子进程。孤儿进程并不会有什么危害。

3、僵尸进程

 一个子进程在父进程还没有调用wait()方法或者waitpid()方法的情况下退出,这个子进程就是僵尸进程;

每个进程结束之后,都会释放自己地址空间中的用户数据,内核区PCB没有办法自己释放掉,需要父进程区释放。当进程终止时,父进程尚未回收,子进程残留资源(PCB)存放于内核中会变成僵尸进程。僵尸进程不能被kill -9命令杀死,如果父进程不调用wait活waitpid函数的话,那么子进程的残留资源就不会被释放,其进程号会被一直占用,但是系统所能使用的进程号是有限的,如果有大量僵尸进程,系统将因为没有可用的进程号而导致无法产生新的进程,这就是僵尸进程的危害。

 4、进程回收

在每个进程退出的时候,内核会释放该进程的所有资源,但是任然会为其保留一定的信息,主要是进程控制块PCB的信息(进程号,退出状态,运行时间等)。

父进程可以调用wait()和waitpid()函数来得到子进程的退出状态,同时彻底清除这个进程。

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

一次调用wait和waitpid函数只能清除一个进程,清理多个进程需要使用循环。

5、wait函数

#include <sys/types.h>
#include <sys/wait.h>

pid_t wait(int *wstatus);

功能:等待任意一个子进程结束,如果任意一个子进程结束,此函数会回收子进程的资源。

参数:wstatus:进程退出时的状态信息,传入的是一个int*类型,传出参数

返回值:成功返回被回收的子进程的id,失败返回-1

调用wait函数进程会被挂起(阻塞),直到它的一个子进程退出或者收到一个不可以被忽略的信号,才会被唤醒继续执行。如果没有子进程了,函数立刻返回,返回-1;如果子进程都结束了返回-1。

示例代码:

#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.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 st;
            int ret = wait(&st);
            if (ret == -1) {
                break;
            }
            if (WIFEXITED(st)) {
                //是不是正常退出
                printf("退出的状态码:%d\n",WEXITSTATUS(st));
            }
            if (WIFSIGNALED(st)) {
                //是不是异常终止
                printf("被哪个信号杀死了:%d", WTERMSIG(st));
            }
            printf("child die, pid = %d\n", getpid());
            sleep(1);
        }
    } else if (pid == 0) {
        // while (1) {
        //     printf("child, pid:%d\n", getpid());
        //     sleep(1);
        // }
        printf("child, pid:%d\n", getpid());
        sleep(1);
        exit(0);
        
    }
 
    return 0;
}

6、退出信息相关宏函数

 7、waitpid函数

#include <sys/types.h>
#include <sys/wait.h>

pid_t waitpid(pid_t pid, int *wstatus, int options);

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

 参数:pid:>0:某个子进程的id

                    =0:回收当前进程组的子进程

                    =-1:表示回收所有的子进程,相当于wait()函数(最常用的

                    <-1:回收某个进程组的组id的绝对值,回收指定进程组中的子进程

             wststus:进程退出时的状态信息,传入的是一个int*类型,传出参数

             options:设置阻塞或者非阻塞,0表示阻塞,WNOHANG表示非阻塞

返回值:>0:返回子进程的id

               =0:options = WNOHANG,表示还有子进程

                = -1:错误,没有子进程

 八、进程间通信(IPC)

1、进程通信

进程是一个独立的资源分配单元,不同的进程之间时独立的,不能在一个进程中直接访问另一个进程的资源。但是进程不是孤立的,需要进程之间进行信息的交互和状态的传递。

进程间通信的目的:

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

2)通知事件:一个进程需要向另一个或者一组进程发送消息,通知发生了某种事件。

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

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

 2、Linux进程通信的方式

 3、匿名管道

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

例子:统计目录中的文件数量,ls | wc -l

 4、管道的特点

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

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

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

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

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

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

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

 

 5、管道的数据结构

6、匿名管道的使用 

1)pipe函数

#include <unistd.h>

int pipe<int pipefd[2]);

 功能:创建匿名管道,用来进行进程间通信

参数:int pipefd[2]这个数组是一个传出参数,pipefd[0]对应管道的读端,pipefd[1]对应管道的写端

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

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

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

示例:创建一个子进程,并从子进程发送数据给父进程输出

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


//子进程给父进程发送数据,父进程读取到数据输出
int main() {
    //在fork之前创建管道
    int pipefd[2];
    int ret = pipe(pipefd);
    if (ret == -1) {
        perror("pipe");
        return -1;
    }

    //创建子进程
    pid_t pid = fork();
    if (pid > 0) {
        //父进程
        //从管道的读取段读取数据
        char buf[1024] = {0};
        int len = read(pipefd[0], buf, sizeof(buf));
        printf("parent recv : %s, pid : %d\n", buf, getpid());

    } else if (pid == 0) {
        //子进程
        //写数据
        char * str = "hello, i am child!";
        write(pipefd[1], str, strlen(str));

    }
    return 0;
}

2)查看管道缓冲的大小

ulimit -a

3)查看管道缓冲大小的函数

#include <unistd.h>

long fpathconf(int fd, int name);

 示例代码:子线程调用ps aux命令,将数据通过管道传输给主线程,并输出

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

int main() {
    //创建匿名管道
    int fd[2];
    int ret = pipe(fd);
    if (ret == -1) {
        perror("pipe");
        return -1;
    }
    //创建子进程
    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\n", buf);
            memset(buf,0,1024);
        }
        wait(NULL);

         
    } else if (pid == 0) {
        //子进程
        //关闭读端
        close(fd[0]);
        //文件描述符重定向 tsdout_fileno->fd[1]
        dup2(fd[1], STDOUT_FILENO);
        execlp("ps", "ps", "aux", NULL);
        //执行ps aux


    } else {
        perror("fork");
        return -1;
    }
    return 0;
}

7、管道的读写特点

使用管道时需要注意以下情况(假设都是i阻塞I/O操作):

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

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

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

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

总结:

读管道:1)管道中有数据,read会返回实际读到的字节数

               2)管道中无数据,写端全部关闭,read返回0,相当于读到文件的末尾;写端没有完全关闭,read阻塞等待

写管道:1)管道的读端全部关闭,进程异常终止(收到SIGPIPE)

               2)读端没有全部关闭:管道已满:write阻塞;管道没有满,write将数据写入,并返回实际写入的字节数。

示例,设置管道非阻塞

#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
    flags |= O_NONBLOCK;            // 修改flag的值
    fcntl(fd[0], F_SETFL, flags);   // 设置新的flag
*/
int main() {

    // 在fork之前创建管道
    int pipefd[2];
    int ret = pipe(pipefd);
    if(ret == -1) {
        perror("pipe");
        exit(0);
    }

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

        // 关闭写端
        close(pipefd[1]);
        
        // 从管道的读取端读取数据
        char buf[1024] = {0};

        int flags = fcntl(pipefd[0], F_GETFL);  // 获取原来的flag
        flags |= O_NONBLOCK;            // 修改flag的值
        fcntl(pipefd[0], F_SETFL, flags);   // 设置新的flag

        while(1) {
            int len = read(pipefd[0], buf, sizeof(buf));
            printf("len : %d\n", len);
            printf("parent recv : %s, pid : %d\n", buf, getpid());
            memset(buf, 0, 1024);
            sleep(1);
        }

    } else if(pid == 0){
        // 子进程
        printf("i am child process, pid : %d\n", getpid());
        // 关闭读端
        close(pipefd[0]);
        char buf[1024] = {0};
        while(1) {
            // 向管道中写入数据
            char * str = "hello,i am child";
            write(pipefd[1], str, strlen(str));
            sleep(5);
        }
        
    }
    return 0;
}


8、有名管道

1)为克服匿名管道只能用于亲缘关系的进程间的通信的问题,提出了有名管道(FIFO)

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

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

4)有名管道和匿名管道的区别:FIFO在文件系统中作为一个特殊文件存在,但FIFO中的内容却存放在内存中;当使用FIFO的进程退出后,FIFO文件将继续保存在文件系统中以便以后使用;FIFO有名字,不相关的进程可以通过打开有名管道进行通信。

 9、有名管道的使用

1)通过命令创建:mkfifo name

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

3)FIFO 严格遵循先进先出(First in First out),对管道及 FIFO 的读总是 从开始处返回数据,对它们的写则把数据添加到末尾。它们不支持诸如 lseek() 等文件定位操作。

4)通过函数创建有名管道

#include <sys/types.h>
#include <sys/stat.h>

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

参数:pathname:管道名称的路径

           mode:文件的权限,和open函数的参数中的mode一样

返回值:成功返回0,失败返回-1并设置错误号

示例代码,创建有名管道,相互通信

write

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <stdlib.h>
#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);
        printf("write data : %s\n", buf);
        write(fd, buf, strlen(buf));
        sleep(1);
    }

    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("test", O_RDONLY);
    if(fd == -1) {
        perror("open");
        exit(0);
    }

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

    close(fd);

    return 0;
}

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

    读管道:
        管道中有数据,read返回实际读到的字节数
        管道中无数据:
            管道写端被全部关闭,read返回0,(相当于读到文件末尾)
            写端没有全部被关闭,read阻塞等待
    
    写管道:
        管道读端被全部关闭,进行异常终止(收到一个SIGPIPE信号)
        管道读端没有全部关闭:
            管道已经满了,write会阻塞
            管道没有满,write将数据写入,并返回实际写入的字节数。

10、示例:有名管道实现简单聊天功能

 11、内存映射

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

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

1)mmap()

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

参数:void *addr:NULL,由内核指定

           length:要映射的数据的长度,不能为0,一般使用文件的长度,使用stat/lseek函数可以获取文件的长度。                        

            prot:对申请的内存映射区的操作权限,要操作映射区必须要读权限

                        PORT_EXEC:可执行的权限

                        PORT_READ:读的权限

                         PORT_WRITE:写权限

                         PORT_NONE:无权限

             一般使用PORT_READ、PORT_READ | PORT_WRITE

            flags:MAP_SHARED:映射区的数据会自动和磁盘文件进行同步,进程间通信必须要设置这个选项

                        MAP_PRIVATE:不同步,内存映射区的数据对原来的数据不会修改,底层重新创建新的文件(写时复制)

             fd:需要映射的那个文件的文件描述符,通过open打开文件得到,文件大小不能为0,open指定的权限不能和port的参数冲突

             offset:偏移量,一般不用,必须指定的时4k的整数倍,0表示不偏移

返回值:返回创建的内存的首地址,失败返回MAP_FAILED(就是-1强制转换为void*类型)

2)munmap

功能:释放内存映射

参数:void *addr:要释放内存的首地址

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

示例代码,通过内存映射实现进程间的通信

12、内存映射的注意事项

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

可以对ptr进行++操作,但是不建议,因为在释放的时候会出现错误

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

会产生错误,返回宏MAP_FAILED,open函数中的权限建议与prot的参数保持一致

◼ 如果文件偏移量为1000会怎样?

偏移量必须是4k的整数倍,不是的话返回错误

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

1)第二个参数,length = 0

2)第三个参数,prot只指定了写权限,或者指定的权限与open指定的权限有冲突

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

可以的,创建的文件大小如果为0则错误,可以对新的文件进行拓展,使用lseek,truncate函数等

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

没有什么影响

◼ 对ptr越界操作会怎样?

越界操作,操作的是非法的内存,会出现段错误

示例代码,使用内存映射拷贝文件

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

*/

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

int main() {
    //1、对原始文件进行映射
    int fd = open("english.txt", O_RDWR);
    if (fd == -1) {
        perror("open");
        exit(0);
    }

    //获取原始文件的大小
    int size = lseek(fd, 0, SEEK_END);

    //2、创建一个新的文件并进行拓展
    int fd1 = open("copy.txt", O_RDWR | O_CREAT, 0664);
    if (fd1 == -1) {
        perror("open");
        exit(0);
    }
    //拓展文件
    truncate("copy.txt", size);
    write(fd1, " " , 1);

    //3、分别做内存映射
    void * ptr1 = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    void * ptr2 = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd1, 0);

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

    if (ptr2 == MAP_FAILED) {
        perror("mmap");
        exit(0)
    }
    //内存拷贝
    memcpy(ptr2, ptr1, size);

    //释放资源
    munmap(ptr2, size);
    munmap(ptr1, size);

    close(fd1);
    close(fd);

    return 0;
}

匿名内存映射示例代码

#include <stdio.h>
#include <sys/mman.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <stdlib.h>
#include <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);
    }

    munmap(ptr, len);

    return 0;
}

九、信号

1、信号的概念

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

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

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

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

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

4)运行kill命令或调用kill函数

◼使用信号的主要目的:

1)让进程直到已经发生了一个特定事件

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

◼信号的特点:

1)简单

2)不能携带大量信息

3)满足某个特点条件才发送

4)优先级比较高

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

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

2、所有信号

 

 

 

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

◼ 信号的 5 中默认处理动作

1)Term 终止进程

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

3)Core 终止进程,并生成一个Core文件

4)Stop 暂停当前进程

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

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

◼SIGKILL(终止进程)和SIGTOP(停止进程)信号不能被捕捉、阻塞或者忽略,只能执行默认动作

3、信号的相关函数

#include <sys/types.h>
#include <signal.h>

int kill(pid_t pid, int sig);
int raise(int sig);
void abort(void);

◼ int kill(pid_t pid, int sig);

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

参数:pid:发送的目标进程号,

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

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

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

                <-1:这个pid等于某个进程组的id取反

           sig:需要发送信号的宏值,如果为01表示不发送任何信号

◼ int raise(int sig);

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

参数:sig:要发送的信号

返回值:成功返回0,失败返回非0

◼ void abort(void);

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

#include <unsitd.h>

unsigned int alarm(unsigned int seconds);

◼ unsigned int alarm(unsigned int seconds);

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

参数:seconds:倒计时的时长,单位为秒,如果参数为0,定时器无效,取消一个定时器,通过alarm(0)

返回值:之前没有设置定时器返回0,之前有设置定时器返回之前定时器倒计时剩余的时间

alarm(10)返回0,过了一秒再设置定时器,alarm(5)返回9

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

程序实际的运行时间 = 内核时间 + 用户时间 + 消耗的时间。进行IO操作是比较浪费时间的。

定时器与进程的状态无关,无论进程处于什么状态,alarm都会计时

◼ int setitimer(int which, const struct itimerval *new_val, struct itimerval *old_value);

#include <sys/timer.h>
int setitimer(int which, const struct itimerval *new_val, struct itimerval *old_value);

功能:设置定时器。可以替代alarm函数,精度更高,单位为微秒,可以实现周期性的定时 

参数:which:定时器以什么时间计时

                ITIMER_REAL:真实时间,时间到达,发送SIGALARM,常用的

                ITIMER_VIRTUAL:用户时间,时间到达,发送SIGVTALRM

                ITIMER_PROF:以该进程在用户态和内核态下所消耗的时间来计算,时间到达,发送SIGPROF

             itimerval *new_val:设置定时器的属性,

              itimerval *old_value:记录上一次定时的时间参数,一般不使用设为NULL

返回值:成功返回0,失败返回-1并设置错误号

示例代码:

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

    //设置延迟时间
    new_value.it_value.tv_sec = 3;
    new_value.it_value.tv_usec = 0;

    int ret = setitimer(ITIMER_REAL, &new_value, NULL);//非阻塞的

    getchar();


    return 0;
}

4、signal信号捕捉函数

#include <signal.h>

       typedef void (*sighandler_t)(int);//函数指针

       sighandler_t signal(int signum, sighandler_t handler);

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

参数:signum:要捕捉的信号

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

                        SIG_IGN:忽略信号

                        SIG_DFL:使用信号默认的行为

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

返回值:成功返回上一次注册的信号处理函数的地址,第一次调用则返回NULL;失败返回SIG_ERR,设置错误号

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

SIGKILL和SIGTOP不能被捕捉也不能被忽略

5、信号集

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

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

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

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

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

6、阻塞信号集和未决信号集

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

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

        -在内核中会将所有未被处理的信号存储在未决信号集中

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

                -这个标志位的值位0,说明信号不是未决状态

                -这个标志位位1,说明信号处于未决状态

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

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

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

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

        -如果没有阻塞,这个信号就被处理

        -如果阻塞了,这个信号就继续处于未决状态,直到解除阻塞,这个信号就被处理

7、与信号集相关的函数

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

示例代码,几个函数的使用

/*
以下信号集相关的函数都是对自定义的信号集进行操作
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 <stdio.h>
#include <sys/time.h>
#include <stdlib.h>
#include <signal.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");
    }

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

    ret = sigismember(&set, SIGQUIT);
    if(ret == 0) {
        printf("SIGQUIT不阻塞\n");
    } else if (ret == 1) {
        printf("SIGQUIT被阻塞\n");
    }
    return 0;
}
 /*
    #include <signal.h>
    int sigprocmask(int how, const sigset_t *set, const sigset_t *oldset);
        -功能:将自定义信号集中的数据设置到内核中(设置阻塞,接触阻塞,替换)
        -参数:
            -how:如何对内核阻塞信号集进行处理
                SIG_BLOCK:将用户设置的阻塞信号添加到内核中,内核中原来的数据不变
                    假设内核中默认的阻塞信号是mask,做的行为是 mask | set
                SIG_UNBLOCK:根据用户设置的数据,对内核中的数据进行解除阻塞
                    操作 mask &= ~set
                SIG_SETMASK:覆盖内核中原来的值
            -set:已经初始化好的用户自定义的信号集
            -oldset:保存设置之前的内核中的阻塞信号集的状态
        -返回值:成功返回0,失败返回-1并设置错误号


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

//编写一个程序,吧所有的常规信号的未决状态打印到屏幕

#include <stdio.h>
#include <sys/time.h>
#include <stdlib.h>
#include <signal.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);

        //遍历
        for (int i = 1; i < 32; 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;
}

7、sigaction信号捕捉函数

#include <signal.h>

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;
    //使用哪一个信号处理对捕捉到的信号进行处理,这个值是0表示是使用sa_handler,是1表示使用sa_sigaction
    int        sa_flags;
    //被废弃掉了,只当NULL
    void     (*sa_restorer)(void);
};

8、SIGCHILD信号

◼ SIGCHLD信号产生的条件

1)子进程终止时

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

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

◼ 以上三种条件都会给父进程发送 SIGCHLD 信号,父进程默认会忽略该信号,可使用SIGCHILD解决僵尸进程的问题。

示例代码,使用SIGCHILD信号来解决僵尸进程的问题。

/*
    SIGCHLD信号产生的3个条件:
        1.子进程结束
        2.子进程暂停了
        3.子进程继续运行
        都会给父进程发送该信号,父进程默认忽略该信号。
    
    使用SIGCHLD信号解决僵尸进程的问题。
*/

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

void myFun(int num) {
    printf("捕捉到的信号 :%d\n", num);
    // 回收子进程PCB的资源
    // while(1) {
    //     wait(NULL); 
    // }
    while(1) {
       int ret = waitpid(-1, NULL, WNOHANG);
       if(ret > 0) {
           printf("child die , pid = %d\n", ret);
       } else if(ret == 0) {
           // 说明还有子进程或者
           break;
       } else if(ret == -1) {
           // 没有子进程
           break;
       }
    }
}

int main() {

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

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

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

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

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

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

    return 0;
}

十、共享内存

1、共享内存

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

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

2、共享内存使用步骤

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

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

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

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

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

3、共享内存相关函数

#include <sys/ipc.h>
#include <sys/shm.h>

int shmget(key_t key, size_t size, int shmflg);
    -功能:创建一个新的共享内存段,或者获取一个既有的共享内存段的标识,新创建的内存段中的数据都会被初始化为0
    -参数:
        -key:key_t类型,是一个整形,通过这个找到或者创建一个共享内存,一般使用16进制,非01值
        -size:共享内存的大小
        -shmflg:属性
            -访问权限
            -附加属性:创建/判断共享内存是否存在
                -创建:IPC_CREAT
                -判断共享内存是否存在,IPC_EXCL,需要和IPC_CREAT一起使用
                    IPC_CREAT | IPC_EXCL | 0664
    -返回值:如果失败,返回-1并设置错误号,成功返回>0,返回共享内存的引用ID,后面操作共享内查询都是通过这个值
        
void *shmat(int shmid, const void *shmaddr, int shmflg);
    -功能:与当前的进程进行关联
    -参数:
        -shmid:共享内存的ID,由shmget的返回值获取
        -shmaddr:申请的共享内存的起始地址,指定NULL,内核指定
        -shmflg:对共享内存的操作
            -读:SHM_RDONLY,必须要有
            -读写:0
    -返回值:成功返回共享内存的首地址,失败返回(void *) -1
           
int shmdt(const void *shmaddr);
    -功能:解除当前进程和共享内存的联系
    -参数:shmaddr:共享内粗拿的首地址
    -返回值:成功返回0,失败返回-1

int shmctl(int shmid, int cmd, struct shmid_ds *buf);
    - 功能:对共享内存进行操作。删除共享内存,共享内存要删除才会消失,创建共享内存的进行被销毁了对共享内存是没有任何影响。
    - 参数:
        - shmid: 共享内存的ID
        - cmd : 要做的操作
            - IPC_STAT : 获取共享内存的当前的状态
            - IPC_SET : 设置共享内存的状态
            - IPC_RMID: 标记共享内存被销毁
        - buf:需要设置或者获取的共享内存的属性信息
            - IPC_STAT : buf存储数据
            - IPC_SET : buf中需要初始化数据,设置到内核中
            - IPC_RMID : 没有用,NULL

key_t ftok(const char *pathname, int proj_id);
    - 功能:根据指定的路径名,和int值,生成一个共享内存的key
    - 参数:
        - pathname:指定一个存在的路径
            /home/nowcoder/Linux/a.txt
            / 
        - proj_id: int类型的值,但是这系统调用只会使用其中的1个字节
                   范围 : 0-255  一般指定一个字符 'a'

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

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

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

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

4、共享内存操作命令

1) ipcs 用法

 ipcs -a // 打印当前系统中所有的进程间通信方式的信息

ipcs -m // 打印出使用共享内存进行进程间通信的信息

ipcs -q // 打印出使用消息队列进行进程间通信的信息

ipcs -s // 打印出使用信号进行进程间通信的信息

2)ipcrm 用法

ipcrm -M shmkey // 移除用shmkey创建的共享内存段

ipcrm -m shmid // 移除用shmid标识的共享内存段

ipcrm -Q msgkey // 移除用msqkey创建的消息队列

ipcrm -q msqid // 移除用msqid标识的消息队列

ipcrm -S semkey // 移除用semkey创建的信号

ipcrm -s semid // 移除用semid标识的信号

十一、守护进程

1、终端

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

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

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

2、进程组

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

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

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

3、会话

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

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

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

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

 4、进程组会话相关操作函数

◼ pid_t getpgrp(void);

◼ pid_t getpgid(pid_t pid);

◼ int setpgid(pid_t pid, pid_t pgid);

◼ pid_t getsid(pid_t pid);

◼ pid_t setsid(void);

5、守护进程

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

守护进程具备以下特征:

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

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

Linux地大多数服务器就是用守护进程实现的,比如Internet服务器intetd,web服务器httpd等

6、守护进程的创建步骤

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

2)子进程调用 setsid() 开启一个新会话。目的就是使其脱离控制终端

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

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

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

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

7)核心业务逻辑

其中父进程退出是在守护进程的父进程中调用exit(),这是为了确保父进程的父进程在其子进程结束时会退出,保证了守护进程的符进程不再继续运行,而且守护进程不是首进程。

示例代码,利用守护进程,每个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 num) {
    // 捕捉到信号之后,获取系统时间,写入磁盘文件
    time_t tm = time(NULL);
    struct tm * loc = localtime(&tm);
    // char buf[1024];

    // sprintf(buf, "%d-%d-%d %d:%d:%d\n",loc->tm_year,loc->tm_mon
    // ,loc->tm_mday, loc->tm_hour, loc->tm_min, loc->tm_sec);

    // printf("%s\n", buf);

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

int main() {

    // 1.创建子进程,退出父进程
    pid_t pid = fork();

    if(pid > 0) {
        exit(0);
    }

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

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

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

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

    // 6.业务逻辑

    // 捕捉定时信号
    struct sigaction act;
    act.sa_flags = 0;
    act.sa_handler = work;
    sigemptyset(&act.sa_mask);
    sigaction(SIGALRM, &act, NULL);

    struct itimerval val;//定时器
    val.it_value.tv_sec = 2;
    val.it_value.tv_usec = 0;
    val.it_interval.tv_sec = 2;
    val.it_interval.tv_usec = 0;

    // 创建定时器
    setitimer(ITIMER_REAL, &val, NULL);

    // 不让进程结束
    while(1) {
        sleep(10);
    }

    return 0;
}

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值