玩命加载中 . . .

2.3-进程间通信(下)


内存映射

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

非阻塞

#include <sys/mman.h>
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
// 将一个文件或者设备的数据映射到内存中
- addr: 映射内存的地址,由内核决定
- length: 要映射的数据的长度,不能为0,建议使用文件的长度(stat/lseek)
- prot: 对申请的内存映射区的权限
    PROT_EXEC: 可执行
    PROT_READ: 可读
    PROT_WRITE: 可写
    PROT_NONE: 没有权限
- flags
    MAP_SHARED: 映射区数据自动和磁盘文件同步,进程间通信必须设置
    MAP_PRIVATE: 不同步,映射区数据改变,对原来文件不会修改,会创建一个新文件(copy on write)
- fd: 需要映射的文件的文件描述符,通过open得到,文件大小要大于0,open的权限不能和prot冲突
- offset: 映射文件的偏移,必须是4k的整数倍
// 返回创建的内存的首地址,失败返回MAP_FAILED,即(void *)-1
====================================================

int munmap(void *addr, size_t length);
// 释放内存映射
- addr: 要释放的内存的首地址
- length: 要释放的内存的大小
#include <sys/mman.h>
#include <stdio.h>
#include <fcntl.h>
#include <sys/types.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <wait.h>

int main() {

    int fd = open("text.txt", O_RDWR);
    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) {  // 父进程读
        wait(NULL);
        char buf[64];
        strcpy(buf, (char *)ptr);
        printf("read data: %s\n", buf);
    }
    else if (pid == 0) {// 子进程写
        strcpy((char *)ptr, "hello, child");
    }

    munmap(ptr, size);  // 释放内存映射
    return 0;
}

  • POSIX(Portable Operating System Interface for Computing Systems)是由IEEE 和ISO/IEC 开发的一簇标准
  • System V,曾经也被称为AT&T System V,是Unix操作系统众多版本中的一支
  • System V 以及POSIX 对信号量、消息队列、共享内存等进程之间共享方式提供了自己的解决方案
  • 3种System V进程间通信方式:信号量、消息队列和共享内存。它们都是由AT&T System V2版本的UNIX引入的,所以统称为System V IPC

下面列举的是System V的实现版本

信号量

int semget(key_t key, int num_sems, int sem_flags);
// 创建一个新的信号量,或者获取一个已经存在的信号量
- key: 用来标识一个全局唯一的信号量集,要用信号量通信的进程需要使用相同的key来创建/获取该信号量
- num_sems: 要创建/获取的信号量集中信号量的数目,创建时必须指定,获取可以设置为0
- sem_flags: 指定一组标志,低端的9个比特是该信号量的权限,其格式和含义都与open的mode参数相同
// 成功返回信号量集的标识符,失败返回-1,并设置errno
======================================================

int semop(int sem_id, struct sembuf *sem_ops, size_t num_sem_ops);
// 执行PV操作
// 与每个信号量关联的一些重要的内核变量
unsigned short semval;  // 信号量的值
unsigned short semzcnt; // 等待信号量变为0的进程数量
unsigned short semncnt; // 等待信号量值增加的进程数量
pid_t sempid;           // 最后一次执行semop操作的进程ID

- sem_id: 由semget调用返回的信号量集标识符
- sem_ops: 指向一个sembuf结构体类型的数组
struct sembuf {
    unsigned short int sem_num; // 信号集中信号量的编号
    short int sem_op;   // 指定操作类型,正整数、0、负整数
    short int sem_flg;  // IPC_NOWAIT非阻塞
};
    + semop大于0,则semop将被操作的信号量的值semval增加semop
    + semop等于0,表示这是一个“等待0”操作,如果此时信号量的值是0,则成功返回。如果信号量的值不是0,则失败返回或者阻塞进程以等待信号量变为0
    + semop小于0,表示对信号量值进行减操作
- num_sem_ops: 指定要执行的操作个数
======================================================

int semctl(int sem_id, int sem_num, int command, ...);
// 对信号量进行直接控制
- sem_id: 由semget调用返回的信号量集标识符
- sem_num: 指定被操作的信号量在信号集中的编号
- command: 指定要执行的操作
    + IPC_STAT: 将信号集关联的数据结构复制到semun.buf中
    + IPC_SET: 将semun.buf中的部分成员复制到信号集关联的数据结构中
    + IPC_RMID: 立即移除信号量集,唤醒所有等待该信号量集的进程
- 第四个参数由用户自定义,给出推荐格式

union semun {
    int val;    // 用于SETVAL命令
    struct semid_ds* buf;   // 用于IPC_STAT和IPC_SET命令
    unsigned short* array;  // 用于GETALL和SETALL命令
    struct seminfo* __buf;  // 用于IPC_INFO命令
};

semget的调用者可以给其key参数传递一个特殊的键值IPC_PRIVATE(其值为0),这样无论该信号量是否已经存在,semget都将创建一个新的信号量,并不是进程私有的,子进程也可以访问


消息队列

消息队列是在两个进程之间传递二进制块数据的一种简单有效的方法,每个数据块都有一个特定的类型,接收方可以根据类型有选择地接收数据,而不一定像匿名管道和有名管道那样必须以先进先出的方式接收数据

#include <sys/msg.h>
int msgget(key_t key, int msgflg);
// 创建一个消息队列,或者获取一个已有的消息队列
- key: 用来标识一个全局唯一的消息队列
- msgflg:9个权限标志组成,用法和创建文件时使用的mode模式标志是一样的
    - IPC_CREAT: 如果消息队列对象不存在,则创建之,否则则进行打开操作
// 成功返回消息队列的标识符,失败返回-1,并设置errno
======================================================
        
int msgsnd(int msqid, const void* msgp, size_t msgsz, int msgflg);
// 把一条消息添加到消息队列中
- msqid: 由msgget调用返回的消息队列标识符
- msgp: 指向一个准备发送的消息,消息必须被定义为如下类型
struct mymsg {
    long mtype;	// 消息类型
    char mtext[1];	// 消息数据
};
- msgsz: 消息数据部分mtext的长度
- msgflg: 控制msgsnd的行为,仅支持IPC_NOWAIT标志,即以非阻塞的方式发送消息。默认情况下,发送消息时如果消息队列满了,则msgsnd将阻塞,若IPC_NOWAIT标志被指定,则msgsnd将立即返回并设置errno为EAGAIN
// 成功返回0,失败返回-1,并设置errno
======================================================
    
ssize_t msgrcv(int msqid, void* msgp, size_t msgsz, long msgtype, int msgflg);
// 从消息队列中获取消息
- msqid: 由msgget调用返回的消息队列标识符
- msgp: 存储接收的数据
- msgsz: 消息数据部分mtext的长度
- msgtype: 指定接收何种类型的消息
	- msgtyp等于0,读取消息队列中的第一个消息
    - msgtyp大于0,读取消息队列中第一个类型为msgtype的消息
// 成功返回0,失败返回-1,并设置errno
======================================================

int msgctl(ing msqid, int cmd, struct msqid_ds *buf);
// 控制消息队列的某些属性
- msqid: 由msgget调用返回的消息队列标识符
- cmd: 指定要执行的命令
    - IPC_STAT: 将消息队列关联的数据结构复制到buf中
    - IPC_SET: 将buf中的部分成员复制到消息队列关联的数据结构中
    - IPC_RMID: 立即移除消息队列,唤醒所有等待读消息和写消息的进程

共享内存

共享内存允许两个或者多个进程共享物理内存的同一块区域(段)。由于一个共享内存段会成为一个进程用户空间的一部分,因此这种IPC机制无须内核介入。一个进程将数据复制进共享内存中,这些数据就能被共享同一个段的进程使用

#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);
// 创建一段新的共享内存,或者获取一个既有的共享内存段的标识,数据会初始化为0
- key: 通过这个参数找到或者创建一个共享内存,一般使用十六进制的非0- size: 共享内存的大小,如果是获取已存在的共享内存,可以设置为0
- shmflg: 共享内存的属性,与open的mode参数相同
    访问权限
    创建: IPC_CREAT
    判断是否存在: IPC_EXCL,要和IPC_CREAT一起使用
// 成功返回共享内存的引用ID,失败返回-1,设置错误号
==============================================

void *shmat(int shmid, const void *shmaddr, int shmflg);
// 和当前进程关联
- shmid: 共享内存的标识,由shmget返回值获取
- shmaddr: 申请的共享内存的起始地址,给NULL,由操作系统选择
- shmflg: 对共享内存的操作
    SHM_RDONLY: 读权限,必须有
   	SHM_REMAP: 如果shmaddr已经关联到一段共享内存上,则重新关联
    0: 读写权限
// 成功返回共享内存的起始地址,失败返回(void *)-1并设置errno
==============================================

int shmdt(const void *shmaddr);
// 解除当前进程和共享内存的关联
- shmid: 共享内存的首地址
// 成功返回0,失败返回-1并设置errno
==============================================

int shmctl(int shmid, int cmd, struct shmid_ds *buf);
// 控制共享内存的某些属性
- shmid: 共享内存标识符
- cmd: 要执行的命令
    - IPC_STAT: 将共享内存相关的内核数据结构复制到buf
    - IPC_SET: 将buf中的部分成员复制到共享内存相关的内核数据结构中
    - IPC_RMID: 将共享内存打上删除的标记,这样当最后一个使用它的进程调用shmdt将它从进程中分离时,该共享内存就被删除了
==============================================

key_t ftok(const char *pathname, int proj_id);
// 根据指定的路径和值生成一个共享内存的key
- pathname: 指定路径 
- proj_id: 只需要1字节,一般给一个字符

写端

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

    // 3. 写数据
    char *str = "hello world";
    memcpy(ptr, str, strlen(str) + 1);	// 带上结尾的'\0'

    printf("press any key to continue...\n");
    getchar();

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

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

读端

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

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

    printf("press any key to continue...\n");
    getchar();

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

    // 5. 删除共享内存
    shmctl(shmid, IPC_RMID, NULL);
    return 0;
}
  • 共享内存不需要文件,共享一段内存
  • 内存映射需要文件,共享一个文件

IPC命令

3种System V IPC进程间通信方式都使用一个全局唯一的键值(key)来描述一个共享资源,当程序调用semget/shmget/msgget时,就创建了这些共享资源的一个实例

Linux提供ipcs命令,以观察当前系统上拥有哪些共享资源实例

ipcs -m // 打印出使用共享内存进行进程间通信的信息
ipcrm -M shmkey // 移除用shmkey创建的共享内存段
ipcrm -m shmid  // 移除用shmid标识的共享内存段

信号

信号码 信号名 事件
2 SIGINT 当用户按下Ctrl+C组合键时,用户终端向正在运行中的由该终端启动的程序发送此信号。默认终止进程
3 SIGQUIT 当用户按下Ctrl+\组合键时,用户终端向正在运行中的由该终端启动的程序发送此信号。默认终止进程
9 SIGKILL 无条件终止进程,该信号不能被忽略、处理和阻塞。可以杀死任何进程
11 SIGSEGV 指示进程进行了无效内存访问(段错误)。默认终止进程并产生core文件
13 SIGPIPE Broken pipe 向一个没有读端的管道写数据。终止进程
17 SIGCHLD 子进程结束时,父进程会收到这个信号。默认忽略此信号
18 SIGCONT 如果进程已停止,则使其继续运行。继续/忽略
19 SIGSTOP 暂停进程,此信号不能被忽略、处理和阻塞

信号的5种默认处理动作

  • Term: 终止进程
  • Ign: 忽略这个信号
  • Core: 终止进程,并产生一个core文件
  • Stop: 暂停进程
  • Cont: 继续执行当前被暂停的进程

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

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

kill/raise/abort函数

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

int kill(pid_t pid, int sig);
// 给任何进程或进程组pid,发送任何信号sig
- pid
    > 0: 将信号发送给指定的进程
    = 0: 将信号发送给当前的进程组
    = -1: 将信号发送给每一个有权限接受这个信号的进程
    < -1: pid=某个进程组id取反
- sig: 信号的编号或宏值,0表示不发送信号
====================================================

int raise(int sig);
// 给当前进程发送信号,成功返回0,失败返回非0
====================================================

#include <stdlib.h>
void abort(void);
// 发送SIGABRT信号给当前进程,杀死当前进程

信号集

多个信号可使用一个信号集的数据结构sigset_t来表示

未决信号集阻塞信号集都是用64位的位图来表示 某个信号产生,未决信号集中对应位置就置1,然后与阻塞信号集中对应位置比较,如果阻塞信号集中的位置是1,说明要阻塞该信号,则信号仍处于未决状态,直到阻塞解除;否则不阻塞,信号可以被处理。默认是不阻塞的,可以手动设置 > 只能操作阻塞信号集

操作自定义的阻塞信号集

#include <signal.h>
int sigemptyset(sigset_t *set);
// 清空数据集中数据,置为0
// 成功返回0,失败返回-1,设置错误号

int sigfillset(sigset_t *set);
// 所有标志位置1

int sigaddset(sigset_t *set, int signum);
// 设置某个标志位为1,表示阻塞这个信号

int sigdelset(sigset_t *set, int signum);
// 设置某个标志位为0,表示不阻塞这个信号

int sigismember(const sigset_t *set, int signum);
// 判断某个信号是否阻塞
// 返回值: 返回1表示被阻塞,0表示不阻塞,-1表示调用失败

操作内核中的阻塞信号集

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,设置错误号(EFAULT/EINVAL)
====================================================

int sigpending(sigset_t *set);
// 获取进程当前被挂起的信号集
- set: 传出参数,保存被挂起的信号集
- 返回值: 成功返回0,失败返回-1

进程即使多次接收到同一个被挂起的信号,sigpending函数也只能反应一次。当该信号再次被使能时,该信号的处理函数也只被触发一次

示例

设置进程信号掩码后,被屏蔽的信号将不能被进程接收。如果给进程发送一个被屏蔽的信号,则操作系统将该信号设置为进程的一个被挂起的信号,如果取消对被挂起信号的屏蔽,则它能立即被进程接收到

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

int main() {
    sigset_t sigset;
    sigemptyset(&sigset);   // 先清空  
    sigaddset(&sigset, SIGINT);
    sigaddset(&sigset, SIGQUIT);    // 将2 3信号添加到信号集中

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

    int num = 0;
    while (1) {
        num++;
        sigset_t pendingset;
        sigemptyset(&pendingset);   // 先清空
        sigpending(&pendingset);    // 获取未决信号集
        for (int i = 1; i <= 32; i++) { // 只看前32位
            if ((sigismember(&pendingset, i)) == 1) {
                printf("1");    // 对应信号被阻塞
            }
            else if ((sigismember(&pendingset, i)) == 0) {
                printf("0");    // 对应信号没被阻塞
            }
            else {
                perror("sigpending");
                exit(0);
            }
        }
        printf("\n");
        sleep(1);
        if (num == 10) {    // 解除阻塞
            sigprocmask(SIG_UNBLOCK, &sigset, NULL);
        }
    }

    return 0;
}
00000000000000000000000000000000
00000000000000000000000000000000
^C
01000000000000000000000000000000    // SIGINT被阻塞
01000000000000000000000000000000
01000000000000000000000000000000
^\
01100000000000000000000000000000    // SIGQUIT被阻塞
01100000000000000000000000000000
01100000000000000000000000000000
01100000000000000000000000000000
01100000000000000000000000000000
// 输出10次后解除阻塞,SIGINT信号被处理,程序结束

sigaction函数

可以设置信号处理函数的系统调用

int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
- signum: 要捕获的信号类型
- act: 信号处理方法
- oldact: NULL
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 <signal.h>
#include <sys/time.h>
#include <stdio.h>
#include <stdlib.h>

void myalarm(int num) {
    printf("catch signal: %d\n", num);
    printf("-----\n");
}

int main() {
    struct sigaction act;
    act.sa_flags = 0;   // 使用sa_handler
    act.sa_handler = myalarm;
    sigemptyset(&act.sa_mask);  // 清空临时阻塞信号集
    sigaction(SIGALRM, &act, NULL);

    // 过3秒后,每隔2秒定时一次
    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);
    printf("timer start\n");
    while (1);  // 死循环
    return 0;
}
timer start
catch signal: 14    // 捕捉到SIGALRM信号
-----
catch signal: 14
-----

SIGCHLD信号

产生条件

  • 子进程终止时
  • 子进程接收到SIGSTOP信号暂停
  • 子进程处于暂停态,接收到SIGCONT继续执行时

父进程默认会忽略该信号

解决僵尸进程问题

alarm函数

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

非阻塞,无论进程处于何种状态,alarm都会计时

#include <unistd.h>
unsigned int alarm(unsigned int seconds);
// 设置定时器,定时到了会给当前进程发送SIGALARM信号
- seconds: 定时时间,0表示定时器无效
// 返回值: 之前没有定时器,返回0,之前有定时器,返回剩余时间

SIGALRM: 信号码14,默认终止当前进程,每个进程有且只有唯一一个定时器

setitimer函数

#include <sys/time.h>
int setitimer(int which, const struct itimerval *new_value, struct itimerval *old_value);
- 功能: 设置定时器,精度us,可以周期性定时
- 返回值: 成功返回0,失败返回-1
- which: 定时器以什么时间计时
    - ITIMER_REAL: 真实时间,计时到了发送SIGALRM信号
    - ITIMER_VIRTUAL: 用户时间,计时到了发送SIGVTALRM信号
    - ITIMER_PROF: 以该进程在用户态和内核态下所消耗的时间来计算,计时到了发送SIGPROF信号

- new_value: 定时器的属性
struct itimerval {  // 定时器的结构体
    struct timeval it_interval; /* 时间间隔 */
    struct timeval it_value;    /* 延长多长时间执行定时器 */
};

struct timeval {    // 时间的结构体
    time_t      tv_sec;         /* 秒 */
    suseconds_t tv_usec;        /* 微秒 */
};

- old_value: 记录上一次定时的时间参数,不用就指定NULL

信号处理函数

#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,设置错误号

示例

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

void myalarm(int num) {
    printf("catch signal: %d\n", num);
    printf("-----\n");
}

int main() {
    signal(SIGALRM, myalarm);
    // 过3秒后,每隔2秒定时一次
    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);
    }
    printf("timer start\n");

    getchar();

    return 0;
}
timer start
catch signal: 14
-----
catch signal: 14    // 每隔2秒处理一次
-----

文章作者: kunpeng
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 kunpeng !
  目录