第13章 多进程编程

13.1 fork系统调用

        Linux创建新进程的系统调用是fork函数。

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

pid_t fork(void);
返回值:
每次调用都返回2次:
1、在父进程中返回子进程PID
2、在子进程中返回0
失败时返回-1,并设置errno

        fork复制当前进程,复制后子进程跟父进程具有相同的内存空间,且代码与父进程完成相同。数据的复制采用的是写时复制,即只有在任一进程对数据执行了写操作时,复制才会发生。此外,创建子进程后,父进程中打开的文件描述符默认在子进程中也是打开的,且文件描述符的引用计数加1。父进程的用户根目录、当前工作目录等变量的引用计数均会加1。原进程设置的信号处理函数不再对新进程起作用。

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

int gval = 10;
int main()
{
    pid_t pid;
    int val = 10;
    pid = fork();
    if (pid == 0)   //子进程处理
    {
        val += 2;
    }
    else    //父进程
    {
        val -= 2;
    }
    if (pid == 0)
        printf("child proc, val = %d\n", val);
    else
        printf("parent proc, val = %d\n", val);
    
    return 0;
}

结果:
parent proc, val = 8
child proc, val = 12

13.2 exec系列系统调用

        fork创建子进程后执行的是和父进程相同的程序,子进程往往要调用一种exec函数来执行另一个程序。当进程调用一种exec函数后,该进程的用户空间代码和数据完全被新程序替换,exec函数后的代码不会再被执行。调用exec并不创建新进程,所以调用exec前后该进程的id不变。

#include <unistd.h>
extern char** environ;

带字母l的表示参数需要逐个给出, 以NULL结尾。如: execl("/bin/ls", "ls", "-a", NULL);

带字母v的表示参数可以以数组的形式给出。如: char *arr = {"ls", "-a", NULL}; execv("/bin/ls", arr);

带字母p的表示文件路径可以只填写文件名。如: execvp("ls", arr);

带字母e的表示新进程(就是替换子进程的那个进程)的环境变量可以在envp[]数组中自定义。如execve("ls", arr, envp);

        exec系列函数是不返回的,只有在出错时返回-1,并设置errno。如果没出错,原程序中exec调用之后的代码都不会执。exec系列函数的关系图如下:

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

int main()
{
    int pid, status;
    pid = fork();
    if (pid == 0)
    {
        char *argv[] = {"echo", "THIS", "IS", "ECHO", 0};
        execvp("echo", argv);
        printf("exec failed!\n");
    }
    else{
        printf("parent waiting\n");
        wait(&status);
        printf("the child exited with status %d\n", status);
    }
    return 0;
}

结果:(说明exec函数后面没有执行)
parent waiting
THIS IS ECHO
the child exited with status 0

 

13.3 处理僵尸进程

        僵尸进程:子进程结束,父进程没有回收子进程、释放子进程占用的内核资源,此子进程处于僵尸状态,被称为僵尸进程。

        父进程可以调用waitwaitpid函数来回收子进程,销毁僵尸进程。

13.3.1 wait函数--阻塞

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

//可以等待任意子进程终止
pid_t wait(int* status);
参数:
status:保存子进程的退出状态信息
返回值:成功时返回子进程ID,失败时返回-1并设置errno

sys/wait.h文件中定义了几个宏来帮助解释子进程的退出状态信息: 

         wait函数将阻塞父进程,直到该进程的子进程,结束为止。下面是使用wait的实例:

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

int main()
{
    int status;
    pid_t pid = fork();
    if (pid == 0)   
        return 3;
    else
    {
        printf("child PID : %d\n", pid);
        wait(&status);
        if (WIFEXITED(status))
        {
            printf("child send one : %d\n", WEXITSTATUS(status));
        }
    }
    return 0;
}
结果:
child PID : 124959
child send one : 3

13.3.2 waitpid函数--非阻塞

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

pid_t waitpid(pid_t pid, int* status, int options);
参数:
pid:等待终止的子进程的PID。若传-1,则与wait函数相同,可以等待任意子进程终止。
status:保存子进程的退出状态信息
options:控制waitpid函数的行为。常用值为WNONHANG,表示waitpid是非阻塞的。
返回值:成功时返回子进程ID(或0),失败时返回-1并设置errno

        waitpid函数是非阻塞的:如果pid指定的目标子进程还没有结束或意外终止,则waitpid立即返回0;如果目标子进程正常退出,则waitpid返回该子进程的PID

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

int main()
{
    int status;
    pid_t pid = fork();
    if (pid == 0)  
    {
        sleep(5);
        return 100;
    } 
    else
    {
        while(!waitpid(-1, &status, WNOHANG))
        {
            sleep(3);
            puts("sleep 3s!");  //即使子进程没终止,也会打印这句话,因外waitpid的非阻塞性
        }
        if (WIFEXITED(status))
        {
            printf("child send %d\n", WEXITSTATUS(status));
        }
    }
    return 0;
}
结果:
sleep 3s!
sleep 3s!
child send 100

13.4 管道

        管道是父进程和子进程间通信的常用手段。

        创建一根管道后,父子进程之间能够传递数据,利用的是fork调用后复制两个管道文件描述符,如下图所示:(管道和套接字一样,属于操作系统,并非属于进程的资源,因此,fork复制的并非管道,而是管道的文件描述符

下面是利用pipe创建一跟管道进行父子进程通信的代码示例:

#include <stdio.h>
#include <unistd.h>
#define BUF_SIZE 30

int main()
{
    int fds[2];
    char str1[] = "who are you?";
    char str2[] = "I'm Lee!";
    char buf[BUF_SIZE] = {0};

    //创建一个管道,fd[0]读,fd[1]写
    pipe(fds);    //对于网络通信,也可以用socketpair(AF_UNIX, SOCK_STREAM, 0, fds);
    pid_t pid = fork();
    if (pid == 0)
    {
        write(fds[1], str1, sizeof(str1));    //发送str1
        //sleep(2);
        read(fds[0], buf, BUF_SIZE);          //读取管道内的数据
        printf("child process output: %s\n", buf);  //打印读取的数据
    }
    else
    {
        read(fds[0], buf, BUF_SIZE);
        printf("father process output: %s\n", buf);
        write(fds[1], str2, sizeof(str2));
        //sleep(2);
    }
    return 0;
}
结果:
child process output: who are you?

实际运行的结果跟我么想象的不一样,是因为:数据进入管道后成为无主数据,子进程先是把数据发送到管道,接着又从管道读取数据,读取的时候不会区分这个数据是谁发的,只要管道内有数据就能读。想要使结果正常,就要把代码中的sleep注释放开。

        为了实现双向通信,可以创建2个管道,各自负责不同的数据流动即可

实现代码如下:

#include <stdio.h>
#include <unistd.h>
#define BUF_SIZE 30

int main()
{
    int fds1[2], fds2[2];
    char str1[] = "who are you?";
    char str2[] = "I'm Lee!";
    char buf[BUF_SIZE] = {0};

    //创建2个管道
    pipe(fds1), pipe(fds2);
    pid_t pid = fork();
    if (pid == 0)
    {
        write(fds1[1], str1, sizeof(str1));
        read(fds2[0], buf, BUF_SIZE);
        printf("child process output: %s\n", buf);
    }
    else
    {
        read(fds1[0], buf, BUF_SIZE);
        printf("father process output: %s\n", buf);
        write(fds2[1], str2, sizeof(str2));
    }
    return 0;
}
结果:
father process output: who are you?
child process output: I'm Lee!

        不过,管道只能用于有关联的2个进程(如父子进程)间的通信。而system V IPC(消息队列,共享内存、信号量)能用于无关联的多个进程之间的通信,因为它们都使用一个全局唯一的键值来标识一条管道。但有一种特殊的管道名为FIFO,也叫命名管道,也能用于无关联进程之间的通信,但在网络编程中使用的不多

13.5 信号量

13.5.1 信号量原语

        当多个进程同时访问系统上的某个资源时,需要考虑进程的同步问题,确保任意时刻只有一个进程可以拥有对资源的独占式访问。信号量实现了这一保护机制。

        信号量(semaphore)表示资源的数目,主要用于实现进程间的互斥与同步,它只能取自然数值并且只支持两种操作:等待(wait)和信号(signal)。由于在Linux中,等待和信号具有特殊含义,所以对信号量的这两种操作常被称呼为P、V操作,P、V操作是两种原子操作。V操作会增加信号量SV的数值,P操作会减少它P操作是用在进入临界区之前,V操作是用在退出临界区之后,这两个操作必须成对出现。假设有信号量SV,则对它的P、V操作含义如下:

  • P(SV):如果SV的值大于0,就将它减1;如果SV的值为0,则挂起进程的执行。
  • V(SV):如果有其他进程因为等待SV而挂起,则唤醒之;如果没有,则将SV加1。

信号量跟信号没有任何关系。信号量又分为两种:二进制信号量和计数信号量。最常用、最简单的信号量是二进制信号量,只能取0和1两个值,二进制信号量又称为互斥锁。P、V操作解释如下:

P(sema)     //sema:信号量
{
    while(sema == 0);   //当信号量为0时,进行等待操作,此时进程被挂起,直到sema不为0
    sema = sema - 1;    //sema大于0,进程进入临界区,sema值减1
}
V(sema)
{
    sema = sema + 1;    //进程执行完任务,从临界区出来,sema的值增加1
}

13.5.2 信号量函数

#include <semaphore.h>
typedef union
{
  char __size[__SIZEOF_SEM_T];
  long int __align;
} sem_t;

功能:初始化一个信号量
int sem_init(sem_t *sem, int pshared, unsigned int value);
参1:sem信号量
参2:pshared取0用于线程间;取非0(一般为1)用于进程间
参3:value指定信号量初值

功能:以原子操作的方式将信号量的值减1 --
int sem_wait(sem_t *sem);              

功能:为sem_wait()的非阻塞版,不进行等待。如果信号量计数大于0,则信号量立即减1并返回0,否则立即返回-1
int sem_trywait(sem_t *sem);        

功能:与 sem_wait() 类似,只不过 abs_timeout 指定一个阻塞的时间上限。
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout); 
参2:abs_timeout采用的是绝对时间,由自1970-01-01 00:00:00 +0000(UTC) 秒数和纳秒数构成。
struct timespec {
    time_t tv_sec;        /* 秒 */
    long   tv_nsec;       /* 纳秒 */
};
sem_timedwait存在缺陷:假设当前系统时间是1565000000(2019-08-05 18:13:20),
sem_timedwait传入的阻塞等待的时间戳是1565000100(2019-08-05 18:15:00),
那么sem_timedwait就需要阻塞1分40秒(100秒),若在sem_timedwait阻塞过程中,
中途将系统时间往前修改成1500000000(2017-07-14 10:40:00),
那么sem_timedwait此时就会阻塞2年多!
返回值:
如果信号量大于0,则对信号量进行递减操作并立马返回正常;
如果信号量小于0,则阻塞等待,当阻塞超时时返回失败(errno 设置为 ETIMEDOUT)。
 

功能:以原子操作的方式将信号量的值加1 ++
int sem_post(sem_t *sem);            

功能:销毁一个信号量
int sem_destroy(sem_t *sem);   

功能:读取sem中信号量计数,放到sval指向的整数上
int sem_getvalue(sem_t * sem, int * sval);
    
所有这些函数在成功时都返回 0;错误保持信号量值没有更改,-1 被返回,并设置 errno 

13.5.3 semget、semop、semctl系统调用

        Linux信号量的API主要包含3个系统调用:semget、semop、semctl。它们都被设计为操作一组信号量,即信号集,而不是单个信号量

13.5.3.1 semget系统调用

        semget用于创建一个新的信号量集,或者获取一个已经存在的信号量集:

#include <sys/sem.h>
int semget(key_t key, int num_sems, int sem_flags);
参数解释:
key:键值,标识一个全局唯一的信号量集。取IPC_PRIVATE时有如下含义:
无论该信号量是否存在,都将创建一个新的信号量。
num_sems:指定要创建/获取的信号量集中信号量的数目。
如果是创建信号量,则该值必须被指定;如果是获取,则可以设为0
sem_flags:信号量的创建方式或权限,跟open()的mode参数一样。
它低端的9个bit位表示权限,如:0644表示-rw-r--r-。
它还可以取如下值按位或来创建信号集:
IPC_CREAT:如果信号量集不存在,则创建一个信号量,否则获取。
IPC_EXCL:如果要创建的信号量集已存在,则出错返回 -1,并设置errno为EEXIST。
返回值:成功时返回一个正整数,即信号量集标识符(类似open()返回文件描述符);失败时返回-1并设置errno。

        如果semget用于创建信号量集,则与之关联的内核数据结构体semid_ds将被创建并初始化:

#include <sys/sem.h>
struct ipc_perm
{
    key_t key;    //键值
    uid_t uid;    //所有者的有效用户ID
    gid_t gid;    //所有者的有效组ID
    uid_t cuid;    //创建者的有效用户ID
    gid_t cgid;    //创建者的有效组ID
    mode_t mode;    //访问权限
};

struct semid_ds
{
    struct ipc_perm sem_perm;        //信号量的操作权限
    unsigned long int sem_nsems;    //该信号量集中的信号量数目
    time_t sem_otime;                //最后一次调用semop的时间
    time_t sem_ctime;                //最后一次调用semctl的时间
};

        使用示例:

13.5.3.2 semop系统调用

        semop用来改变信号量的值,即执行P、V操作。首先介绍与每个信号量关联的一些重要的内核变量

unsigned short semval;    //信号量的值
unsigned short semzcnt;    //等待信号量值变为0的进程数
unsigned short semncnt;    //等待信号量增加的进程数
unsigned short semadj;    //跟踪进程对信号量的修改情况
pid_t sempid;            //最后一次执行semop操作的进程ID

        semop对信号量的操作实际上就是对这些内核变量的操作,其定义如下:

#include <sys/sesm.h>
int semop(int sem_id, struct sembuf* sem_ops, size_t num_sem_ops);
参数解释:
sem_id:semget()返回的信号量集标识符
sem_ops:sembuf类型结构体数组指针
num_sem_ops:sem_ops数组元素的个数
返回值:成功返回0;失败返回-1并设置errno,失败时,sem_ops数组中指定的所有操作都不执行。
struct sembuf
{
    unsigned short int sem_num;    //待操作信号在信号集中的编号。第一个信号的编号为0
    short int sem_op;    //
    short int sem_flag;
};
sem_flag:取如下值:
IPC_NOWAIT:无论信号量操作是否成功,semop都将立即返回。类似于非阻塞I/O操作
SEM_UNDO:当进程退出时取消正在进行的semop操作。
sem_op:取值如下:
A、大于0:被操作信号量值semval增加sem_op。调用进程对信号量集有写权限。若设置了SEM_UNDO标志,系统将更新semadj变量。
B、等于0:等待信号量为0。调用进程要对信号量集有读权限。如果信号量值为0,则立即返回成功;否则返回失败或阻塞进程以等待信号量变为0。当未指定IPC_NOWAIT标志,则信号量的semzcnt值加1,进程被投入睡眠直到下列3个条件之一发生:
(1)semval等于0。此时系统将该信号量的semzcnt值减1
(2)信号量所在的信号量集被进程移除。此时semop()返回失败,errno设为EIDRM。
(3)调用被信号中断。此时semop()返回失败,errno设为EINT,且系统将该信号量的semzcnt值减1.
C、小于0:被操作信号量值semval减去sem_op的绝对值。调用进程对信号量集有写权限。如果semval>=|sem_op|:若设置了SEM_UNDO标志,系统将更新semadj变量。如果semval<|sem_op|:semop()返回失败或者阻塞以等待信号量可用。当未指定IPC_NOWAIT标志,则信号量的semncnt值加1,进程被投入睡眠直到下列3个条件之一发生:
(1)semval >= |sem_op|。此时semncnt值减1,且semval减去|sem_op|,如果设置了SEM_UNDO标志,系统将更新semadj变量。
(2)信号量所在的信号量集被进程移除。此时semop()返回失败,errno设为EIDRM。
(3)调用被信号中断。此时semop()返回失败,errno设为EINT,且系统将该信号量的semncnt值减1.

        semop对数组sem_ops中的每个成员按照数组顺序依次执行操作,并且该过程是原子操作

13.5.3.3 semctl系统调用

        semctl用来控制信号量。

#include <sys/sem.h>
int semctl(int sem_id, int sem_num, int command, ...);
参数解释:
sem_id:semget()返回的信号量集标识符。
sem_num:被操作的信号量在信号量集中的编号,第一个信号量的编号是0。
command:操作命令。通常是下面的两个值:
(1)SETVAL:把信号量初始化为一个已知的值。这个值通过union semun中的val成员设置,其作用是在信号量第一次使用前对它进行设置。
(2)IPC_RMID:用于删除一个已经无需继续使用的信号量集标识符。
第4个参数:
union semun
{
    int val;    //用于SETVAL命令
    struct semid_ds* buf;   //用于IPC_STAT和IPC_SET命令
    unsigned short* array;  //用于GETALL和SETALL命令
    struct seminfo* __buf;  //用于IPC_INFO命令
};

struct seminfo
{
    int semmap;         //linux内核没有使用
    int semmni;         //系统最多可以拥有的信号量集数目
    int semmns;         //系统最多可以拥有的信号量数目
    int semmnu;         //Linux内核没有使用
    int semmsl;         //一个信号量集最多允许包含的信号量数目
    int semopm;         //semop一次最多能执行的sem_op操作数目
    int semume;         //Linux内核没有使用
    int semusz;         //sem_undo结构体大小
    int semvmx;         //最大允许的信号量值
    int semaem;         //最多允许的UNDO次数(带SEM_UNDO标志的semop操作次数)
}

返回值:成功返回值取决于command参数;失败时返回-1并设置errno。

        semctl()的command参数可取值如下表

        GETNCNT、GETPID、GETVAL、GETZCNT、SETVAL操作的是单个信号量,而其他操作针对的是在整个信号量集,此时semctl的参数sem_num被忽略

这三个系统调用的使用示例:

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

union semun
{
    int val;
    struct semid_ds* buf;
    unsigned short int* array;
    struct seminfo* __buf;
};

//op为-1时执行P操作,为1时执行V操作
void pv(int sem_id, int op)
{
    struct sembuf sem_b;
    sem_b.sem_num = 0;
    sem_b.sem_op = op;
    sem_b.sem_flg = SEM_UNDO;
    semop(sem_id, &sem_b, 1);
}

int main()
{
    int sem_id = semget(IPC_PRIVATE, 1, 0666);  //创建含有一个信号量的信号量集,对该信号量集有读写权限

    union semun sem_un;
    sem_un.val = 1;     //将信号量的值设为1
    semctl(sem_id, 0, SETVAL, sem_un); //设置第0个信号量的值

    pid_t id = fork();
    if(id < 0)
    {
        return 1;
    }
    else if (id == 0)
    {
        printf("child try to get binary sem\n");
        //在父子进程间共享IPC_PRIVATE信号量的关键在于二者都可以操作该信号量集的标识符
        pv(sem_id, -1);
        printf("child get the sem and would release it after 5s\n");
        sleep(5);
        pv(sem_id, 1);
        exit(0);
    }
    else
    {
        printf("parent try to get binary sem\n");
        pv(sem_id, -1);
        printf("parent get the sem and would release it after 5s\n");
        sleep(5);
        pv(sem_id, 1);
    }
    waitpid(id, NULL, 0);
    semctl(sem_id, 0, IPC_RMID, sem_un);    //删除信号量集
    return 0;
}
结果:
parent try to get binary sem
parent get the sem and would release it after 5s
child try to get binary sem
child get the sem and would release it after 5s

13.6 共享内存

        共享内存是最高效的IPC机制,因为它不涉及进程之间的任何数据传输,但是,我们必须同步进程对共享内存的访问,防止产生竞态条件。因此,共享内存通常和其他的进程通信方式一起使用。

13.6.1 共享内存的几个系统调用

        Linux共享内存主要包括4个系统调用:shmget、shmat、shmdt、shmctl

        首先介绍一下生成键值的ftok函数。每一个共享存储段都有一个对应的键值(key)相关联(消息队列、信号量也同样需要):

#include <sys/ipc.h>
key_t ftok(const char* path, int id);
参数解释:
path: 传入一个路径(一般是当前路径“ . ”)
id:0~255之间随便填写一个数(要做通信的话通信的另外一端要与这个数保持一致才能找到对应的ipcID)
返回值:成功返回键值(相当于32位的int)。出错返回-1
例如:key_t key = ftok(".", 66);

13.6.1.1 shmget系统调用

        shmget创建一段新的共享内存,或者获取一段已经存在的共享内存。

#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);
参数解释:(和semget()的参数差不多)
key:键值,标识一段全局唯一的共享内存。取IPC_PRIVATE时有如下含义:
无论该共享内存是否存在,都将创建一个新的共享内存。
size:指定共享内存大小,单位是字节。
如果是新建内存,则必须指定size值;如果是获取已有内存,则size值可为0
shmflg:和semget()的sem_flags参数相同,为所需要的操作和权限。
shmflg的值为IPC_CREAT:如果不存在key值的共享存储空间,且权限不为0,则创建共享存储空间,并返回一个共享存储标识符。如果存在,则直接返回共享存储标识符。
shmflg的值为 IPC_CREAT | IPC_EXCL:如果不存在key值的共享存储空间,且权限不为0,则创建共享存储空间,并返回一个共享存储标识符。如果存在,则产生错误,errno为EEXIST。
shmflg额外支持如下两个标志:
(1)SHM_HUGETLB,系统将使用“大页面”来为共享内存分配空间。
(2)SHM_NORESERVE,不为共享内存保留交换分区(swap空间)。
返回值:成功时返回一个正整数,是共享内存的标识符;shmget失败时返回-1,并设置errno。
例如:int id = shmget(key,4096,IPC_CREAT|IPC_EXCL|0666);创建一个大小为4096个字节的权限为0666(所有用户可读可写)的共享存储空间,并返回一个整形共享存储标识符,如果key值已经存在有共享存储空间了,则出错返回-1。

        如果shmget用于创建共享内存,则这段共享内存的所有字节都被初始化为0, 与之关联的内核数据结构shmid_ds将被创建并被初始化:

struct shmid_ds
{
    struct ipc_per shm_perm;    //共享内存的操作权限
    size_t shm_segsz;    //共享内存的大小,单位是字节
    __time_t shm_atime;    //对这段内存最后一次调用shmat的时间
    __time_t shm_dtime;    //对这段内存最后一次调用shmdt的时间
    __time_t shm_ctime;    //对这段内存最后一次调用shmctl的时间
    __pid_t shm_cpid;    //创建者的PID
    __pid_t shm_lpid;    //最后一次执行shmat或shmdt操作的进程的PID
    shmatt_t shm_nattach;    //目前关联到此共享内存的进程数量
};
    

13.6.1.2 shmat系统调用(at:attach)

        如果一个进程已创建或打开一个共享内存,则在需要使用它时,要调用函数shmat()把该共享内存连接到进程上,即要把待使用的共享内存映射到进程空间。函数shmat()通过系统调用sys_shmat()实现。

#include <sys/shm.h>
void* shmat(int shm_id, const void* shm_addr, int shmflg);    
参数解释:
shm_id:shmget()的返回值,即共享内存的标识。
shm_addr:指定共享内存出现在进程内存地址的什么位置,直接指定为NULL让内核自己决定一个合适的地址位置。
shmflg:为对数据的操作,如果指定为SHM_RDONLY则以只读方式连接此段,其他值为读写方式连接此段。
返回值:成功返回指向共享存储段的指针;错误返回-1。
例如:char *addr  = shmat(id, NULL, 0);就会返回第一个可用的共享内存地址的指针的值给addr。

         shmat()成功时,将修改内核数据结构shmid_ds的部分字段:

  • 将shm_nattach加1
  • 将shm_lpid设置为调用进程的PID
  • 将shm_atime设置为当前的时间

13.6.1.3 shmdt系统调用(dt:detach)

        shmdt()可以断开共享内存与进程的连接

#include <sys/shm.h>
int shmdt(const void* shm_addr);
参数解释:
shm_addr:为共享存储段的地址,即调用shmat()时的返回值。。
返回值:成功时返回0;失败返回-1并设置errno.

        shmdt()成功时,将修改内核数据结构shmid_ds的部分字段:

  • 将shm_nattach减1
  • 将shm_lpid设置为调用进程的PID
  • 将shm_dtime设置为当前的时间

13.6.1.4 shmctl系统调用(ctl:control)

        shmctl()可以对共享内存进行一些控制

#include <sys/shm.h>
int shmctl(int shm_id, int command, struct shmid_ds* buf);
参数解释:
shm_id:shmget()的返回值。
command:控制命令,见下表。常用的有如下几个:
    IPC_RMID:删除共享内存
    IPC_STAT:得到共享内存的状态,把共享内存的shmid_ds结构复制到buf中
    IPC_SET:改变共享内存的状态,把buf所指的shmid_ds结构中的uid、gid、mode复制到共享内存的shmid_ds结构内。
buf:shmid_ds类型结构体指针。
返回值:成功时返回值取决于command参数;失败时返回-1并设置errno
例如:int ret = shmctl(id, IPC_RMID,NULL);删除id号的共享存储空间

        shmctl()支持的命令(command参数):

        共享内存的总结:

优点:我们可以看到使用共享内存进行进程间的通信真的是非常方便,而且函数的接口也简单,数据的共享还使进程间的数据不用传送,而是直接访问内存,也加快了程序的效率。同时,它也不像匿名管道那样要求通信的进程有一定的父子关系。

缺点:共享内存没有提供进程间同步和互斥的功能。所以,共享内存通常是要与信号量结合使用。

        下面以生产者消费者模式为例,使用信号量与共享内存实现进程间通信:

定义两个信号量S1=1和S2=0,S1=1表示生产者进程可以写共享资源,S1=0则表示不可写;S2=1表示消费者进程可以读共享资源,S2=0则表示不可读。

whlie(true)
{
	P(S1)
	消息写到共享资源
	V(S2)
}
 
消费者进程:
whlie(true)
{
	P(S2)
	从共享资源读取消息
	V(S1)
}

shm_sem.h 

#ifndef _SHM_SEM_H
#define _SHM_SEM_H

#define SHM_KEY 38111
#define SEM_KEY 38222
#define SHM_SIZE 2048
#define SEM_NUM 2

int sem_p(int semid, int semnum);
int sem_v(int semid, int semnum);
int get_semval(int semid, int semnum);

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

#endif

shm_sem.cpp


#include <stdio.h>  
#include <string.h>  
#include <stdlib.h>  
#include <unistd.h> 
#include <errno.h> 
#include <sys/types.h>  
#include <sys/ipc.h>  
#include <sys/sem.h>
#include "shm_sem.h"

int sem_p(int semid, int semnum)
{
    struct sembuf sem_b;
    sem_b.sem_num = semnum;     //待操作信号在信号集中的编号
    sem_b.sem_op = -1;
    sem_b.sem_flg = SEM_UNDO;

    return semop(semid, &sem_b, 1);
}

int sem_v(int semid, int semnum)
{
    struct sembuf sem_b;
    sem_b.sem_num = semnum;
    sem_b.sem_op = 1;
    sem_b.sem_flg = SEM_UNDO;

    return semop(semid, &sem_b, 1);
}

//获取第semnum个信号量的值
int get_semval(int semid, int semnum)
{
    return semctl(semid, semnum, GETVAL);
}

producer.cpp

#include <stdio.h>  
#include <string.h>  
#include <stdlib.h>  
#include <unistd.h> 
#include <errno.h> 
#include <sys/types.h>  
#include <sys/ipc.h>  
#include <sys/shm.h> 
#include <sys/sem.h> 
 
#include "shm_sem.h"

int main()
{
    int ret;
    int shmid;
    int semid;
    void* ptrShm = NULL;
    struct shmid_ds shm_ds;
    struct semid_ds sem_ds;
    char buf[1024] = {0};
    union semun arg;
    unsigned short semvalArr[SEM_NUM] = {1, 0};
    
    memset(&shm_ds, 0, sizeof(shm_ds));
    memset(&sem_ds, 0, sizeof(sem_ds));

    //创建含有2个信号量的信号集,且具有读写权限
    semid = semget((key_t)SEM_KEY, SEM_NUM, IPC_CREAT|IPC_EXCL|0666);
    if (semid == -1)
    {
        if (errno == EEXIST)    //信号量集已存在
        {
            //获取已存在的信号量集
            semid = semget((key_t)SEM_KEY, 0, IPC_CREAT|0660);
            if (semid == -1)    //获取失败
            {
                printf("semget() error: %s\n", strerror(errno));
                return -1;
            }
            printf("get sem success. semid=%d\n", semid);
        }
        else    //创建信号量集失败
        {
            printf("semget() error: %s\n", strerror(errno));
            return -1;
        }
    }
    else    //第一次创建信号量集,要进行初始化
    {
        printf("first create sem success. semid=%d\n", semid);
        arg.array = semvalArr;
        //使0号信号量的值为1,1号信号量的值为0
        ret = semctl(semid, 0, SETALL, arg);
        if (ret == -1)  //设置信号量的值失败
        {
            printf("semctl() SETALL error: %s\n", strerror(errno));
            ret = semctl(semid, 0, IPC_RMID);   //删除信号量集
            if (ret == -1)
            {
                printf("semctl() error: %s\n", strerror(errno));
            }
            printf("semctl() success. Sem is deleted.\n"); 
		    return -1;
        }
        printf("init sem success. semval[0]=[%d] semval[1]=[%d].\n", get_semval(semid, 0), get_semval(semid, 1));
    }

    //创建共享内存,拥有读写权
    shmid = shmget((key_t)SHM_KEY, SHM_SIZE, IPC_CREAT|IPC_EXCL|0666);
    if (shmid == -1)    
    {
        if (errno == EEXIST)    //共享内存已存在
        {
            printf("creat shm fali, try to get shm\n");
            shmid = shmget((key_t)SHM_KEY, SHM_SIZE, IPC_CREAT|0666);   //获取共享内存
            if (shmid == -1)    //获取共享内存失败
            {
                printf("get shm fail\n");
                return -1;
            }
        }
        else    //创建失败
        {
            printf("creat shm fail, try to get shm fail\n");
            return -1;
        }
    }
    printf("get shm success, shmid = %d\n", shmid);

    //把共享内存连接到进程,具有读写权
    ptrShm = shmat(shmid, NULL, !SHM_RDONLY);
    if (ptrShm == NULL) //连接失败
    {
        printf("shmat error : %s\n", strerror(errno));
        goto FAILED;    //删除创建的共享内存
    }
    printf("shmat sucess, begin to write shm\n");
    while(1)
    {
        memset(&buf, 0, sizeof(buf));
        //从终端将内容写入buf中
        if (fgets(buf, sizeof(buf)-1, stdin) < 0)
        {
            printf("fget errno\n");
            fflush(stdin);
            continue;
        }
        fflush(stdin);

        //1、P操作,0号信号量--,值变为0,除此之外的写操作会失败
        ret = sem_p(semid, 0);
        if (ret == -1)
        {
            printf("P sem fail\n");
            goto FAILED;
        }

        //2、P操作成功,然后将buf内的数据写入共享内存,实现生产消息
        memset(ptrShm, 0, SHM_SIZE);    //初始化共享内存
        memcpy((char*)ptrShm, buf, strlen(buf));

        //3、写完之后,V操作,1号信号量++,值变为1,消费者此刻才可以消费数据
        ret = sem_v(semid, 1);
        if (ret == -1)
        {
            printf("V sem fail\n");
            goto FAILED;
        }
        printf("producer write result : %s\n", buf);
        //输入exit退出循环
        if (memcmp(buf, "exit", 4) == 0)
        {
            break;
        }
    }

    //断开共享内存和进程的连接
    ret = shmdt(ptrShm);
    if (ret == -1)
    {
        printf("shmdt fail\n");
        goto FAILED;
    }
    printf("shmdt success\n");
    goto FAILED;    //正常结束,然后删除共享内存和信号量

FAILED: //删除共享内存和信号量集
    ret = shmctl(shmid, IPC_RMID, &shm_ds);
    if (ret == -1)
    {
        printf("delete shm errno : %s\n", strerror(errno));
    }
    printf("delete shm success\n");

    ret = semctl(semid, 0, IPC_RMID);
    if (ret == -1)
    {
        printf("delete sem error : %s\n", strerror(errno));
    }
    printf("delete sem success\n");

    return -1;
}

consumer.cpp 

#include <stdio.h>  
#include <string.h>  
#include <stdlib.h>  
#include <unistd.h> 
#include <errno.h> 
#include <sys/types.h>  
#include <sys/ipc.h>  
#include <sys/shm.h> 
#include <sys/sem.h> 
 
#include "shm_sem.h"

int main()
{
    int ret;
    int shmid;
    int semid;
    void* ptrShm = NULL;
    struct shmid_ds shm_ds;
    struct semid_ds sem_ds;
    char buf[1024] = {0};
    union semun arg;
    unsigned short semvalArr[SEM_NUM] = {1, 0};
    
    memset(&shm_ds, 0, sizeof(shm_ds));
    memset(&sem_ds, 0, sizeof(sem_ds));

    //创建含有2个信号量的信号集,且具有读写权限
    semid = semget((key_t)SEM_KEY, SEM_NUM, IPC_CREAT|IPC_EXCL|0666);
    if (semid == -1)
    {
        if (errno == EEXIST)    //信号量集已存在
        {
            //获取已存在的信号量集
            semid = semget((key_t)SEM_KEY, 0, IPC_CREAT|0660);
            if (semid == -1)    //获取失败
            {
                printf("semget() error: %s\n", strerror(errno));
                return -1;
            }
            printf("get sem success. semid=%d\n", semid);
        }
        else    //创建信号量集失败
        {
            printf("semget() error: %s\n", strerror(errno));
            return -1;
        }
    }
    else    //第一次创建信号量集,要进行初始化
    {
        printf("first create sem success. semid=%d\n", semid);
        arg.array = semvalArr;
        //使0号信号量的值为1,1号信号量的值为0
        ret = semctl(semid, 0, SETALL, arg);
        if (ret == -1)  //设置信号量的值失败
        {
            printf("semctl() SETALL error: %s\n", strerror(errno));
            ret = semctl(semid, 0, IPC_RMID);   //删除信号量集
            if (ret == -1)
            {
                printf("semctl() error: %s\n", strerror(errno));
            }
            printf("semctl() success. Sem is deleted.\n"); 
		    return -1;
        }
        printf("init sem success. semval[0]=[%d] semval[1]=[%d].\n", get_semval(semid, 0), get_semval(semid, 1));
    }

    //创建共享内存,拥有读写权
    shmid = shmget((key_t)SHM_KEY, SHM_SIZE, IPC_CREAT|IPC_EXCL|0666);
    if (shmid == -1)    
    {
        if (errno == EEXIST)    //共享内存已存在
        {
            printf("creat shm fali, try to get shm\n");
            shmid = shmget((key_t)SHM_KEY, SHM_SIZE, IPC_CREAT|0666);   //获取共享内存
            if (shmid == -1)    //获取共享内存失败
            {
                printf("get shm fail\n");
                return -1;
            }
        }
        else    //创建失败
        {
            printf("creat shm fail, try to get shm fail\n");
            return -1;
        }
    }
    printf("get shm success, shmid = %d\n", shmid);

    //把共享内存连接到进程,具有读写权
    ptrShm = shmat(shmid, NULL, !SHM_RDONLY);
    if (ptrShm == NULL) //连接失败
    {
        printf("shmat error : %s\n", strerror(errno));
        goto FAILED;    //删除创建的共享内存
    }
    printf("shmat sucess, begin to read shm\n");
    while(1)
    {
        //1、P操作,1号信号量--,值变为0,除此之外的读操作会失败
        ret = sem_p(semid, 1);
        if (ret == -1)
        {
            printf("P sem fail\n");
            goto FAILED;
        }

        //2、P操作成功,然后将共享内存的数据读入buf,实现消费消息
        memset(&buf, 0, sizeof(buf));
        memcpy(buf, (char*)ptrShm, sizeof(buf));

        //3、读完之后,V操作,0号信号量++,值变为1,生产者者此刻才可以生产数据
        ret = sem_v(semid, 0);
        if (ret == -1)
        {
            printf("V sem fail\n");
            goto FAILED;
        }
        printf("consumer read result : %s\n", buf);

        //读到exit退出循环
        if (memcmp(buf, "exit", 4) == 0)
        {
            break;
        }
    }

    //断开共享内存和进程的连接
    ret = shmdt(ptrShm);
    if (ret == -1)
    {
        printf("shmdt fail\n");
        goto FAILED;
    }
    printf("shmdt success\n");
    goto FAILED;    //正常结束,然后删除共享内存和信号量
    
FAILED: //删除共享内存和信号量集
    ret = shmctl(shmid, IPC_RMID, &shm_ds);
    if (ret == -1)
    {
        printf("delete shm errno : %s\n", strerror(errno));
    }
    printf("delete shm success\n");

    ret = semctl(semid, 0, IPC_RMID);
    if (ret == -1)
    {
        printf("delete sem error : %s\n", strerror(errno));
    }
    printf("delete sem success\n");

    return -1;
}

编译:
g++ -I./ shm_sem.cpp producer.cpp -o producer
g++ -I./ shm_sem.cpp consumer.cpp -o consumer

执行结果:
[root@localhost test2]# ./producer
first create sem success. semid=327680
init sem success. semval[0]=[1] semval[1]=[0].
get shm success, shmid = 524291
shmat sucess, begin to write shm
11111111111111111111111111111111
producer write result : 11111111111111111111111111111111

exit
producer write result : exit

shmdt success
delete shm success
delete sem success

[root@localhost test2]# ./consumer
get sem success. semid=327680
creat shm fali, try to get shm
get shm success, shmid = 524291
shmat sucess, begin to read shm
consumer read result : 11111111111111111111111111111111

consumer read result : exit

shmdt success
delete shm errno : Invalid argument
delete shm success
delete sem error : Invalid argument
delete sem success

13.6.2 共享内存的POSIX方法

        首先介绍一下6.5小节中mmap本身提供的进程间的通信方式:无亲缘进程间通信亲缘进程间通信

(1)通过匿名内存映射(无需内存映射文件)提供亲缘进程间的通信

        mmap提供匿名内存映射机制,即将mmap的flags参数指定为:MAP_SHARED | MAP_ANONYMOUS(MAP_SHARED 是必填的)。这样就彻底避免了内存映射文件的创建和打开,简化了对文件的操作。匿名内存映射机制的目的就是为了提供一个穿越父子进程间的内存映射区,很方便的提供了亲缘进程间的通信。

#include <iostream>
#include <cstring>
#include <cerrno>
 
#include <unistd.h>
#include <fcntl.h>
#include <sys/mman.h>
 
using namespace std;
 
int main(int argc, char **argv)
{
    int *memPtr;
    memPtr = (int *) mmap(NULL, sizeof(int), PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, 0, 0);
    if (memPtr == MAP_FAILED)
    {
        cout<<"mmap failed..."<<strerror(errno)<<endl;
        return -1;
    }
 
    *memPtr = 0;
 
    if (fork() == 0)
    {
        *memPtr = 1;
        cout<<"child:set memory "<<*memPtr<<endl;
        return 0;
    }
 
    sleep(1);
    cout<<"parent:memory value "<<*memPtr<<endl;
 
    return 0;
}
结果:
child:set memory 1
parent:memory value 1

(2)通过内存映射文件提供无亲缘进程间的通信

        不同的进程通过对同一内存映射文件进行读写,实现进程间的通信。

//进程1
#include <iostream>
#include <cstring>
#include <errno.h>
 
#include <unistd.h>
#include <fcntl.h>
#include <sys/mman.h>
 
using namespace std;
 
#define  PATH_NAME "/memmap"
 
int main()
{
    int *memPtr;
    int fd;
 
    fd = open(PATH_NAME, O_RDWR | O_CREAT, 0666);
    if (fd < 0)
    {
        cout<<"open file "<<PATH_NAME<<" failed...";
        cout<<strerror(errno)<<endl;
        return -1;
    }
 
    ftruncate(fd, sizeof(int));   //将fd所指文件的大小改为sizeof(int)
 
    memPtr = (int *)mmap(NULL, sizeof(int), PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    close(fd);
 
    if (memPtr == MAP_FAILED)
    {
        cout<<"mmap failed..."<<strerror(errno)<<endl;
        return -1;
    }
 
    *memPtr = 111;
	cout<<"process:"<<getpid()<<" send:"<<*memPtr<<endl;
 
    return 0;
}
 
//进程2
#include <iostream>
#include <cstring>
#include <errno.h>
 
#include <unistd.h>
#include <fcntl.h>
#include <sys/mman.h>
 
using namespace std;
 
#define  PATH_NAME "/memmap"
 
int main()
{
    int *memPtr;
    int fd;
 
    fd = open(PATH_NAME, O_RDWR | O_CREAT, 0666);
    if (fd < 0)
    {
        cout<<"open file "<<PATH_NAME<<" failed...";
        cout<<strerror(errno)<<endl;
        return -1;
    }
 
    memPtr = (int *)mmap(NULL, sizeof(int), PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    close(fd);
 
    if (memPtr == MAP_FAILED)
    {
        cout<<"mmap failed..."<<strerror(errno)<<endl;
        return -1;
    }
 
    cout<<"process:"<<getpid()<<" receive:"<<*memPtr<<endl;
 
    return 0;
}
结果:
./p1
process:122255 send:111
./p2
process:122298 receive:111

上面的代码都没进行同步操作,在实际的使用过程要考虑到进程间的同步通常会用信号量来进行共享内存的同步

        上面讲到,通过打开同一个文件,mmap可以实现无关进程之间的内存共享。接下来介绍另外一种使用mmap在无关进程之间共享内存的方式,该方式无需任何文件的支持,但它需要使用shm_open函数来创建或打开一个POSIX共享内存对象。下图描述了两者的差别:

使用POSIX内存方法有以下2个步骤:

  1. 通过shm_open()创建或打开一个POSIX共享内存对象
  2. 调用mmap()将共享内存映射到当前进程的地址空间

POSIX共享内存对象的特殊操作函数只有创建(打开)和删除两个函数,其他对共享内存的操作都是通过已有的函数进行的。

#include <sys/mman.h>

创建或打开一个POSIX共享内存对象。一定要用ftruncate把文件大小设置为共享内存大小
int shm_open(const char* name, int oflag, mode_t mode);
参数解释:
name:要打开或创建的共享内存文件名。由于shm_open打开或操作的文件都是位于/dev/shm目录的,因此name不能带路径。
oflag:打开的文件操作属性。O_RDONLY,O_RDWR,O_CREAT,O_EXCL,O_TRUNC。其中O_RDONLY和O_RDWR标志必须且仅能存在一项。
mode:用于设置创建的共享内存区对象的权限属性。和open以及其他POSIX IPC的xxx_open函数不同的是,该参数必须一直存在,如果oflag参数中没有O_CREAT标志,该位可以置0。如:0666
返回值:成功返回一个文件描述符,用于后续的mmap调用,将共享内存关联到调用进程。失败时返回-1并设置errno。

重置文件大小为length。任何open打开的文件都可以用这个函数,不限于shm_open打开的文件。
int ftruncate(int fd, off_t length);
返回值:成功返回0;失败返回-1并设置errno


删除共享内存对象。
int shm_unlink(const char* name);
参数解释:
name:shm_open()的第一个参数。当所有使用name指定的共享内存对象的进程都使用munmap将它从进程中分离后,系统将销毁这个共享内存对象所占据的资源。
返回值:成功返回0;失败返回-1并设置errno

13.6.3 共享内存实例

         下面利用共享内存实现一个多进程的群聊服务器程序:一个子进程处理一个客户连接,将所有的客户端消息放到共享内存中。整个逻辑如下图:

 主要的数据结构和函数:

 服务户端父进程大致逻辑:

int main()
{
    …
    epollfd = epoll_create(5);
    addfd(epollfd, serv_sock);
    //创建用来传输信号的管道
    ret = socketpair(PF_UNIX, SOCK_STREAM, 0, sig_pipefd);
    setnonblocking(sig_pipefd[1]);
    addfd(epollfd, sig_pipefd[0]);  //注册pipefd[0]上的可读事件
    addSig(SIGHUP, sigHandler); //终端接口检测到一个连接断开,发送此信号
    addSig(SIGPIPE, sigHandler);   //往读端被关闭的socke或管道写数据,会发送此信号。
    addSig(SIGTERM, sigHandler);   //接收到kill命令
    addSig(SIGINT, sigHandler);    //用户按下中断键(Delete或Ctrl+C)
    //创建共享内存,保存要发送给客户端的数据
    shmfd = shm_open(shm_name, O_CREAT | O_RDWR, 0666);
    ret = ftruncate(shmfd, USER_LIMIT * BUFFER_SIZE);   //设置共享内存的大小
    //将共享内存映射到进程地址空间
    share_mem = (char*)mmap(NULL, USER_LIMIT * BUFFER_SIZE, PROT_READ|PROT_WRITE, MAP_SHARED, shmfd, 0);
    close(shmfd);   //关闭共享内存描述符
    while(!stop_server)
    {
        int number = epoll_wait(epollfd, events, MAX_EVENT_NUMBER, -1);
        for (int i = 0; i < number; i++)
        {
            int sockfd = events[i].data.fd;
            if (sockfd == serv_sock)
            {
                int connfd = accept(serv_sock, (struct sockaddr*)&client_address, &clnt_addrlen);
                if (user_count >= USER_LIMIT)   //当前客户数超标
                {
                    send(connfd, info, strlen(info), 0);
                    close(connfd);
                    continue;
                }
                //保存第user_count个客户连接的相关数据
                users[user_count].address = client_address;
                users[user_count].connfd = connfd;
                //在主进程和子进程间建立管道,以传递必要的数据
                ret = socketpair(PF_UNIX, SOCK_STREAM, 0, users[user_count].pipefd);
                //创建子进程
                pid_t pid = fork();
                if (pid == 0)  //子进程
                {
                    //close掉不能用的Epollfd,listenfd,users[user_count].pipefd[0]
                    //sig_pipefd[0],sig_pipefd[1]
                    run_child(user_count, users, share_mem);
                    //子进程业务处理完毕
                    munmap((void*)share_mem, USER_LIMIT*BUFFER_SIZE);
                }
                else    //父进程
                {
                    //close掉connfd,users[user_count].pipefd[1],
                    addfd(epollfd, users[user_count].pipefd[0]);
                    users[user_count].pid = pid;
                    //记录新的客户连接在数组users中的索引值,建立进程pid和该索引值之间的映射关系
                    sub_process[pid] = user_count;
                    user_count++;
                }
            }
            //如果是管道的一端有数据可读,那么处理信号事件
            else if ((sockfd == sig_pipefd[0]) && (events[i].events & EPOLLIN))
            {
                char signals[1024] = {0};
                ret = recv(sig_pipefd[0], signals, sizeof(signals), 0);
                for(int j=0; j<ret; j++)
                {
                    switch(signals[i])
                    {
                        case SIGCHLD://子进程退出,表示有某个客户端关闭了连接
                        //回收子进程
                        while((pid = waitpid(-1, &stat, WNOHANG)) > 0)
                        {
                            ...
                        }
                        //接收到下面这两个信号,终止程序
                        case SIGTERM:
                        case SIGINT:
                            kill(pid, SIGTERM); //向pid进程发送SIGTERM信号
                    }
                }
            }
            //某个子进程通过管道向父进程发送了数据
            else if (events[i].events & EPOLLIN)
            {
                //读取管道数据,child变量记录了是哪个客户连接有数据到达。
                ret = recv(sockfd, (char*)&child, sizeof(child), 0);
                //告知除第child号客户连接外的其他客户连接:第child号客户发了消息,你们赶紧从共享内存中读取它发的内容
               for (int j = 0; j < user_count; j++)
               {
                    if (users[j].pipefd[0] != sockfd)
                    {
                         send(users[j].pipefd[0], (char*)&child, sizeof(child), 0);
                    }
               }
            }
        }
    }
    //服务器程序结束,释放所有资源
}

服务户端子进程大致逻辑:

int child_epollfd = epoll_create(5);
int connfd = users[idx].connfd;
int pipefd = users[idx].pipefd[1];
addfd(child_epollfd, users[idx].pipefd[1]);
addfd(child_epollfd, connfd);
//child_term_handler负责改变stop_child值
addSig(SIGTERM, child_term_handler, false);
while(!stop_child)
{
    int number = epoll_wait(child_epollfd, events, MAX_EVENT_NUMBER, -1);
    for (int i = 0; i < number; i++)
    {
        int sockfd = events[i].data.fd;
        if ((sockfd == connfd) && (events[i].events & EPOLLIN))
        {
            ret = recv(connfd, share_mem + idx*BUFFER_SIZE, BUFFER_SIZE-1, 0);
            //根据ret值先判断子进程是否需要停止(stop_child)
            …
            //成功读取客户数据,然后通过管道来通知主进程进行处理
            send(users[idx].pipefd[1], (char*)&idx, sizeof(idx), 0);
        }
        //主进程通过管道通知本进程将第client个客户的发送发送了,该子进程要把这个数据发送给自己负责的客户端
        else if ((sockfd == users[idx].pipefd[1]) && (events[i].events & EPOLLIN))
        {
            //获取群聊中发送数据的客户的编号client
            ret = recv(users[idx].pipefd[1], (char*)&client, sizeof(client), 0);
            //根据ret值先判断子进程是否需要停止(stop_child)
            ...
            //在共享内存中找到该客户发送的数据,然后发送给自己负责的客户端
            send(connfd, share_mem + client * BUFFER_SIZE, BUFFER_SIZE, 0);
        }
    }
};
//客户端连接断开,该子进程也该销毁了,释放它所打开的资源
close(connfd);
close(users[idx].pipefd[1]);
close(child_epollfd);

整体实现代码

#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>
#include <stdlib.h>
#include <sys/epoll.h>
#include <signal.h>
#include <sys/wait.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>

#define USER_LIMIT 5
#define BUFFER_SIZE 1024
#define FD_LIMIT 65535
#define MAX_EVENT_NUMBER 1024
#define PROCESS_LIMIT 65536

//处理一个客户端连接必要的数据
struct client_data
{
    sockaddr_in address;    //客户端socket地址
    int connfd;             //socket文件描述符
    pid_t pid;              //处理这个连接的子进程的PID
    int pipefd[2];          //和父进程通信用的管道。父进程用pipefd[0]收发数据,子进程用pipefd[1]收发数据
};

static const char* shm_name = "/my_shm";
int sig_pipefd[2];
int epollfd;
int listenfd;
int shmfd;
char* share_mem = 0;

//客户连接数组。进程用客户连接的编号来索引这个数组,即可获得相关的客户连接数据
client_data* users = 0;
//子进程和客户连接的映射关系表。用进程的PID来索引这个数组,即可取得该进程所处理的客户连接的编号
int* sub_process = 0;
//当前客户数量
int user_count = 0;
bool stop_child = false;

//设置文件描述符为非阻塞
int setnonblocking(int fd)
{
    int flag = fcntl(fd, F_GETFL);
    fcntl(fd, F_SETFL, flag | O_NONBLOCK);
}

//将描述符加入到epollfd中
void addfd(int epfd, int fd)
{
    struct epoll_event event;
    event.data.fd = fd;
    event.events = EPOLLIN | EPOLLET;   //使用边缘触发模式
    epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &event);
    setnonblocking(fd); //加入的描述符设为非阻塞
}

//简单的信号处理函数
void sigHandler(int sig)
{
    printf("capture signal, signal is %d\n", sig);
    //保留原来的errno,在函数最后恢复,保证函数的可重入性
    int saveErrno = errno;
    int msg = sig;
    //将信号值发送到管道,通知主循环
    send(sig_pipefd[1], (char*)&msg, 1, 0);
    errno = saveErrno;
}

//为信号绑定处理函数
void addSig(int sig, void(*handler)(int), bool restart=true)
{
    struct sigaction act;
    memset(&act, '\0', sizeof(act));

    act.sa_handler = handler;//设置信号处理函数
    if (restart)    act.sa_flags |= SA_RESTART; //被此信号中断的系统调用自动重启动
    sigfillset(&act.sa_mask);   //初始化信号屏蔽集
    assert(sigaction(sig, &act, NULL) != -1);   //注册信号处理函数
}

void del_resource()
{
    close(sig_pipefd[0]);
    close(sig_pipefd[1]);
    close(listenfd);
    close(epollfd);
    shm_unlink(shm_name);   //删除共享内存
    delete [] users;
    delete [] sub_process;
}

//停止一个子进程
void child_term_handler(int sig)
{
    stop_child = true;
}

//子进程运行的函数。idx指该子进程处理的客户连接的编号,users是保存所有客户连接数据的数组
//share_mem值共享内存的起始地址
int run_child(int idx, client_data* users, char* share_mem)
{
    epoll_event events[MAX_EVENT_NUMBER];
    int child_epollfd = epoll_create(5);
    assert(child_epollfd != -1);
    int connfd = users[idx].connfd;
    addfd(child_epollfd, connfd);
    int pipefd = users[idx].pipefd[1];  //假设子进程用pipefd[1]读数据
    addfd(child_epollfd, pipefd);

    int ret;
    //为SIGTERM设置信号处理函数
    addSig(SIGTERM, child_term_handler, false);   //终端执行kill命令会发出SIGTERM信号
    while(!stop_child)
    {
        int number = epoll_wait(child_epollfd, events, MAX_EVENT_NUMBER, -1);
        if ((number < 0) && (errno != EINTR))   //epoll_wait失败且使信号导致的系统中断
        {
            printf("epoll failuer\n");
            break;
        }

        for (int i = 0; i < number; i++)
        {
            int sockfd = events[i].data.fd;
            //子进程负责的客户连接有数据到达
            if ((sockfd == connfd) && (events[i].events & EPOLLIN))
            {
                //初始化对应的读缓存
                memset(share_mem + idx*BUFFER_SIZE, '\0', BUFFER_SIZE);
                //将客户数据读取到对应的读缓存中。读缓存是共享内存的一段,开始于idx*BUFFER_SIZE处,长度为BUFFER_SIZE字节。
                //因此,各个客户连接的读缓存是共享的
                ret = recv(connfd, share_mem + idx*BUFFER_SIZE, BUFFER_SIZE-1, 0);
                if (ret < 0)
                {
                    if (errno != EAGAIN)    //读时发生内部错误,结束进程
                    {
                        stop_child = true;
                    }
                }
                else if (ret == 0)  //客户端断开连接
                {
                    stop_child = true;
                }
                else
                {
                    //成功读取客户数据,然后通过管道来通知主进程进行处理
                    send(pipefd, (char*)&idx, sizeof(idx), 0);
                }
            }
            //主进程通过管道通知本进程将第client个客户的数据发送到本进程负责的客户端
            else if ((sockfd == pipefd) && (events[i].events & EPOLLIN))
            {
                int client = 0;
                ret = recv(sockfd, (char*)&client, sizeof(client), 0);
                if (ret < 0)
                {
                    if (errno != EAGAIN)    //读时发生内部错误,结束进程
                    {
                        stop_child = true;
                    }
                }
                else if (ret == 0)  //客户端断开连接
                {
                    stop_child = true;
                }
                else
                {
                    //成功读取主进程发来的共享内存起始编号,然后子进程读取共享内存数据并发送给客户端
                    send(connfd, share_mem + client * BUFFER_SIZE, BUFFER_SIZE, 0);
                }
            }
            else    //其他情况不做处理
            {
                continue;
            }
        }
    }
    close(connfd);
    close(pipefd);
    close(child_epollfd);
    return 0;
}

int main()
{
    struct sockaddr_in serv_addr;
    socklen_t addr_sz;
 
    memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_port = htons(12345);
    serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
 
    int serv_sock = socket(PF_INET, SOCK_STREAM, 0);
    assert(serv_sock >= 0);
 
    int ret = bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
    assert(ret != -1);
 
    ret = listen(serv_sock, 5);
    assert(ret != -1);
    
    user_count = 0;
    users = new client_data[USER_LIMIT + 1];
    sub_process = new int[PROCESS_LIMIT];
    //初始化sub_process
    for (int i = 0; i < PROCESS_LIMIT; i++)
    {
        sub_process[i] = -1;
    }

    epoll_event events[MAX_EVENT_NUMBER];
    epollfd = epoll_create(5);
    assert(epollfd != -1);
    addfd(epollfd, serv_sock);

    //创建管道
    /*sockpair函数创建的管道是全双工的,不区分读写端
    此处我们假设sig_pipefd[1]为写端,sig_pipefd[0]为读端
    */
    ret = socketpair(PF_UNIX, SOCK_STREAM, 0, sig_pipefd);
    assert(epollfd != -1);
    setnonblocking(sig_pipefd[1]);
    addfd(epollfd, sig_pipefd[0]);  //注册pipefd[0]上的可读事件

    //为信号绑定处理函数(将信号值发送到管道,通知主循环)。
    addSig(SIGHUP, sigHandler); //终端接口检测到一个连接断开,发送此信号
    addSig(SIGPIPE, sigHandler);   //往读端被关闭的socke或管道写数据,会发送此信号。
    addSig(SIGTERM, sigHandler);   //接收到kill命令
    addSig(SIGINT, sigHandler);    //用户按下中断键(Delete或Ctrl+C)

    bool stop_server = false;
    bool terminate = false;

    //创建共享内存,作为所有客户socket连接的读缓存,即:共享内存用来保存要发送给客户端的数据
    shmfd = shm_open(shm_name, O_CREAT | O_RDWR, 0666);
    assert(shmfd != -1);
    ret = ftruncate(shmfd, USER_LIMIT * BUFFER_SIZE);   //设置共享内存的大小
    assert(ret != -1);

    //将共享内存映射到进程地址空间
    share_mem = (char*)mmap(NULL, USER_LIMIT * BUFFER_SIZE, PROT_READ|PROT_WRITE, MAP_SHARED, shmfd, 0);
    assert(share_mem != MAP_FAILED);
    close(shmfd);   //关闭共享内存描述符

    while(!stop_server)
    {
        int number = epoll_wait(epollfd, events, MAX_EVENT_NUMBER, -1);
        if ((number < 0) && (errno != EINTR))   //epoll_wait失败且使信号导致的系统中断
        {
            printf("epoll failuer\n");
            break;
        }

        for (int i = 0; i < number; i++)
        {
            int sockfd = events[i].data.fd;
            //新的客户连接请求到来
            if (sockfd == serv_sock)
            {
                struct sockaddr_in client_address;
                socklen_t clnt_addrlen = sizeof(client_address);
                int connfd = accept(serv_sock, (struct sockaddr*)&client_address, &clnt_addrlen);
                
                if (connfd < 0)
                {
                    printf("errno is : %d\n", errno);
                    continue;
                }
                
                if (user_count >= USER_LIMIT)   //当前客户数超标
                {
                    const char* info = "too many users\n";
                    printf("%s\n", info);
                    //向客户端发送连接超标警告,然后主动关闭连接
                    send(connfd, info, strlen(info), 0);
                    close(connfd);
                    continue;
                }
                //保存第user_count个客户连接的相关数据
                users[user_count].address = client_address;
                users[user_count].connfd = connfd;
                //在主进程和子进程间建立管道,以传递必要的数据
                ret = socketpair(PF_UNIX, SOCK_STREAM, 0, users[user_count].pipefd);
                assert(ret != -1);

                     //创建子进程
                pid_t pid = fork();
                if (pid < 0)
                {
                    close(connfd);
                    continue;
                }
                else if (pid == 0)  //子进程
                {
                    //关掉不能用的fd
                    close(epollfd);
                    close(listenfd);
                    close(users[user_count].pipefd[0]);
                    close(sig_pipefd[0]);
                    close(sig_pipefd[1]);
                    run_child(user_count, users, share_mem);
                    //子进程业务处理完毕
                    munmap((void*)share_mem, USER_LIMIT*BUFFER_SIZE);
                    exit(0);
                }
                else    //父进程
                {
                    close(connfd);
                    close(users[user_count].pipefd[1]);
                    addfd(epollfd, users[user_count].pipefd[0]);
                    users[user_count].pid = pid;
                    //记录新的客户连接在数组users中的索引值,建立进程pid和该索引值之间的映射关系
                    sub_process[pid] = user_count;
                    user_count++;
                }
            }
            //如果是管道的一端有数据可读,那么处理信号事件
            else if ((sockfd == sig_pipefd[0]) && (events[i].events & EPOLLIN))
            {
                int sig;
                char signals[1024] = {0};
                ret = recv(sig_pipefd[0], signals, sizeof(signals), 0);
                if (ret <= 0)
                    continue;
                else
                {
                    //每个信号值占1字节,所以按字节来逐个接收信号
                    for(int j=0; j<ret; j++)
                    {
                        switch(signals[i])
                        {
                            //子进程退出,表示有某个客户端关闭了连接
                            case SIGCHLD:
                            {
                                pid_t pid;
                                int stat;
                                while((pid = waitpid(-1, &stat, WNOHANG)) > 0)
                                {
                                    //用子进程的PID取得被关闭的客户连接的编号
                                    int del_user = sub_process[pid];
                                    sub_process[pid] = -1;
                                    if ((del_user < 0) || (del_user > USER_LIMIT))  //该子进程已被回收过
                                    {
                                        continue;
                                    }
                                    //清除第del_user个客户连接使用的相关数据
                                    epoll_ctl(epollfd, EPOLL_CTL_DEL, users[del_user].pipefd[0], 0);
                                    close(users[del_user].pipefd[0]);

                                    users[del_user] = users[--user_count];  //将最后一个客户连接数据移到第del_user的位置。
                                    sub_process[users[del_user].pid] = del_user;    //pid在sub_process中的映射关系也修改
                                }
                                if (terminate && user_count == 0)
                                {
                                    stop_server = true;
                                }
                                break;
                            }
                            //接收到下面这两个信号,终止程序
                            case SIGTERM:
                            case SIGINT:
                            {
                                //没有子进程运行,说明所有客户端都关闭了,此时结束服务器程序
                                printf("kill all the child now\n");
                                if (user_count == 0)
                                {
                                    stop_server = true;
                                    break;
                                }
                                for (int i = 0; i < user_count; i++)
                                {
                                    int pid = users[i].pid;
                                    kill(pid, SIGTERM); //向pid进程发送SIGTERM信号
                                }
                                terminate = true;
                                break;
                            }
                            default:
                                break;
                        }
                    }
                }

            }
            //某个子进程通过管道向父进程发送了数据
            else if (events[i].events & EPOLLIN)
            {
                int child = 0;
                //读取管道数据,child变量记录了是哪个客户连接有数据到达。
                ret = recv(sockfd, (char*)&child, sizeof(child), 0);
                printf("read data from child accross pipe\n");
                if (ret == -1 || ret == 0)
                {
                    continue;
                }
                else
                {
                    /*向除负责处理第child个客户连接的子进程之外的其他子进程发送消息,通知他们赶紧从共享内存中读取数据,然后发给自己负责的客户端
                    相当于一人在群里发消息,其他人都会接收到这个消息
                    */
                    for (int j = 0; j < user_count; j++)
                    {
                        if (users[j].pipefd[0] != sockfd)
                        {
                            printf("send data to child accross pipe\n");
                            send(users[j].pipefd[0], (char*)&child, sizeof(child), 0);
                        }
                    }
                }
            }
        }
    }
    //服务器程序结束,释放所有资源
    del_resource();
    return 0;
}

编译g++ server.cpp -o server -lrt

13.7 消息队列

        消息队列是在两个进程之间传递二进制块数据的一种简单有效的方式,本质是消息的链表,存放在内核中。每个数据块都有一个特定的类型,接收方可以根据类型来有选择的接收数据,而不一定向管道那样必须以先进先出的方式接收数据。消息队列里的消息哪怕进程崩溃了也不会消失。

        Linux消息队列主要包括4个系统调用:msgget、msgsnd、msgrcv、msgctl

13.7.1 msgget系统调用

        msgget用于创建一个消息队列或者获取一个已有的消息队列

#include <sys/msg.h>
int msgget(key_t key, int msgflg);
参数解释:
key:键值。标识一个全局唯一的消息队列
msgflg:消息队列的创建方式或权限。跟semget()的semflgs参数一样。

返回值:
成功时返回一个正整数,即消息队列的标识符;失败时返回-1并设置errno。跟semget()一样。

        如果msgget用于创建消息队列,则与之关联的内核数据结构msqid_ds将被创建并初始化(内核为每个消息队列维护着一个结构,结构名为msqid_ds,里面存放着消息队列的大小,pid,存放时间等一些参数):

struct msqid_ds
{
    struct ipc_perm msg_perm;    //消息队列的操作权限
    time_t msg_stime;            //最后一次调用msgsnd的时间
    time_t msg_rtime;            //最后一次调用msgrcv的时间
    time_t msg_ctime;            //最后一次被修改的时间
    unsigned long __msg_cbytes;  //消息队列中已有的字节数
    msgqnum_t msg_qnum;          //消息队列中已有的消息数
    msglen_t msg_qbytes;         //消息队列允许的最大字节数
    pid_t msg_lspid;             //最后执行msgsnd的进程的PID
    pid_t msg_lrpid;             //最后执行msgrcv的进程的PID
};

13.7.2 msgsnd系统调用

        msgsnd用于把一条消息添加到消息队列中

#include <sys/msg.h>
int msgsnd(int msqid, const void* msg_ptr, size_t msg_sz, int msgflg);
参数:
msqid:msgget()返回值。
msg_ptr:指向一个准备发送的消息。消息必须被定义为如下类型:
    struct msgbuf
    {
        long mtype;    //消息类型,整数。
        chaar mtext[512];    //消息数据
    };
msg_sz:消息的数据部分(mtext)的长度。长度为0表示没有消息数据
msgflg:控制msgsnd行为。通常仅支持IPC_NOWAIT,即:以非阻塞的方式发送消息,且msgsnd()将立即返回并设置errno为EAGAIN。默认情况下:发送消息时如果消息队列满了,则msgsnd()将阻塞。

返回值:
成功时返回0,失败时返回-1并设置errno。
msgsnd()成功时将修改内核数据结构msqid_ds的如下字段:
1、将msg_qnum加1
2、将msg_lspid设置为调用进程的PID
3、将msg_stime设置为当前时间

13.7.3 msgrcv系统调用

        msgrcv用来从消息队列中获取消息

#include <sys/msg.h>
int msgrcv(int msqid, void* msg_ptr, size_t msg_sz, long int msgtype, int msgflg);
参数解释:
msqid:msgget()返回的消息队列标识符。
msg_ptr:指向存储接收的消息的地址
msg_sz:消息数据部分(mtext)的长度
msgtype:指定接收何种类型的消息。可取如下几种值来指定消息类型:
    msgtype = 0 :读取消息队列中的第一个消息
    msgtype > 0 :读取消息队列中的第一个类型为msgtype的消息(除非指定了MSG_EXCEPT)
    msgtype < 0 :读取消息队列中的第一个类型值比msgtype的绝对值小的消息。
msgflg:控制msgrcv()的行为。可取如下标志的按位或:
    IPC_NOWAIT:如果消息队列中没有消息,则msgrcv()立即返回并设置errno为ENOMSG
    MSG_EXCEPT:如果msgtype>0,则接收消息队列种第一个非msgtype类型的消息
    MSG_NOERROR:如果消息数据部分长度超过了msg_sz,就将它截断。

返回值:
成功时返回0,失败时返回-1并设置errno。
msgrcv()成功时将修改内核数据结构msqid_ds的如下字段:
1、将msg_qnum减1
2、将msg_lspid设置为调用进程的PID
3、将msg_rtime设置为当前时间

处于阻塞状态的msgrcv()可能被如下2种异常情况中断:
1、消息队列被移除。此时msgrcv()将立即返回并设置errno为EIDRM
2、程序接收到信号。此时msgrcv()将立即返回并设置errno为EINTR

13.7.4 msgctl系统调用

        msgctl用来控制消息队列的某些属性

#include <sys/msg.h>
int msgctl(int msqid, int command, struct msqid_ds* buf);
参数解释:
msqid:msgget()返回的消息队列标识符。
command:指定要执行的命令,可取值如下表。
buf:有command参数决定。

返回值:
成功时的返回值取决于command参数;失败时返回-1并设置errno

        下面是消息的发送者和接收者。发送者往消息队列里发送消息(只发送一种类型的消息),接收者从消息队列里读取消息。当发送者发送QUIT时删除消息队列,两个程序都退出。

发送者

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<sys/msg.h>
#include<sys/ipc.h>

struct mymsgbuf
{
    long int mtype;
    char mtext[512];
};

int main()
{
    struct mymsgbuf msgbuf;
    key_t key = ftok("/tmp", 66);   //创建键值对
    int id = msgget(key, IPC_CREAT | 0666); //创建消息队列,所有用户可读可写
    if (id == -1)
    {
        printf("create msg error\n");
        return 0;
    }
    char msg[512];
    while(1)
    {
        memset(msg, 0, sizeof(msg));
        msgbuf.mtype = 1;
        printf("input message:");
		fgets(msg, sizeof(msg), stdin);
		strcpy(msgbuf.mtext, msg);

        if (msgsnd(id, (void*)&msgbuf,  512, 0) < 0)
        {
            printf("send msg error \n");
			return 0;
        }

        //输入QUIT终止循环
        if (strncmp(msg, "QUIT", 4) == 0)
            break;
    }
    //删除消息队列
    if (msgctl(id, IPC_RMID, NULL) < 0)
    {
        printf("del msg error \n");
		return 0;
    }
    return 0;
}

接收者

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<sys/msg.h>
#include<sys/ipc.h>

struct mymsgbuf
{
    long int mtype;
    char mtext[512];
};

int main()
{
    struct mymsgbuf msgbuf;
    key_t key = ftok("/tmp", 66);   //创建键值对,必须为66,跟创建时保持一致
    int id = msgget(key, IPC_CREAT | 0666); //获取消息队列,所有用户可读可写
    if (id == -1)
    {
        printf("open msg error\n");
        return 0;
    }
    char msg[512];
    while(1)
    {
        //获取类型为1的消息体
        if (msgrcv(id, (void*)&msgbuf,  512, 1, 0) < 0)
        {
            printf("send msg error \n");
			return 0;
        }
        printf("recv data : %s\n", msgbuf.mtext);
        //如果消息内容是QUIT,表明消息发送者已结束发送,所以这边也要停止接收
        if (strncmp(msgbuf.mtext, "QUIT", 4) == 0)
            break;
    }
    return 0;
}

 运行结果

[root@localhost test]# ./sender 
input message:1111111111
input message:QUIT

[root@localhost test]# ./recver
recv data : 1111111111

recv data : QUIT

        如果程序退出时没有删除消息队列,且队列中有消息,当重新打开队列后,仍能读取队列中的消息

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值