第13章 多进程编程
13.1 fork系统调用
:::tips
pid_t fork(void);
:::
每次调用返回两次,在父进程中返回子进程的PID,在子进程中返回0。用来判断当前进程是父进程还是子进程。
fork函数复制当前进程,在内核进程表中创建一个新的进程表项。该进程的PPID被设置成原进程的PID,信号位图被清除(原进程设置的信号处理函数不再对新进程起作用)。数据的复制采用写时复制(即只有在任一进程(父进程或子进程)对数据执行了写操作时,复制才会发生(先是缺页中断,然后操作系统给子进程分配内存并复制父进程的数据))。
13.2 exec系列系统调用
在子进程中执行其他程序,即替换当前进程映像。
13.3 处理僵尸进程
子进程结束时,内核不会立即释放该进程的进程表表项,以满足父进程后续对该子进程退出信息的查询。子进程结束后,父进程读取其退出状态之前,子进程处于僵尸态(另一种情况是父进程结束或异常终止,子进程继续运行,此时PPID为1)。
wait函数:阻塞进程,直到该进程的任意子进程结束运行。
waitpid函数:只等待由pid参数指定的子进程。参数指定非阻塞:如果pid指定的目标子进程还没有结束或意外终止,则立即返回0;如果目标子进程确实正常退出了,则返回该子进程的PID。
在事件已经发生的情况下执行非阻塞调用才能提高程序的调用。当一个进程结束时,将给其父进程发送SIGCHLD信号,此时可以在父进程中捕获该信号,在信号处理函数中调用waitpid来彻底结束子进程。
13.4 管道
也是父子进程间通信的常用方法。
双向数据传输,必须使用两个管道,可以使用全双工管道socketpair。
管道只能用于有关联的两个进程(父子)间通信。有一种特殊的管道叫命名管道(FIFO)能用于无关联进程间通信。
无关联进程间通信:信号量、共享内存、消息队列。
13.5 信号量
进程同步:多个进程同时访问系统资源,确保任一时刻只有一个进程拥有对资源的独占式访问。
临界区(关键代码段):程序对共享资源的访问。
P(SV):如果SV的值大于0,就将它减1;如果SV的值为0,则挂起进程的执行。进入临界区。
V(SV):如果有其他进程因为等待SV而挂起,则唤醒之;如果没有,则将SV加1。退出临界区。
使用二进制信号量,原子操作,同时完成两步操作:检测变量是否为true/false,如果是则再将它设置为false/true。
:::tips
int semget(key_t key, int num_sems, int sem_flags);
:::
semget:用于创建一个新的信号量集,或者获取一个已经存在的信号量集。
:::tips
int semop(int sem_id, struct sembuf* sem_ops, size_t num_sem_ops);
:::
semop:改变信号量的值,即执行P、V操作,对信号量的操作实际上就是对内核变量的操作。
:::tips
int semctl(int sem_id, int sem_num, int command, …);
:::
semctl:允许调用者对信号量进行直接控制
semget的调用者可以给其key参数传递一个特殊的犍值IPC_PRIVATE(其值为0),这样无论该信号量是否已经存在,semget都将创建一个新的信号量。
父、子进程间使用一个IPC_PRIVATE信号量来同步:
#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); //初始化信号量的值为1,表示可用
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 5 seconds\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 5 seconds\n");
sleep(5);
pv(sem_id, 1);
}
waitpid(id, NULL, 0);
semctl(sem_id, 0, IPC_RMID, sem_un); /* 删除信号量 */
return 0;
}
工作在prefork(主进程会在启动时创建一定数量的子进程,这些子进程会等待接受客户端的连接。一旦有客户端连接到服务器,其中一个子进程会接受连接并处理客户端的请求,而其他子进程会继续等待新的连接)模式下的httpd网页服务器程序使用1个IPC_PRIVATE信号量来同步各子进程对epoll_wait的调用权。
13.6 共享内存
最高效,不涉及进程之间的任何数据传输。需要同步进程,避免竞态条件(读写不是原子操作,数据不一致或过时,缓存不一致,尝试获取锁和占用锁不是原子操作)。
:::tips
int shmget(key_t key, size_t size, int shmflg);
:::
shmget:创建一段新的共享内存,或者获取一段已经存在的共享内存。
:::tips
void* shmat(int shm_id, const void* shm_addr, int shmflg);
int shmdt(const void* shm_addr);
:::
shmat:共享内存在创建/获取后,不能立即访问,需要先将它关联到进程的地址空间中。
shmdt:共享内存使用完后,需要将它从进程地址空间中分离。
:::tips
int shmctl(int shm_id, int command, struct shmid_ds* buf);
:::
shmctl:控制共享内存的某些属性。
mmap函数:利用MAP_ANONYMOUS标志可以实现父、子进程之间的匿名内存共享;通过打开同一个文件,mmap也可以实现无关进程之间的内存共享;另外一种无须文件支持,需要先使用shm_open函数来创建或打开一个POSIX共享内存对象,返回的fd用于后续mmap调用,共享内存使用完后,shm_unlink标记为等待删除,当所有使用该共享内存对象的进程都使用ummap将它从进程中分离之后,系统将销毁这个共享内存对象所占据的资源。
#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]; /* 和父进程通信用的管道 */
};
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 + 1];
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, 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_FAILED);
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);
}
/* 保存第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))
{
char signals[1024];
ret = recv(sig_pipefd[0], signals, sizeof(signals), 0);
if (ret == -1)
{
continue;
}
else if (ret == 0)
{
continue;
}
else
{
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];
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 child now\n");
if (user_count == 0)
{
stop_server = true;
break;
}
for (int k = 0; k < user_count; ++k)
{
int pid = users[k].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 pepe\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;
}
13.7 消息队列
在两个进程间传递二进制块数据的方式。接收方可以根据类型来有选择地接收数据,而不是像管道那样先进先出。
:::tips
int msgget(key_t key, int msgflg);
:::
msgget:创建一个消息队列,或者获取一个已有的消息队列。
:::tips
int msgsnd(int msqid, const void* msg_ptr, size_t msg_sz, int msgflg);
:::
msgsnd:把一条消息添加到消息队列中。
:::tips
int msgrcv(int msqid, void* msg_ptr, size_t msg_sz, long int msgtype, int msgflg);
:::
msgrcv:从消息队列中获取消息。
:::tips
int msgctl(int msqid, int command, struct msqid_ds* buf};
:::
msgctl:控制消息队列的某些属性。
13.8 IPC命令
信号量、共享内存、消息队列都使用一个全局唯一的key来描述一个共享资源,调用xxget即创建了共享资源的实例。
ipcs命令:观察当前系统上拥有哪些共享资源实例。
ipcrm命令:删除遗留在系统中的共享资源。
13.9 在进程间传递文件描述符
传递一个文件描述符并不是传递一个文件描述符的值,而是要在接收进程中创建一个新的文件描述符,并且该文件描述符和发送进程中被传递的文件描述符指向内核中相同的文件表项。
#include <sys/socket.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <assert.h>
#include <string.h>
static const int CONTROL_LEN = CMSG_LEN(sizeof(int));
/* 发送文件描述符,fd参数是用来传递信息的UNIX域的socket,fd_to_send参数是待
* 发送的文件描述符 */
void send_fd(int fd, int fd_to_send)
{
struct iovec iov[1];
struct msghdr msg;
char buf[0];
iov[0].iov_base = buf;
iov[0].iov_len = 1;
msg.msg_name = NULL;
msg.msg_namelen = 0;
msg.msg_iov = iov;
msg.msg_iovlen = 1;
cmsghdr cm;
cm.cmsg_len = CONTROL_LEN;
cm.cmsg_level = SOL_SOCKET;
cm.cmsg_type = SCM_RIGHTS;
*(int *)CMSG_DATA(&cm) = fd_to_send;
msg.msg_control = &cm; /* 设置辅助数据 */
msg.msg_controllen = CONTROL_LEN;
sendmsg(fd, &msg, 0);
}
/* 接收目标文件描述符 */
int recv_fd(int fd)
{
struct iovec iov[1];
struct msghdr msg;
char buf[0];
iov[0].iov_base = buf;
iov[0].iov_len = 1;
msg.msg_name = NULL;
msg.msg_namelen = 0;
msg.msg_iov = iov;
msg.msg_iovlen = 1;
cmsghdr cm;
msg.msg_control = &cm; /* 设置辅助数据 */
msg.msg_controllen = CONTROL_LEN;
recvmsg(fd, &msg, 0);
int fd_to_read = *(int *)CMSG_DATA(&cm);
return fd_to_read;
}
int main(int argc, char *argv[])
{
int pipefd[2];
int fd_to_pass = 0;
/* 创建父、子进程间的管道,文件描述符pipefd[0]和pipefd[1]都是UNIX域的socket */
int ret = socketpair(PF_UNIX, SOCK_DGRAM, 0, pipefd);
assert(ret != -1);
pid_t pid = fork();
assert(pid >= 0);
if (pid == 0)
{
close(pipefd[0]);
fd_to_pass = open("test.txt", O_RDWR, 0666);
/* 子进程通过管道将文件描述符发送到父进程,如果文件test.txt打开失败
* 则子进程将标准输入文件描述符发送到父进程 */
send_fd(pipefd[1], (fd_to_pass > 0) ? fd_to_pass : 0);
close(fd_to_pass);
exit(0);
}
close(pipefd[1]);
fd_to_pass = recv_fd(pipefd[0]); /* 父进程从管道接收目标文件描述符 */
char buf[1024];
memset(buf, '\0', 1024);
read(fd_to_pass, buf, 1024); /* 读目标文件描述符,以验证其有效性 */
printf("I got fd %d and data %s \n", fd_to_pass, buf);
close(fd_to_pass);
return 0;
}