多进程编程——常用调用函数

1.fork系统调用

fork()函数通过系统调用创建一个与原来进程几乎完全相同的进程,也就是两个进程可以做完全相同的事,但如果初始参数或者传入的变量不同,两个进程也可以做不同的事。
一个进程调用fork()函数后,系统先给新的进程分配资源,例如存储数据和代码的空间。然后把原来的进程的所有值都复制到新的新进程中,只有少数值与原来的进程的值不同。相当于克隆了一个自己。

   #include <sys/types.h>
   #include <unistd.h>
   pid_t fork(void);
         返回值:子进程返回0,父进程返回子进程pid,出错返回-1

sdfa

2.exec函数

exec函数替换当前进程映像,执行其他程序。

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

int execl(const char *path, const char *arg, ...
                       /* (char  *) NULL */);
int execlp(const char *file, const char *arg, ...
                       /* (char  *) NULL */);
int execle(const char *path, const char *arg, ...
                       /*, (char *) NULL, char * const envp[] */);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[],
                       char *const envp[]);
                       返回值:若出错,返回-1,若成功,不返回。

在这里插入图片描述
exec函数不会关闭原程序打开的文件描述符,除非该文件描述符被设置类似SOCK_CLOEXEC的属性。

3.处理僵尸进程

多进程程序中,父进程一般需要跟踪子进程的退出状态。因此,当子进程退出运行时,内核不会立即释放该进程的进程表表项,从而满足父进程后续对该子进程退出信息的查询(父进程还在运行)。在子进程运行结束之后,父进程读取其退出状态之前称该子进程处于僵尸态。
另一种使子进程处于僵尸态的情况是:父进程结束或者异常终止,而子进程继续运行,此时子进程的PPID将会被操作系统设置为1,即init进程。init进程接管了该子进程,并等待期结束。在父进程退出之后,子进程退出之前,该子进程处于僵尸状态。
无论何种状况,如果父进程没有正确的处理子进程的返回信息,子进程都将停留僵尸状态并占用内核资源。以下函数在父进程中调用,等待子进程的结束并获取子进程的返回信息熊二避开哪里的僵尸进程的产生,或者是子进程僵尸状态立即结束:

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

pid_t wait(int *wstatus);

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

The wait() system call suspends execution of the calling process until one of its children terminates. The call wait(&wstatus) is equivalent to:
       waitpid(-1, &wstatus, 0);
       The waitpid() system call suspends execution of the calling process until a child specified by pid argument has changed state. By default, waitpid() waits only for terminated children, but this behavior is modifiable via the options argument, as described below.
The value of pid can be:

  • < -1 meaning wait for any child process whose process group ID is equal to the absolute value of pid.
  • -1 meaning wait for any child process.
  • 0 meaning wait for any child process whose process group ID is equal to that of the calling process.
  • 0< meaning wait for the child whose process ID is equal to the value of pid.
含义
WIFEXITED(wstatus)returns true if the child terminated normally, that is, by calling exit(3) or _exit(2), or by returning
WEXITSTATUS(wstatus)returns the exit status of the child. This consists of the least significant 8 bits of the status argument that the child specified in a call to exit(3) or _exit(2) or as the argument for a return statement in main(). This macro should be employed only if WIFEXITED returned true.
WIFSIGNALED(wstatus)returns true if the child process was terminated by a signal.
WTERMSIG(wstatus)returns the number of the signal that caused the child process to terminate. This macro should be employed only if WIFSIGNALED returned true.
WIFSTOPPED(wstatus)returns true if the child process was stopped by delivery of a signal; this is possible only if the call was done using WUNTRACED or when the child is being traced (see ptrace(2)).
WSTOPSIG(wstatus)returns the number of the signal which caused the child to stop. This macro should be employed only if WIFSTOPPED returned true.

The value of options is an OR of zero or more of the following constants:

  • WNOHANG return immediately if no child has exited.

  • WUNTRACED also return if a child has stopped (but not traced via ptrace(2)). Status for traced children
    which have stopped is provided even if this option is not specified.

  • WCONTINUED (since Linux 2.6.10)
    also return if a stopped child has been resumed by delivery of SIGCONT.

4. 进程间通信(System V)

4.1 管道

管道是 UNIX 系统上最古老的 IPC 方法,它在 20 世纪 70 年代早期 UNIX 的第三个版本上就出现了。
**使用管道连接两个进程**

4.1.1 特点:

  • 一个管道是一个字节流。 在使用管道时是不存在消息或消息边界的概念的。从管道中读取数据的进程可以读取任意大小的数据块,而不管写入进程写入管道的数据块的大小是什么。此外,通过管道传递的数据是顺序的——从管道中读取出来的字节的顺序与它们被写入管道的顺序是完全一样的。
  • 从管道中读取数据 试图从一个当前为空的管道中读取数据将会被阻塞直到至少有一个字节被写入到管道中为止。如果管道的写入端被关闭了,那么从管道中读取数据的进程在读完管道中剩余的所有数据之后将会看到文件结束(即 read()返回 0)
  • 管道是单向的 管道的一段用于写入,另一端则用于读取。
  • 可以确保写入不超过 PIPE_BUF 字节的操作是原子的
  • 管道的容量是有限的 管道其实是一个在内核内存中维护的缓冲器,这个缓冲器的存储能力是有限的。一旦管道被填满之后,后续向该管道的写入操作就会被阻塞直到读者从管道中移除了一些数据为止。

4.1.2 创建和使用管道

pipe()系统调用创建一个新管道。

#include <unistd.h>
int pipe(int pipefd[2]);
			Return 0 on success,or -1 on errror

成功的 pipe()调用会在数组 filedes 中返回两个打开的文件描述符:一个表示管道的读取端(filedes[0]),另一个表示管道的写入端(filedes[1])。 使用 pipe()创建完管道之后的情况,其中调用进程通过文件描述符引用了管道的两端。
一般来讲都是使用管道让两个进程进行通信。为了让两个进程通过管道进行连接,在调用完 pipe()之后可以调用 fork()。在fork()期间,子进程会继承父进程的文件描述符的副本,这样就会出现下图的情形。
在这里插入图片描述
虽然父进程和子进程都可以从管道中读取和写入数据,但这种做法并不常见。因此,在fork()调用之后,其中一个进程应该立即关闭管道的写入端的描述符,另一个则应该关闭读取端的描述符。如,如果父进程需要向子进程传输数据,那么它就会关闭管道的读取端的描述符 filedes[0],而子进程就会关闭管道的写入端的描述符 filedes[1],如下图所示
在这里插入图片描述
管道是单向的,要实现父子进程之间的双向数据传输显然需要使用两个管道。

特例:socket编程接口提供了一个创建全双工管道的系统调用:socketpair。

4.2 信号量

一个信号量是一个由内核维护的整数,其值被限制为大于或等于 0。在一个信号量上可以执行各种操作(即系统调用),包括:

  • 将信号量设置成一个绝对值;
  • 在信号量当前值的基础上加上一个数量;
  • 在信号量当前值的基础上减去一个数量;
  • 等待信号量的值等于 0。
    上面操作中的后两个可能会导致调用进程阻塞。当减小一个信号量的值时,内核会将所有试图将信号量值降低到 0 之下的操作阻塞。类似的,如果信号量的当前值不为 0,那么等待信号量的值等于 0 的调用进程将会发生阻塞。不管是何种情况,调用进程会一直保持阻塞直到其他一些进程将信号量的值修改为一个允许这些操作继续向前的值,在那个时刻内核会唤醒被阻塞的进程。

在这里插入图片描述
以下部分对信号量(System V)简单介绍。

在 Linux 上信号量API定义在 sys/sem.h头文件中,主要包含三个系统调用:semget、semop、semctl。他们都被设计为操作一组信号量,即信号量集。

4.2.1 semget系统调用

semget系统调用创建一个新的信号量集,或者获取一个已经存在的信号量集。其定义为:

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>

int semget(key_t key, int nsems, int semflg);

返回值:

  • If successful, the return value will be the semaphore set identifier (a nonnegative integer),
  • otherwise, -1 is returned, with errno indicating the error

参数:

  • key 是一个键值,用来标识一个全局的信号量集。要通过信号量通信的进程需要使用相同的键值来创建/获取该信号量。
  • nsems 参数指定要创建/获取的信号集中信号量的数目。如果是创建信号量,则该值必须被指定:如果是获取已经存在的信号量,则可以把它设置为0
  • semflg 参数是一个位掩码,它指定了施加于新信号量集之上的权限或需检查的一个既有集合的权限,其格式和含义都与系统调用open的mode参数相同。在 semflg中可以通过对下列标记中的零个或多个取 OR 来控制 semget()的操作

               IPC_CREAT 如果不存在与指定的 key 相关联的信号量集,那么就创建一个新集合。
               IPC_EXCL 如果同时指定了 IPC_CREAT 并且与指定的 key 关联的信号量集已经存在,那么返回EEXIST 错误。

如果semget用于创建信号量集,则与之相关联的内核数据结构体sem_ds将被创建并初始化。semid_ds结构体定义如下:
struct shmid_ds {
struct ipc_perm shm_perm; /* Ownership and permissions /
size_t shm_segsz; /
Size of segment (bytes) /
time_t shm_atime; /
Last attach time /
time_t shm_dtime; /
Last detach time /
time_t shm_ctime; /
Last change time /
pid_t shm_cpid; /
PID of creator /
pid_t shm_lpid; /
PID of last shmat(2)/shmdt(2) /
shmatt_t shm_nattch; /
No. of current attaches */

};

4.2.2 semop系统调用

semop()系统调用在 semid 标识的信号量集中的信号量上执行一个或多个操作。

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>

int semop(int semid, struct sembuf *sops, size_t nsops);
		                            返回值:成功0,失败-1

sops 参数是一个指向数组的指针,数组中包含了需要执行的操作,nsops 参数给出了数组的大小(数组至少需包含一个元素)。操作将会按照在数组中的顺序以原子的方式被执行。sops数组中的元素是形式如下的结构。

struct sembuf {
	unsigned short  sem_num;  // Semaphere number
	short			sem_op;   // Operation to be performed
	short			sem_flg;   //  Operation flags  (IPC_NOWAIT and SEM_UNDO)
};

sem_num 字段标识出了在集合中的哪个信号量上执行操作。sem_op 字段指定了需执行的操作

  • 如果 sem_op 大于 0,那么就将 sem_op 的值加到信号量值上,其结果是其他等待减小信号量值的进程可能会被唤醒并执行它们的操作。调用进程必须要具备在信号量上的修改(写)权限。
  • 如果 sem_op 等于 0,那么就对信号量值进行检查以确定它当前是否等于 0。如果等于0,那么操作将立即结束,否则 semop()就会阻塞直到信号量值变成 0 为止。调用进程必须要具备在信号量上的读权限。
  • 如果 sem_op 小于 0,那么就将信号量值减去 sem_op。如果信号量的当前值大于或等于 sem_op 的绝对值,那么操作会立即结束。否则 semop()会阻塞直到信号量值增长到在执行操作之后不会导致出现负值的情况为止。调用进程必须要具备在信号量上的修改权限。

从语义上来讲,增加信号量值对应于使一种资源变得可用以便其他进程可以使用它,而减小信号量值则对应于预留(互斥地)进程需使用的资源。在减小一个信号量值时,如果信号量的值太低——即其他一些进程已经预留了这个资源——那么操作就会被阻塞。 当 semop()调用阻塞时,进程会保持阻塞直到发生下列某种情况为止。

  • 另一个进程修改了信号量值使得待执行的操作能够继续向前。
  • 一个信号中断了 semop()调用。发生这种情况时会返回 EINTR 错误。( semop()在被一个信号处理器中断之后是不会自动重启的。)
  • 另一个进程删除了 semid 引用的信号量。发生这种情况时 semop()会返回 EIDRM 错误。
    在特定信号量上执行一个操作时可以通过在相应的 sem_flg 字段中指定 IPC_NOWAIT 标记来防止 semop()阻塞。此时,如果 semop()本来要发生阻塞的话就会返回 EAGAIN 错误。
    尽管通常一次只会操作一个信号量,但也可以通过一个 semop()调用在一个集合中的多个信号量上执行操作。这里需要指出的关键一点是这组操作的执行是原子的,即 semop()要么立即执行所有操作,要么就阻塞直到能够同时执行所有操作。

4.2.3 semctl系统调用

semctl()系统调用在一个信号量集或集合中的单个信号量上执行各种控制操作。

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>

int semctl(int semid, int semnum, int cmd, ...);

semid 参数是操作所施加的信号量集的标识符。对于那些在单个信号量上执行的操作,
semnum 参数标识出了集合中的具体信号量。对于其他操作则会忽略这个参数,并且可以将其设置为 0。
cmd 参数指定了需执行的操作。
一些特定的操作需要向 semctl()传入第四个参数,这个参数由用户自定义,但sys/sem.h给出了推荐格式:

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;  /* Number of entries in semaphore
                                         map; unused within kernel */
	int semmni;  /* Maximum number of semaphore sets */
	int semmns;  /* Maximum number of semaphores in all
                                         semaphore sets */
	int semmnu;  /* System-wide maximum number of undo
                                         structures; unused within kernel */
	int semmsl;  /* Maximum number of semaphores in a
                                         set */
	int semopm;  /* Maximum number of operations for
                                         semop(2) */
	int semume;  /* Maximum number of undo entries per
                                         process; unused within kernel */
	int semusz;  /* Size of struct sem_undo */
	int semvmx;  /* Maximum semaphore value */
	int semaem;  /* Max. value that can be recorded for
                                         semaphore adjustment (SEM_UNDO) */
};

semctl支持的所有命令如表所示:
在这里插入图片描述
semget的调用者可以给其key参数传递一个特殊键值IPC_PRIVATE(其值为0),这样无论该信号量是否已经存在,semget都将创建一个新的信号量。使用该键值的可以是其他进程,尤其是子进程也有方法来访问这个信号量。所以IPC_PRIVATE有些误导(历史原因),应该称作IPC_NEW。

#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操作,op为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 argc, char *argv[] )
{
	int sem_id = semget( IPC_PRIVATE, 1, 0666 );
	
	union semun sem_un;
	sem_un.val = 1;
	semctl( sem_id, 0, SETVAL, sem_un );
	
	pid_t id = fork();
	if( id < 0 )
		return 1;
	else if( id == 0 ){
		printf(" child try to get binary sem\n");
		/*在父、子进程间共享IPC_PRIVATE信号量的关键在于二者都有可以操作该信号量的标识符sem_id*/
		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;
}

4.3 共享内存

共享内存允许两个或多个进程共享物理内存的同一块区域(通常被称为段)。由于一个共享内存段会成为一个进程用户空间内存的一部分,因此这种IPC 机制无需内核介入。所有需要做的就是让一个进程将数据复制进共享内存中,并且这部分数据会对其他所有共享同一个段的进程可用。与管道或消息队列要求发送进程将数据从用户空间的缓冲区复制进内核内存和接收进程将数据从内核内存复制进用户空间的缓冲区的做法相比,这种 IPC 技术的速度更快。(每个进程也存在通过系统调用来执行复制操作的开销。)
另一方面,共享内存这种 IPC 机制不由内核控制意味着通常需要通过某些同步方法使得进程不会出现同时访问共享内存的情况(如两个进程同时执行更新操作或者一个进程在从共享内存中获取数据的同时另一个进程正在更新这些数据)。

4.3.1 概述

为使用一个共享内存段通常需要执行下面的步骤。

  • 调用 shmget()创建一个新共享内存段或取得一个既有共享内存段的标识符(即由其他进程创建的共享内存段)。这个调用将返回后续调用中需要用到的共享内存标识符。
  • 使用 shmat()来附上共享内存段,即使该段成为调用进程的虚拟内存的一部分。
  • 此刻在程序中可以像对待其他可用内存那样对待这个共享内存段。为引用这块共享内存,程序需要使用由 shmat()调用返回的 addr 值,它是一个指向进程的虚拟地址空间中该共享内存段的起点的指针。
  • 调用 shmdt()来分离共享内存段。在这个调用之后,进程就无法再引用这块共享内存了。这一步是可选的,并且在进程终止时会自动完成这一步。
  • 调用 shmctl()来删除共享内存段。只有当当前所有附加内存段的进程都与之分离之后内存段才会被销毁。只有一个进程需要执行这一步。

4.3.2 shmget系统调用

shmget()系统调用创建一个新共享内存段或获取一个既有段的标识符。新创建的内存段中的内容会被初始化为 0。

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

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

key 参数是使用在 45.2 节中介绍的其中一种方法(即通常是 IPC_PRIVATE 值或由 ftok()返回的键)生成的键。
当使用 shmget()创建一个新共享内存段时,size 则是一个正整数,它表示需分配的段的字节数。内核是以系统分页大小的整数倍来分配共享内存的,因此实际上 size 会被提升到最近的系统分页大小的整数倍。如果使用 shmget()来获取一个既有段的标识符,那么 size 对段不会产生任何效果,但它必须要小于或等于段的大小。
shmflg 参数执行的任务与其在其他 IPC get 调用中执行的任务一样,即指定施加于新共享内存段上的权限或需检查的既有内存段的权限。此外,在 shmflg 中还可以对下列标记中的零个或多个取 OR 来控制 shmget()的操作。

  • IPC_CREAT Create a new segment. If this flag is not used, then shmget() will find the segment associated with key and check to see if the user has permission to access the segment.

  • IPC_EXCL This flag is used with IPC_CREAT to ensure that this call creates the segment. If the segment already exists, the call fails.

  • SHM_NORESERVE (since Linux 2.6.15) This flag serves the same purpose as the mmap(2) MAP_NORESERVE flag. Do not reserve swap space for this segment. When swap space is reserved, one has the guarantee that it is possible to modify the segment. When swap space is not reserved one might get SIGSEGV upon a write if no physical memory is available.

shmget()在成功时返回新或既有共享内存段的标识符。

struct shmid_ds {
	struct ipc_perm shm_perm;    /* Ownership and permissions */
	size_t          shm_segsz;   /* Size of segment (bytes) */
	time_t          shm_atime;   /* Last attach time */
	time_t          shm_dtime;   /* Last detach time */
	time_t          shm_ctime;   /* Last change time */
	pid_t           shm_cpid;    /* PID of creator */
	pid_t           shm_lpid;    /* PID of last shmat(2)/shmdt(2) */
	shmatt_t        shm_nattch;  /* No. of current attaches */
	...
};

内核为每个共享存储段维护着一个结构,该结构至少包括以下部分:

struct ipc_perm {
               key_t          __key;    /* Key supplied to shmget(2) */
               uid_t          uid;      /* Effective UID of owner */
               gid_t          gid;      /* Effective GID of owner */
               uid_t          cuid;     /* Effective UID of creator */
               gid_t          cgid;     /* Effective GID of creator */
               unsigned short mode;     /* Permissions + SHM_DEST and
                                           SHM_LOCKED flags */
               unsigned short __seq;    /* Sequence number */
};

4.3.3 shmat和shmdt系统调用

shmat()系统调用将 shmid 标识的共享内存段附加到调用进程的虚拟地址空间中。当一个进程不再需要访问一个共享内存段时就可以调用 shmdt()来讲该段分离出其虚拟地址空间了。

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

void *shmat(int shmid, const void *shmaddr, int shmflg);

int shmdt(const void *shmaddr);

shmid 参数是由shmget调用返回的共享内存标识符,shmaddr指定将共享内存关联到进程的哪块空间地址。shmaddr 参数和 shmflg 位掩码参数中 SHM_RND 位的设置控制着段是如何被附加上去的。

  • 如果 shmaddr 是 NULL,那么段会被附加到内核所选择的一个合适的地址处。这是附加一个段的优选方法。
  • 如果 shmaddr 不为 NULL 并且没有设置 SHM_RND,那么段会被附加到由 shmaddr 指定的地址处,它必须是系统分页大小的一个倍数(否则会发生 EINVAL 错误)。
  • 如果 shmaddr 不为 NULL 并且设置了 SHM_RND,那么段会被映射到的地址为在shmaddr 中提供的地址被舍入到最近的常量 SHMLBA(shared memory low boundary address)的倍数。这个常量等于系统分页大小的某个倍数。将一个段附加到值为SHMLBA 的倍数的地址处在一些架构上是有必要的,因为这样才能够提升 CPU 的快速缓冲性能和防止出现同一个段的不同附加操作在 CPU 快速缓冲中存在不一致的视图的情况。

为 shmaddr 指定一个非 NULL 值(即上面列出的第二种和第三种情况)不是一种推荐的做法,其原因如下

  • 它降低了一个应用程序的可移植性。在一个 UNIX 实现上有效的地址在另一个实现上可能是无效的。
  • 试图将一个共享内存段附加到一个正在使用中的特定地址处的操作会失败。例如,当一个应用程序(可能在一个库函数中)已经在该地址处附加了另一个段或创建一个内存映射时就会发生这种情况。
shmat()的 shmflg 位掩码值
描述
SHM_RDONLY附加只读段
SHM_REMAP替换位于 shmaddr 处的任意既有映射
SHM_RND将 shmaddr 四舍五入为 SHMLBA 字节的倍数

在这里插入图片描述

4.3.4 概述

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

int shmctl(int shmid, int cmd, struct shmid_ds *buf);

shmctl() performs the control operation specified by cmd on the System V shared memory segment whose identifier is given in shmid.

The buf argument is a pointer to a shmid_ds structure

shmctl支持的命令

在这里插入图片描述
示例程序:

#include <sys/socket.h>
#include <netinet/in.h>
#include <assert.h>
#include <stdio.h>
#include <errno.h>
#include <string.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>
#include <unistd.h>
#include <arpa/inet.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];                 //和父进程通信用的管道
};

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 old_option = fcntl(fd, F_GETFL);
    int new_option = old_option | O_NONBLOCK;
    fcntl(fd, F_SETFL, new_option);
    return old_option;
}

void addfd(int epollfd, int fd)
{
    epoll_event event;
    event.data.fd = fd;
    event.events = EPOLLIN | EPOLLET;
    epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &event);
    setnonblocking(fd);
}

void sig_handler(int sig)
{
    int save_errno = errno;
    int msg = sig;
    send(sig_pipefd[1], (char *)&msg, 1, 0);
    errno = save_errno;
}

void addsig(int sig, void(*handler)(int), bool restart = true)
{
    struct sigaction sa;
    memset(&sa, '\0', sizeof(sa));
    sa.sa_handler = handler;
    if (restart) 
    {
        sa.sa_flags |= SA_RESTART;
    }
    sigfillset(&sa.sa_mask);
    assert(sigaction(sig, &sa, 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];
    //子进程使用I/O复用技术来同时监听两个文件描述符:客户连接socket、与父进程通信的管道文件描述符
    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];
    addfd(child_epollfd, pipefd);
    int ret;
    //子进程需要设置自己的信号处理函数
    addsig(SIGTERM, child_term_handler, false);

    while(!stop_child)
    {
        int number = epoll_wait(child_epollfd, events, MAX_EVENT_NUMBER, -1);
        if((number < 0) && (errno != EINTR))
        {
            printf("epoll failure\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(int argc, char *argv[])
{
    if (argc < 2) 
    {
        printf("usage: %s ip_address port_number\n", basename(argv[0]));
        return 1;
    }
    const char* ip = argv[1];
    int port = atoi(argv[2]);

    int ret = 0;
    struct sockaddr_in address;
    bzero(&address, sizeof(address));
    address.sin_family = AF_INET;
    inet_pton(AF_INET, ip, &address.sin_addr);
    address.sin_port = htons(port);

    listenfd = socket(PF_INET, SOCK_STREAM, 0);
    assert(listenfd > 0);

    ret = bind(listenfd, (struct sockaddr *)&address, sizeof(address));
    assert(ret != -1);

    ret = listen(listenfd, 5);
    assert(ret != -1);

    user_count = 0;
    users = new client_data[USER_LIMIT + 1];
    sub_process = new int [PROCESS_LIMIT];
    for( int i = 0; i < PROCESS_LIMIT; ++ i)
    {
        sub_process[i] = -1;
    }

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

    ret = socketpair(PF_UNIX, SOCK_STREAM, 0, sig_pipefd);
    assert(ret != -1);
    setnonblocking(sig_pipefd[1]);
    addfd(epollfd, sig_pipefd[0]);
    
    addsig(SIGCHLD, sig_handler);
    addsig(SIGTERM, sig_handler);
    addsig(SIGINT, sig_handler);
    addsig(SIGPIPE, SIG_IGN);
    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_SHARED);
    close(shmfd);

    while( !stop_server )
    {
        int number = epoll_wait(epollfd, events, MAX_EVENT_NUMBER, -1);
        if ((number < 0) && (errno != EINTR)) 
        {
            printf("epoll failure\n");
            break;
        }
        for (int i = 0; i < number; i ++ )
        {
            int sockfd = events[i].data.fd;
            //新的客户连接到来
            if (sockfd == listenfd ) 
            {
                struct sockaddr_in client_address;
                socklen_t client_addrlength = sizeof(client_address);
                int connfd = accept(listenfd, (struct sockaddr *)&client_address, &client_addrlength);
                if (connfd < 0) 
                {
                    printf("errno is: %d\n", errno);
                    continue;
                }

                if (user_count >= USER_LIMIT) 
                {
                    const char* info = "too many users\n";
                    printf("%s", 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) 
                {
                    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];
                ret = recv(sig_pipefd[0], signals, sizeof(signals), 0);
                if (ret == -1) 
                {
                    continue;
                }
                else if (ret == 0) 
                {
                    continue;
                }
                else
                {
                    for (int i = 0; i < ret; ++ i)
                    {
                        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];
                                        sub_process[users[del_user].pid] = del_user;
                                    }
                                    if (terminate && user_count == 0) 
                                    {
                                        stop_server = true;
                                    }
                                    break;
                                }
                            case SIGTERM:
                            case SIGINT:
                                {
                                    //结束服务程序
                                    printf("kill all the clild 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);
                                    }
                                    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) 
                {
                    continue;
                }
                else if (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;
}

4.4 消息队列

每一个队列都有一个msqid_ds结构与之关联

struct msqid_ds {
               struct ipc_perm msg_perm;     /* Ownership and permissions */
               time_t          msg_stime;    /* Time of last msgsnd(2) */
               time_t          msg_rtime;    /* Time of last msgrcv(2) */
               time_t          msg_ctime;    /* Time of last change */
               unsigned long   __msg_cbytes; /* Current number of bytes in
                                                queue (nonstandard) */
               msgqnum_t       msg_qnum;     /* Current number of messages
                                                in queue */
               msglen_t        msg_qbytes;   /* Maximum number of bytes
                                                allowed in queue */
               pid_t           msg_lspid;    /* PID of last msgsnd(2) */
               pid_t           msg_lrpid;    /* PID of last msgrcv(2) */
 };

4.4.1 msgget

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>

int msgget(key_t key, int msgflg);

key是一个键值,用来标识一个全局唯一的消息队列
msgflg 含义和使用与semget系统调用的sem_flags参数相同
msgget成功时返回一个正整数值,它是消息队列的标识符。失败时返回-1,并设置errno。

4.4.2 msgsnd

msgsnd将数据放到消息队列中。

   #include <sys/types.h>
   #include <sys/ipc.h>
   #include <sys/msg.h>

   int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);

msqid是msgget返回的消息队列标识符
msgp指向一个准备发送的消息,消息必须定义为如下形式:

   struct msgbuf {
       long mtype;       /* message type, must be > 0 */
       char mtext[1];    /* message data */
   };

msgflg控制msgsnd的行为,前只定义了一个这样的标记。
IPC_NOWAIT 执行一个非阻塞的发送操作。通常,当消息队列满时,msgsnd()会阻塞直到队列中有足够的空间来存放这条消息。但如果指定了这个标记,那么 msgsnd()就会立即返回 EAGAIN 错误。
当 msgsnd()调用因队列满而发生阻塞时可能会被信号处理器中断。当发生这种情况时,
msgsnd()总是会返回 EINTR 错误。(在 msgsnd()系统调用永远不会自动重启,不管在建立信号处理器时是否设置了 SA_RESTART 标记。) 向消息队列写入消息要求具备在该队列上的写权限。

在这里插入图片描述

4.4.3 msgrcv

   #include <sys/types.h>
   #include <sys/ipc.h>
   #include <sys/msg.h>

   int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);

   ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp,
                  int msgflg);

在这里插入图片描述

4.4.4 msgctl

msgctl()系统调用在标识符为 msqid 的消息队列上执行控制操作。

   #include <sys/types.h>
   #include <sys/ipc.h>
   #include <sys/msg.h>

   int msgctl(int msqid, int cmd, struct msqid_ds *buf);

cmd 参数指定了在队列上执行的操作,其取值是下列值中的一个。

  • IPC_RMID 立即删除消息队列对象及其关联的 msqid_ds 数据结构。队列中所有剩余的消息都会丢失,所有被阻塞的读者和写者进程会立即醒来,msgsnd()和 msgrcv()会失败并返回错误 EIDRM。这个操作会忽略传递给 msgctl()的第三个参数。
  • IPC_STAT 将与这个消息队列关联的 msqid_ds 数据结构的副本放到 buf 指向的缓冲区中。
  • IPC_SET 使用buf 指向的缓冲区提供的值更新与这个消息队列关联的msqid_ds数据结构中被选中的字段。

IPC命令

查看当前系统有哪些共享资源实例(需要root权限)

sudo ipcs    
------ Message Queues --------
key        msqid      owner      perms      used-bytes   messages    

------ Shared Memory Segments --------
key        shmid      owner      perms      bytes      nattch     status      

------ Semaphore Arrays --------
key        semid      owner      perms      nsems 
  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值