linux并发服务器 —— 多进程并发 - 进程间的通信及实践(五)

 进程间的通信

进程是一个独立的资源分配单元,不能在一个进程中直接访问另一个进程的资源;

进程间通信(IPC)的目的:

1. 数据传输 - A进程发送数据给B进程

2. 通知事件 - eg. 进程终止通知父进程

3. 资源共享 - 多个进程之间共享资源,需要内核提供互斥和同步机制

4. 进程控制 - 进程控制另一个进程的执行

匿名管道(管道)

UNIX系统IPC的最古老形式,所有UNIX系统都支持这种通信机制;

ls | wc -l : 统计一个目录中的文件数目;| - 管道符

管道的特点

1.是一个内核内存中维护的缓冲器,存储能力有限,不同操作系统大小不同;

2. 匿名管道没有文件实体,有名管道有文件实体(但不存储数据),可以按照操作文件的方式对管道进行操作;

3. 一个管道是一个字节流,使用管道不存在消息/消息边界的概念;不管写入数据块多大,可以读任意大小的数据块;

4. 先进先出;

5. 管道是半双工的,数据传递方向是单向的;

6. 读数据是一次性操作,读了就没了,并且无法随机访问数据;

7. 匿名管道只能在有亲缘关系的进程之间进行通信;

管道的数据结构 - 循环队列

/*
    #include <unistd.h>
    int pipe(int pipefd[2]);
        功能:创建一个匿名管道 用于进程间通信
        参数: 是一个传出参数
            pipefd[0] - 管道的读端
            pipefd[1] - 管道的写端
        返回值:
            成功 - 0
            失败 - -1
        管道默认是阻塞的,管道中没有数据read阻塞,管道满了write阻塞
*/
#include <iostream>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
using namespace std;

// 子进程写 父进程读
int main(){

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

    pid_t pid = fork();
    if(pid>0){
        cout<<"我是你爹"<<endl;
        while(1){
            char buf[1024];
            int len = read(pipefd[0] , buf , sizeof(buf));
            cout<<"父进程 "<<getpid()<<"读到了: "<<buf<<endl;

            char str[10] = "hello zz";
            write(pipefd[1] , str , sizeof(str));
            sleep(2);

        }
    }
    else if(pid == 0){
        // 写数据
        cout<<"我是你儿子"<<endl;
        while(1){
            char str[10] = "hello 647";
            write(pipefd[1] , str , sizeof(str));
            sleep(2);

            char buf[1024];
            int len = read(pipefd[0] , buf , sizeof(buf));
            cout<<"子进程 "<<getpid()<<"读到了: "<<buf<<endl;
        }
    }

    return 0;
}

查管道大小:

1. ulimit - a

2. fpathconf(int fd , int name); name - _PC_PIPE_BUF 获取管道大小

通过管道实现ps aux

/*
    实现ps aux | grep
    父子进程通信 - 子进程 ps aux,结束后发送给父进程 过滤即可

    pipe() - 创建管道
    execlp() - 调用ps aux
    将子进程标准输出stdout_fileno重定向到父进程 - 管道写端 dup2
*/
#include <iostream>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
using namespace std;

// 子进程写 父进程读
int main(){

    int fd[2];
    int ret = pipe(fd);

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

    pid_t pid = fork();

    if(pid > 0){
        close(fd[1]);
        char buf[1024] = {0};
        int len = -1;
        while((len = read(fd[0] , buf , sizeof(buf) - 1)) > 0){
            cout<<buf;
            memset(buf , 0 , 1024);
        }
        wait(NULL);
    }
    else if(pid == 0){
        close(fd[0]);
        dup2(fd[1] , STDOUT_FILENO);
        execlp("ps" , "ps" , "aux" , NULL);
        perror("execlp");
        exit(0);
    }
    else{
        perror("fork");
        exit(0);
    }

    return 0;
}
管道的读写特点

(假设都是阻塞I/O操作)

1. 所有指向管道的写端描述符都关闭(写端引用计数为0),剩余数据都读完后再read会返回0;

2. 如果有指向管道写端的文件描述符没有关闭(引用计数>0),但没有写数据,read会阻塞;

3. 读端都关闭,进程向管道写数据,该进程会收到信号SIGPIPE,通常会导致进程异常终止;

4. 读端没有完全关闭,而持有管道读端的进程也没从管道读数据,写数据写满了就阻塞;

设置管道非阻塞

通过设置文件描述符非阻塞;

// 通过fcntl(fd , F_GETFL)获得状态,再设置可实现管道非阻塞
int flg = fcntl(pipefd[0] , F_GETFL);
flg |= O_NONBLOCK;
int cnt = fcntl(pipefd[0] , F_SETFL , flg);

有名管道(FIFO文件)

提供一个路径名与之关联,有文件的实体,以FIFO文件形式存在文件系统中,没有亲缘关系也能通过有名管道实现通信;

数据结构也是一个环形队列;

与匿名管道的区别:

1. FIFO在文件系统中作为特殊文件存在,但文件里没有内容,内容存在内存里的缓冲区;

2. FIFO退出后,文件会保留;

3. FIFO有名字,不相关进程可以通信;

通过mkfifo 名字创建有名管道,创建后可以用open打开,常见文件I/O函数都可以用;

有名管道的读端写端实践:

/*
    创建fifo文件
    1. 命令 - mkfifo 名字
    2. 函数 - int mkfifo(const char *pathname, mode_t mode);
        参数:
            pathname - 管道名称的路径
            mode - 权限 和open的一样
        返回值:
            成功 - 0
            失败 - -1 并设置 errno

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

       
*/
// 向管道写数据
#include <sys/types.h>
#include <sys/stat.h>
#include <iostream>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
using namespace std;

int main(){

    int dec = access("fifo" , F_OK);
    if(dec==-1){
        cout<<"管道不存在"<<endl;
        int ret = mkfifo("fifo" , 0664);

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

    // 打开管道
    int fd = open("fifo" , O_WRONLY);
    if(fd == -1){
        perror("open");
        exit(0);
    }

    for(int i = 0 ; i<100 ; i++){
        char buf[1024];
        sprintf(buf , "hello 647 : %d" , i);
        cout<<buf<<endl;
        write(fd , buf , sizeof(buf));
        sleep(1);
    }
    close(fd);
    return 0;
}


// 向管道读数据
#include <sys/types.h>
#include <sys/stat.h>
#include <iostream>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
using namespace std;

int main(){
    // 打开管道
    int fd = open("fifo" , O_RDONLY);
    if(fd == -1){
        perror("open");
        exit(0);
    }

    while(1){
        char buf[1024] = {0};
        int len = read(fd , buf , sizeof(buf));
        if(len == 0){
            cout<<"写端断开"<<endl;
            break;
        }
        cout<<"get: "<<buf<<endl;
    }

    close(fd);

    return 0;
}

一个为只读而打开一个管道的进程会阻塞,直到有写的权限打开管道,反之同理;

读管道:

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

        管道无数据

                写端全关闭,read返回0

                写端没有全关闭,read阻塞

写管道:

        读端全关闭,异常终止SIGPIPE

        读端没有全关闭

                管道满了,阻塞

                 没满,正常写入即可,返回实际写入字节数

有名管道实现聊天功能
/*
    实现进程A、B的聊天功能
    需要两个管道 一个A读B写 一个B读A写
    进程A:
        1. 只写打开fifo1
        2. 只读打开fifo2
        3. 循环读写数据
            while(1){
                获取键盘录入 fgets
                write fifo1

                read fifo2
            }
    进程B与进程A相反即可
            while(1){
                read fifo1
                获取键盘录入 fgets
                write fifo2
            }
*/
#include <sys/types.h>
#include <sys/stat.h>
#include <iostream>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
using namespace std;

int main(){
    // 判断管道文件是否存在
    int ret = access("fifo1" , F_OK);
    if(ret == -1){
        ret = mkfifo("fifo1" , 0664);
        if(ret == -1){
            perror("mkfifo");
            exit(0);
        }
    }

    ret = access("fifo2" , F_OK);
    if(ret == -1){
        ret = mkfifo("fifo2" , 0664);
        if(ret == -1){
            perror("mkfifo");
            exit(0);
        }
    }

    // 只写开fifo1
    int fdw = open("fifo1" , O_WRONLY);
    if(fdw == -1){
        perror("open");
        exit(0);
    }
    cout<<"打开fifo1成功..."<<"等待写入..."<<endl;


    // 只读开fifo2
    int fdr = open("fifo2" , O_RDONLY);
    if(fdr == -1){
        perror("open");
        exit(0);
    }
    cout<<"打开fifo2成功..."<<"等待读取..."<<endl;

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

        // 读数据
        memset(buf , 0 , sizeof(buf));
        ret = read(fdr , buf , sizeof(buf));
        if(ret<=0){
            perror("read");
            exit(0);
        }
        cout<<"进程A读到数据 - "<<buf;
    }

    close(fdr);
    close(fdw);

}


#include <sys/types.h>
#include <sys/stat.h>
#include <iostream>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
using namespace std;

int main(){
    // 判断管道文件是否存在
    int ret = access("fifo1" , F_OK);
    if(ret == -1){
        ret = mkfifo("fifo1" , 0664);
        if(ret == -1){
            perror("mkfifo");
            exit(0);
        }
    }

    ret = access("fifo2" , F_OK);
    if(ret == -1){
        ret = mkfifo("fifo2" , 0664);
        if(ret == -1){
            perror("mkfifo");
            exit(0);
        }
    }

    // 只读开fifo1
    int fdr = open("fifo1" , O_RDONLY);
    if(fdr == -1){
        perror("open");
        exit(0);
    }
    cout<<"打开fifo1成功..."<<"等待读取..."<<endl;


    // 只写开fifo2
    int fdw = open("fifo2" , O_WRONLY);
    if(fdw == -1){
        perror("open");
        exit(0);
    }
    cout<<"打开fifo2成功..."<<"等待写入..."<<endl;

    char buf[128];
    // 循环写读
    while(1){
        // 读数据
        memset(buf , 0 , sizeof(buf));
        ret = read(fdr , buf , sizeof(buf));
        if(ret<=0){
            perror("read");
            exit(0);
        }
        cout<<"进程B读到数据 - "<<buf;

         memset(buf , 0 , sizeof(buf));
        // 获取输入数据
        fgets(buf , sizeof(buf) , stdin);
        // 写入
        ret = write(fdw , buf , strlen(buf));
        if(ret == - 1){
            perror("write");
            exit(0);
        }
    }

    close(fdr);
    close(fdw);

}

内存映射

效率较高的IPC , 直接对内存进行操作;将磁盘文件的数据映射到内存(栈和堆之间的地址空间),通过修改内存来修改磁盘文件;

通过将磁盘文件映射到两个进程地址空间中实现进程间的通信;

mmap - 文件映射到内存/munmap -  解除映射

/*
    

    void *mmap(void *addr, size_t length, int prot, int flags,
                int fd, off_t offset);
        功能: 映射一个文件到内存中
        参数:
            addr - 映射内存的首地址 - NULL 由内核指定
            length - 映射的数据的长度 !=0 , 建议文件长度 -> 分页整数倍
                    获取文件长度 - stat lseek
            prot -  对申请的内存映射区的操作权限
                PROT_EXEC - 执行
                PROT_READ - 读权限
                PROT_WRITE - 写权限
                PROT_NONE - 没有权限
                要操作映射内存,必须要有读的权限
                PROT_READ、PROT_READ|PROT_WRITE
            flags
                MAP_SHARED - 映射区的数据自动和磁盘文件同步(进程通信必备)
                MAP_PRIVATE - 映射区的数据自动和磁盘文件不同步(copy on write)
            fd - 磁盘文件描述符(open获得)
                - 文件大小!=0 , open指定权限不能和prot有冲突(open>prot)
            offset - 偏移量 必须是4K整数倍 一般不用;0 - 不偏移
        返回值:创建好的内存首地址 , 失败返回MAP_FAILED(void* -1)

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

*/

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

    NOTE:内存映射区通信是非阻塞的
*/

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


using namespace std;

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

    if(fd == -1){
        perror("open");
        exit(0);
    }
    // 获取文件大小
    int size = lseek(fd , 0 , SEEK_END);

    void* ptr = mmap(NULL , size , PROT_READ|PROT_WRITE , MAP_SHARED , fd , 0);
    if(ptr == MAP_FAILED){
        perror("mmap");
        exit(0);
    }

    // 创建子进程
    pid_t pid = fork();
    if(pid>0){
        strcpy((char*) ptr , "我是你爹");
        wait(NULL);
    }
    else if(pid == 0){
        char buf[64];
        strcpy(buf , (char*) ptr);
        cout<<"子进程读到的数据:"<<buf<<endl;
    }

    munmap(ptr , size);
    return 0;
}
NOTE

1.如果对mmap的返回值(ptr)做++! I(ptr++),munmap是否能够成功?

可以对ptr进行++操作;

但munmap需要传递内存的首地址,++后不能正确释放内存;


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

错误,返回MAP_FAILED

open函数中的权限建议与prot权限保持一致,大于等于也可;


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

文件偏移量必须是4k的整数倍 - 错误 返回MAP_FAILED


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

        - length = 0

        - prot的权限只指定了写 或 权限大于open

        ..............


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

可以!但是创建的文件大小如果为0则会出错

可以对新的文件进行拓展 - lseek/truncate


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

不会产生问题;映射区仍然存在 创建映射区的fd关闭没什么影响


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

void* ptr = mmap(NULL , 100 ...)

4K

越界操作操作的是非法内存 ——> 段错误

通过内存映射实现文件拷贝
// 使用内存映射实现拷贝
/*
    思路:
        1. 原始文件进行映射
        2. 创建一个新的文件(通过拓展,保证文件不为0)
        3. 新文件的数据映射到内存
        4. 通过内存拷贝 即可实现
        5. 释放资源
*/

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


using namespace std;

int main(){
    // 1.
    int fd = open("old.txt" , O_RDWR);
    if(fd == -1){
        perror("open");
        exit(0);
    }
    int size = lseek(fd , 0 , SEEK_END);
    // 2.
    int fdn = open("new.txt" , O_RDWR|O_CREAT , 0664);
    if(fdn == -1){
        perror("open");
        exit(0);
    }
    truncate("new.txt" , size);
    write(fdn , " " , 1);
    // 3.
    void* ptr1 = mmap(NULL , size , PROT_READ | PROT_WRITE , MAP_SHARED , fd , 0);
    if(ptr1 == MAP_FAILED){
        perror("mmap");
        exit(0);
    }
    void* ptr2 = mmap(NULL , size , PROT_READ | PROT_WRITE , MAP_SHARED , fdn , 0);
    if(ptr2 == MAP_FAILED){
        perror("mmap");
        exit(0);
    }

    // 4.
    memcpy(ptr2 , ptr1 , size);
    // 5.
    munmap(ptr2 , size);
    munmap(ptr1 , size);
    close(fdn);
    close(fd);

    return 0;
}

匿名映射
/*
    匿名映射:不需要文件实体 只能用在父子进程之间的通信
     - flags - MAP_ANONYMOUS fd指定为-1即可 offset为0
*/
#include <iostream>
#include <stdio.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <string.h>
#include <sys/wait.h>


using namespace std;

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 647");
        wait(NULL);
    }
    else if(pid == 0){
        sleep(1);
        cout<<(char*) ptr<<endl;
    }

    int ret = munmap(ptr , len);
    if(ret == -1){
        perror("munmap");
        exit(0);
    }

    return 0;
}

信号概述

信号是事件发生时对进程的通知机制 , 有时称为软件中断(软件层次上对中断机制的模拟 - 异步通信);

信号通常源于内核 ,引发内核产生信号的事件:

1. 前台进程,用户通过输入特殊中断字符发送信号;eg. Ctrl+c

2. 硬件发生异常

3. 系统状态发生变化 - alarm定时器到期引起SIGALRM

4. kill 命令

信号的特点:

1. 简单

2. 不能携带大量信息

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

4. 优先级较高*

查看系统定义的信号列表:kill -l;1~31为常规信号 , 其余为预定义好的实时信号

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

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

kill、raise、abort函数

Core处理动作会生成Core文件 - 保存进程异常退出的错误信息

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

int main(){
    char* buf;
    strcpy(buf , "hello");
    return 0;
}

 要生成core文件需要先通过ulimit修改core文件的大小;

注意这里生成不了core文件可以通过sudo service apport stop关闭错误收集系统;

进gdb调试后,core-file core即可打印错误信息;

/*
    int kill (pid_t pid, int sig);
        功能:给任何进程/进程组pid,发送任何信号sig
        参数:
            pid
                >0 - 将信号发送给指定进程
                0 - 将信号发送给当前进程组所有进程
                -1 - 将信号发送给每一个有权限接收这个信号的进程
                <-1 - abs(pid)进程组
            sig - 信号编号/宏值,如果为0表示不发送任何信号
        kill(getppid() , 9);

    int raise (int sig);
        功能:给当前进程发送信号(给自己)
        参数:sig - 要发送的信号
        返回值:
            成功 - 0
            失败 - !0

    void abort(void);
        功能:发送SIGABRT给当前进程,杀死当前进程
*/
#include <iostream>
#include <stdio.h>
#include <sys/types.h>
#include <signal.h>
#include <unistd.h>
using namespace std;

int main(){

    pid_t pid = fork();

    if(pid == 0){
        int i = 0;
        for(i ; i<5 ; i++){
            cout<<"子进程"<<endl;
            sleep(1);
        }
    }
    else if(pid > 0){
        cout<<"父进程"<<endl;
        sleep(2);
        cout<<"杀死子进程"<<endl;
        kill(pid , SIGINT);
    }

    return 0;
}

alarm函数(自然定时法 , 与进程状态无关)

unsigned int alarm(unsigned int seconds);

/*/
    #include <unistd.h>
    unsigned int alarm(unsigned int seconds);
        功能:设置定时器(闹钟),函数调用开始倒计时,不会阻塞
            倒计时为0,给当前进程发SIGALRM
        参数:seconds - 倒计时/s , 参数为0 - 定时器无效
            取消一个定时器 - alarm(0)
        返回值 
            - 若之前没有定时器返回0
            - 之前定时器,倒计时剩余的时间
    SIGALRM:默认终止当前进程,某个进程都只有一个唯一定时器;
*/
#include <iostream>
#include <unistd.h>
using namespace std;

int main(){

    int sec = alarm(5);
    cout<<"seconds = "<<sec<<endl;

    sleep(2);
    sec = alarm(2);
    cout<<"seconds = "<<sec<<endl;

    while(1){

    }

    return 0;
}

 实际的时间 = 内核时间 + 用户时间 + 消耗的时间(IO...)

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

setitimer定时器函数(非阻塞)
/*
    #include <sys/time.h>
    int setitimer(int which, const struct itimerval *new_value,
                    struct itimerval *old_value);
        功能:设置定时器(闹钟) , 替代alarm函数,精度更高 - us
                可以实现周期定时
        参数: 
            which - 定时器以什么时间计时
                ITINER_REAL - 真实事件->SIGALRM
                ITIMER_VIRTUAL - 用户时间->SIGVTALRM
                ITIMER_PROF - 用户态和内核态下的事件->SIGPROF
            new_value - 设置定时器的属性
                struct itimerval { // 定时器的结构体
                    struct timeval it_interval;  //Interval for periodic timer 间隔时间 
                    struct timeval it_value;     //Time until next expiration  延迟时间
                };

                struct timeval { // 时间的结构体
                    time_t      tv_sec;          //seconds 
                    suseconds_t tv_usec;         //microseconds 
                };
            old_value - 记录上一次的时间参数 一般设为NULL 不使用
        返回值:
            成功 - 0
            失败 - -1 并设置errno
*/

#include <iostream>
#include <unistd.h>
#include <sys/time.h>
#include <cstdio>
#include <cstdlib>
using namespace std;

// 过3s 每2s定时一次
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);

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

    getchar();

    return 0;
}

捕捉signal信号 - signal/sigaction

signal会根据不同的UNIX版本而表现不同;sigaction不会,建议使用sigaction

一定要在定时器设置之前注册信号捕捉!!!

/*
    #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 并设置errno

    SIGKILL SIGSTOP不能被捕捉、忽略、处理

*/
#include <iostream>
#include <unistd.h>
#include <sys/time.h>
#include <signal.h>
#include <cstdio>
#include <cstdlib>
using namespace std;

void meet(int num){
    // num 表示捕捉到信号的值
    cout<<"逮到你了~"<<endl;
}

// 过3s 每2s定时一次
int main(){
    // 注册信号捕捉
    // signal(SIGALRM,  SIG_IGN);
    // signal(SIGALRM,  SIG_DFL);
    signal(SIGALRM,  meet);
    
    // 设置结构体
    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);

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

    getchar();

    return 0;
}

信号集

sigantion涉及到了信号集的概念 - 很多信号组成的集合 , 数据类型 - sigset_t;

PCB中有两个重要的信号集 - 阻塞信号集和未决信号集;两个信号集都由内核使用位图机制来实现 - 64位;不能直接对两个信号集进行操作,需要自定义另一个集合,借助信号集操作函数来对其进行操作;

/*
    对自定义信号集进行操作
    #include <signal.h>
    int sigemptyset(sigset_t *set);
        功能 - 清空信号集的数据,标志位置0
        参数 - 需要操作的信号集(传出参数)
        返回值:
            成功 - 0
            失败 - -1 并设置errno
    int sigfillset(sigset_t *set);
        功能 - 标志位置1
        参数 - 需要操作的信号集(传出参数)
        返回值:
            成功 - 0
            失败 - -1 并设置errno

    int sigaddset(sigset_t *set, int signum);
        功能 - 设置信号集中的某一标志位为1,阻塞该信号
        参数
            set - 需要操作的信号集(传出参数)
            signum - 需要阻塞的信号
        返回值:
            成功 - 0
            失败 - -1 并设置errno
    int sigdelset(sigset_t *set, int signum);
        功能 - 设置信号集中的某一标志位为0,不阻塞该信号
        参数
            set - 需要操作的信号集(传出参数)
            signum - 需要不阻塞的信号
        返回值:
            成功 - 0
            失败 - -1 并设置errno
    int sigismember(const sigset_t *set, int signum);
        功能 - 判断某个信号是否阻塞
        参数
            set - 需要操作的信号集
            signum - 待判断的信号
        返回值:
            被阻塞 - 1
            没被阻塞 - 0
            错误 - -1
*/
#include <iostream>
#include <unistd.h>
#include <sys/time.h>
#include <signal.h>
#include <cstdio>
#include <cstdlib>
using namespace std;

int main(){
    sigset_t set;// 创建
    //清空
    sigemptyset(&set);
    int ret = sigismember(&set , SIGINT);
    if(ret == 0){
        cout<<"SIGINT 不阻塞"<<endl;
    }
    else if(ret == 1){
        cout<<"SIGINT 阻塞"<<endl;
    }

    // 添加信号集
    sigaddset(&set , SIGINT);
    sigaddset(&set , SIGQUIT);

    ret = sigismember(&set , SIGINT);
    if(ret == 0){
        cout<<"SIGINT 不阻塞"<<endl;
    }
    else if(ret == 1){
        cout<<"SIGINT 阻塞"<<endl;
    }

    // 信号集删除
    sigdelset(&set , SIGQUIT);
    ret = sigismember(&set , SIGQUIT);
    if(ret == 0){
        cout<<"SIGQUIT 不阻塞"<<endl;
    }
    else if(ret == 1){
        cout<<"SIGQUIT 阻塞"<<endl;
    }
    return 0;
}

可以通过sigprocmask对内核中的阻塞信号集进行操作

/*
    #include <signal.h>
    int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
        功能:将自定义信号集中的数据设置到内核中
        参数:
            how:如何对内核阻塞信号集进行处理
                SIG_BLOCK - 将用户设置的阻塞信号集添加到内核中 内核中原来的数据不变(|)
                    mask |= set
                SIG_UNBLOCK - 根据用户设置清除内核中阻塞信号(&)
                    mask &= ~set
                SIG_SETMASK - 覆盖
            set - 自定义信号集
            oldset - 保存设置之前内存中信号集状态 , 一般NULL
        返回值:
            成功 - 0
            失败 - -1 (两个错误号)

    int sigpending(sigset_t *set);
        功能:获取内核中未决信号集
        参数:set - 传出参数
*/
// 写一个程序 不断把所有的常规信号的未决状态打印到屏幕(位)
#include <iostream>
#include <unistd.h>
#include <sys/time.h>
#include <signal.h>
#include <cstdio>
#include <cstdlib>
using namespace std;

int main(){

    // 设置2、3信号阻塞
    sigset_t set;
    sigemptyset(&set);

    sigaddset(&set , SIGINT);
    sigaddset(&set , SIGQUIT);

    sigprocmask(SIG_BLOCK , &set , NULL);

    while(1){
        sigset_t pending_set;
        sigemptyset(&pending_set);
        sigpending(&pending_set);

        //遍历前32位
        for(int i = 1 ; i<=32 ; i++){
            if(sigismember(&pending_set , i)==1){
                cout<<"1";
            }
            else if(sigismember(&pending_set , i)==0){
                cout<<"0";
            }
            else{
                perror("sigismember");
                exit(0);
            }
        }
        cout<<endl;
    }



    return 0;
}

信号捕捉函数 - sigaction

信号捕捉处理过程中使用临时阻塞信号集,信号处理完后恢复内核中的阻塞信号集;

信号处理过程中,再发出信号会阻塞,且阻塞的常规信号不支持排队;

/*
    #include <signal.h>

    int sigaction(int signum, const struct sigaction *act,
                    struct sigaction *oldact);
        功能:检查/改变信号的处理
        参数:
            signum - 需要捕捉的信号编号/宏值
            act - 捕捉到信号之后的处理动作
            oldact - 上次的相关设置,一般NULL
        返回值:
            成功 - 0
            失败 - -1

    struct sigaction {
            // 信号捕捉到后的处理函数
            void     (*sa_handler)(int);
            // 不常用
            void     (*sa_sigaction)(int, siginfo_t *, void *);
            // 临时阻塞信号集 信号捕捉函数执行过程中临时阻塞某些信号
            sigset_t   sa_mask;
            // 使用哪个处理捕捉信号 0 - sa_handler/SA_SIGINFO - sa_sigaction
            int        sa_flags;
            // 被废弃掉了 - NULL
            void     (*sa_restorer)(void);
        };


*/
#include <iostream>
#include <unistd.h>
#include <sys/time.h>
#include <signal.h>
#include <cstdio>
#include <cstdlib>
using namespace std;

void meet(int num){
    // num 表示捕捉到信号的值
    cout<<"逮到你了~"<<endl;
}

// 过3s 每2s定时一次
int main(){
    struct sigaction act;
    act.sa_flags = 0;
    act.sa_handler = meet;
    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;

    new_value.it_value.tv_sec = 3;
    new_value.it_value.tv_usec = 0;

    int ret = setitimer(ITIMER_REAL , &new_value , NULL);

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

    while(1){
        
    }

    return 0;
}

SIGCHLD信号

1. 子进程终止

2. 子进程接收到SIGSTOP - 暂停

3. 处于暂停的子进程收到SIGCONT唤醒

给父进程发送SIGCHLD,父进程默认忽略;

/*
    通过SIGCHLD解决僵尸进程的问题
*/
#include <iostream>
#include <cstdio>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <signal.h>
#include <sys/wait.h>
#include <cstdlib>
using namespace std;

void meet(int num){
    while(1){
        int ret = waitpid(-1 , NULL , WNOHANG);
        if(ret > 0){
            cout<<"子进程结束了 - "<<ret<<endl;
        }
        else if(ret == 0){
            break;
        }
        else{
            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 < 5 ; i++){
        pid = fork();
        if(pid == 0){
            break;
        }
    }
    if(pid > 0){
        // 捕捉SIGCHLD信号
        struct sigaction act;
        act.sa_flags = 0;
        act.sa_handler = meet;
        sigemptyset(&act.sa_mask);
        sigaction(SIGCHLD , &act , NULL);
        
        sigprocmask(SIG_UNBLOCK , &set , NULL);

        while(1){
            cout<<"父进程: "<<getpid()<<endl;
            sleep(2);
        }
    }
    else if(pid == 0){
        cout<<"子进程:"<<getpid()<<endl;
    }

    return 0;
}

注意:当没有子进程时,waitpid也会返回-1;并不是只有在错误得时候才返回-1;

共享内存

共享内存的效率高于内存映射;允许多个进程共享同一物理内存区域,共享内存段为用户空间的一部分,因此共享内存无需内核介入(相比其他IPC通信少);

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

/*
    key_t ftok(const char *pathname, int proj_id);
        功能:根据指定路径名和int值生成一个共享内存的key
        参数:
            pathname - 路径名
            proj_id - int值,但系统调用只是用其中1个字节

    问题1:操作系统如何知道一块共享内存被多少个进程关联?
        - 共享内存维护了一个结构体struct shmid_ds 其中有一个成员shm_nattach记录该信息
        可以通过ipcs -a 查所有通信方式信息
                ipcs -m 共享内存;ipcrm 删除(标记删除不会释放)
                ipcs -q 消息队列
                ipcs -s 信号
    问题2:可不可以对共享内存进行多次删除 shmct1
        可以的
        因为shmct1 标记删除共享内存,不是直接删除,当关联进程为0才会被真正删除
        如果一个进程和共享内存取消关联就不能继续操作这个共享内存;
    问题三:共享内存和内存映射的区别
        1. 共享内存可以直接创建,内存映射需要磁盘文件(匿名映射除外)
        2. 共享内存效率更高
        3. 所有进程操作的是统一共享内存,内存映射是每个进程在自己的虚拟地址空间有一块独立内存
        4. 进程突然退出,共享内存还存在,内存映射区会消失,但磁盘文件中的数据还在
        5. 内存映射区:进程退出就销毁
            共享内存:进程退出,需要手动删除(所有关联进程数为0),进程退出会自动和共享内存取消关联
*/
// write
#include <iostream>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <cstdio>
#include <string.h>
using namespace std;

int main(){
    // 1. 创建
    int shmid = shmget(100 , 4096 , IPC_CREAT | 0664);
    // 2. 关联
    void* ptr = shmat(shmid , NULL , 0);
    // 3. 写数据
    char* ctr=new char[20];
    string str = "hello 647";
    strcpy(ctr,str.c_str());
    memcpy(ptr , ctr , sizeof(str)+1);

    cout<<"按任意键继续"<<endl;
    getchar();
    
    // 4. 解除关联
    shmdt(ptr);
    // 5. 删除共享内存
    shmctl(shmid , IPC_RMID , NULL);

    return 0;
}

// read
#include <iostream>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <cstdio>
#include <string.h>
using namespace std;

int main(){
    // 1. 创建
    int shmid = shmget(100 , 0 , IPC_CREAT);
    // 2. 关联
    void* ptr = shmat(shmid , NULL , 0);
    // 3. 读数据
    cout<<(char*)ptr<<endl;

    cout<<"按任意键继续"<<endl;
    getchar();
    
    // 4. 解除关联
    shmdt(ptr);
    // 5. 删除共享内存
    shmctl(shmid , IPC_RMID , NULL);

    return 0;
}

守护进程

终端

UNIX系统,用户通过终端登陆系统得到一个shell进程,这个终端为shell控制终端 - 保存于PCB;

默认情况下标准输入、标准输出、标准错误都指向控制终端;

进程组

进程组是一组相关进程集合,进程组的生命周期从首进程创建组开始,最后一个成员进程退出组结束;

会话

会话是一组相关进程组的集合,会话首进程为创建该会话的id,进程Id为会话id;

一个会话中的所有进程共享单个控制终端,一个终端最多可能会成为一个会话的控制终端;会话首进程为该终端的控制进程;

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

守护进程 - 后台服务进程

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

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

创建步骤

1. fork(),父进程退出 - 确保子进程不是进程组首进程/防止父进程结束后显示shell提示符

2. setsid() - 脱离控制终端

3. 清楚进程umask,确保守护进程创建文件、目录所需的权限

4. 该当前目录为根目录

5. 关继承的打开的文件描述符

6. 关闭文件描述符0 1 2后,守护进程打开/dev/null,重定向文件描述符到该设备

7. 业务逻辑

/*
    写一个守护进程,每隔2s获取系统时间 写入磁盘文件
*/
#include <iostream>
#include <cstdio>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <cstdlib>
#include <fcntl.h>
#include <sys/time.h>
#include <signal.h>
#include <time.h>
#include <string.h>
#include <sstream>
using namespace std;

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);
    // cout<<buf<<endl;
    char* str = asctime(loc);
    int fd = open("time.txt" , O_RDWR|O_CREAT , 0664);
    write(fd , str , strlen(str));
}

int main(){
    // 创建子进程 - 防止进程组冲突
    pid_t pid = fork();

    if(pid>0){
        exit(0);
    }
    // 新建会话 - 脱离控制终端
    setsid();

    // 设置掩码
    umask(022);

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

    // 关文件描述符
    int fd = open("/dev/null" , O_RDWR);
    dup2(fd , STDIN_FILENO);
    dup2(fd , STDOUT_FILENO);
    dup2(fd , STDERR_FILENO);

    // 业务逻辑
    struct sigaction act;
    act.sa_flags = 0;
    act.sa_handler = work;
    sigemptyset(&act.sa_mask);

    sigaction(SIGALRM , &act , NULL);

    struct itimerval val;
    val.it_interval.tv_sec = 2;
    val.it_interval.tv_usec = 0;
    val.it_value.tv_sec = 2;
    val.it_value.tv_usec = 0;

    setitimer(ITIMER_REAL , &val , NULL);

    while(1){
        sleep(10);
    }

    return 0;
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值