fork
#include<sys/types.h>
#include<unistd.h>
pid_t fork(void);
该函数一次调用,两次返回。在父进程中返回子进程的PID,在子进程中返回0,失败返回-1.
fork函数复制当前进程,在内核进程表中创建一个新的进程表项,堆指针、栈指针、标志寄存器的值与原进程相同。但是子进程的PPID被设置为原进程的PID,信号位图被清除(原进程的信号处理函数不对新进程起作用)。
子进程的代码与父进程完全相同,同时复制父进程的数据(堆数据、栈数据、静态变量等)。
复制采用的写时复制机制,即只有任一进程对数据执行了写操作时,才会真正进行复制操作(先产生缺页中断,然后操作系统给子进程分配内存并复制父进程的数据)。
文件描述符: 子进程也会继承父进程打开的文件描述符,因此文件描述符的引用计数会+1. 另外,父进程的用户根目录、当前工作目录等变量的引用也会加一。
exec
在子进程中执行其他程序,即替换当前进程映像,使用exec系列函数:
如果函数正确执行,原程序中exec调用之后的代码都不会执行,因为原程序已经被exec指定的程序完全替代(包括代码和数据)。
处理僵尸进程
僵尸进程
- 当子进程结束运行时,内核不会立即释放该进程的进程表表项(pid,退出时间,退出状态)。以满足父进程后续对该子进程退出状态的查询,在子进程结束运行之后,父进程读取其退出状态之前,我们称该进程在僵尸态。
如果父进程没有正确处理子进程的返回信息,那么子进程将停留在僵尸态,占据内核资源,过多的僵尸进程可能导致新的进程无法创建,所以应该正确处理子进程的结束过程。
#include<sys/types.h>
#include<sys/wait.h>
pid_t wait(int *stat_loc);
pid_t waitpid(pid_t pid, int *stat_loc, int options);
wait函数将阻塞,直到该进程的某个子进程结束,他返回子进程的PID,并将子进程的退出状态信息存储于stat_loc指向的内存中,可以使用宏来解释退出状态。
waitpid只等待由pid参数指定的子进程,如果pid
赋值-1,那么于wait相同,即等待任意子进程。stat_loc
含义相同。options
是可选的,设置为WNOHANG
则waitpid调用是非阻塞的:如果子进程还未结束,则返回0,如果子进程正常退出了,返回PID,失败时返回-1.
为了提高程序效率,我们一般在子进程退出后才调用waitpid。当子进程结束后,会给父进程发送一个信号SIGCHLD
,我们可以在父进程捕获这个信号,并处理子进程。
static void handle_child(int sig){
pid_t pid;
int stat;
// 设置为-1,监听所有子进程
while((pid = waitpid(-1, &stat, WNOHANG)) > 0){
// handle pid的子进程
}
}
管道 pipe
管道可以在父子进程间传递消息,因为fork()
调用之后两个文件描述符fd[0], fd[1]
都保持打开。一对这样的文件描述符只能保证单方向的文件传输,父子进程必须有一个关闭fd[0],另一个关闭fd[1]。如果需要双向通信,则需要使用两个管道,或者使用socketpair
。
#include<unistd.h>
#include<stdio.h>
#include<sys/types.h>
#include<string.h>
using namespace std;
int main() {
int fd[2];
int* readfd = &fd[0], * writefd = &fd[1];
const char* s = "Hello, this is a test text";
char readbuf[100];
memset(readbuf, '\0', 100);
int re;
re = pipe(fd);
if (re == -1) {
perror("pipe");
return -1;
}
pid_t pid = fork();
if (pid == -1) {
perror("fork");
return -1;
}
if (pid != 0) {
// parent
close(*readfd);
write(*writefd, s, strlen(s));
}
else {
close(*writefd);
int len = read(*readfd, readbuf, sizeof(readbuf));
printf("Have read %d len data, is %s", len, readbuf);
}
return 0;
}
信号量
#include<sys/wait.h>
#include<sys/sem.h>
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
union semun
{
int val;
semid_ds* buf;
unsigned short int* array;
seminfo* __buf;
};
void pv(int sem_id, int op){
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);
semun sem_un;
sem_un.val = 1;
semctl(sem_id, 0, SETVAL, sem_un);
int pid = fork();
if(pid < 0){
return -1;
}else if(pid == 0){
printf("In child , try to get binary sem\n");
pv(sem_id, -1);
printf("Child got the sem and will hold it 5 seconds\n");
sleep(5);
pv(sem_id, 1);
exit(0);
}else{
sleep(1);
printf("In Parent , try to get binary sem\n");
pv(sem_id, -1);
printf("Parent got the sem and will hold it 5 seconds\n");
sleep(5);
pv(sem_id, 1);
}
waitpid(pid, NULL, 0);
semctl(sem_id, 0, IPC_RMID, sem_un);
return 0;
}
信号量由一组系统调用实现
#include<sys/sem.h>
/* Get semaphore. */
int semget (key_t __key, int __nsems, int __semflg);
/* Control semaphore. */
int semctl (int __semid, int __semnum, int __cmd, ...);
/* Operate on semaphore. */
int semop (int __semid, struct sembuf *__sops, size_t __nsops);
semget
int semget (key_t key, int num_sems, int sem_flags);
semget
系统调用创建一个新的信号量集,或者获取一个已经存在的信号量集。
其中key标识全局唯一的信号量集,num_sems指定要创建/获取的信号量集中的信号量的数目,如果是创建信号量集,必须显式指定。sem_flags指定一组标志。其格式与含义与open
的mode
参数相似。
创建信号量集后,内核会创建与之关联的数据结构体semid_ds
并初始化。
semctl
可以用来控制信号量集
int semctl (int sem_id, int sem_num, int cmd, ...);
sem_id指定信号量集,sem_num指定要控制的信号量在信号量集中的编号(0 —— n), cmd时信号,后面的是根据cmd的可选项,推荐格式如下:
union semun{
int val; // 用于SETVAL命令
struct semid_ds* buf; // IPC_STAT和IPC_SET
unsigned short* array; // GETALL和SETALL
seminfo* __buf; // IPC_INFO
};
struct seminfo{
int semmap; // not use
int semmni; // 系统最多可拥有的信号量集数目
int semmns; // 系统最多可拥有的信号量数目
int semmnu; // not use
int semmsl; // 一个信号量集最多可包含的信号量数目
int semopm; // semop一次最多可执行多少个sem_op操作数目
int semume; // not use
int semusz; // sem_undo结构体大小
int semvmx; // 最大允许的信号量值
int semaem; // 最多允许的undo次数
};
semop
int semop (int sem_id, struct sembuf *sem_ops, size_t num_sem_ops);
struct sembuf{
unsigned short int sem_num; // 信号量集中待操作的信号量编号
short int sem_op;
short int sem_flg;
};
对信号量集上的信号量进行操作。
声明一个sembuf数组,然后依次设定对其的操作字段,设定数组长度num_sem_ops,然后系统会依次执行。
共享内存
共享内存是最高效的IPC机制,因为它不涉及进程间的任何数据传输。但是我们必须使用其他手段来同步进程对共享内存的访问,否则会产生竞态条件。因此共享内存通常和其他进程间通信方式一起使用。
4个系统调用:shmget
, shmat
, shmdt
, shmctl
.
shmget
该系统调用创建一段新的共享内存,或者获取一段已经存在的共享内存。
#include<sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);
key是键值,唯一表示一段共享内存,size指定共享内存的大小,单位是B,如果是新建共享内存,必须被显式指定。
shmflg参数的使用含义与semget
系统调用的sem_flags参数相同。还有两个额外参数:
- SHM_HUGETLB,类似mmap的MAP_HUGETLB标志,系统将使用大页面来为共享内存分配空间。
- SHM_NORESERVE,类似mmap的MAP_NORESERVE,不为共享内存保留交换分区(swap空间)。这样当物理内存不足时,对该共享内存执行写操作将触发SIGSEGV信号。
shmget调用成功时会返回正整数值:共享内存的标识符。失败时会返回-1.
如果shmget创建共享内存,那么共享内存的所有字节都会被初始化为0,与之关联的内核数据结构shmget_ds
将被创建并初始化。
struct shmid_ds{
struct ipc_perm shm_perm; // 共享内存的操作权限
size_t shm_segsz; // 共享内存大小Byte
__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; // 目前关联到此共享内存的进程数量
};
shmat和shmdt
共享内存被创建或者获取后,我们不能立即访问它,而是需要将其关联到进程的地址空间中才可以使用shmat
,完毕后还需要将其从进程地址空间中分离shmdt
。
#include<sys/shm.h>
void* shmat(int shm_id, const void* shm_addr, int shmflg);
int shmdt(const void* shm_addr);
其中sem_id指定共享内存标识符。shm_addr指定将共享内存关联到进程的地址空间,一般设置为NULL
,使操作系统自动选择。shmflg可选标志SHM_RND会影响shm_addr。
shmflg还可选:
- SHM_RDONLY:只读。不选的话默认可读可写。
- SHM_REMAP:如果地址addr已经被关联到一段共享内存上,则重新关联
- SHM_EXEC:它指定对共享内存的执行权限。对共享内存而言,执行权限实际上和读权限一致。
shmat成功时返回共享内存被关联到的地址,失败则返回(void*)-1
,并设置errno。成功时将修改shmid_ds部分字段: - 将shm_nattach加一
- 将shm_lpid设置为调用进程的pid
- 设置shm_atime时间
shmdt函数将关联到shm_addr处的共享内存从进程中分离。调用成功返回0,失败-1.成功时设置字段:
- 将shm_nattach减一
- 将shm_lpid设置为调用进程的pid
- 设置shm_dtime时间
shmctl
#include<sys/shm.h>
int shmctl(int shm_id, int command, struct shmid_ds* buf);
共享内存POSIX方法
利用mmap的MAP_ANONYMOUS标志可以实现父子进程间的匿名内存共享。通过打开一个文件,mmap也可以实现无关进程间的内存共享。Linux还提供了另一种利用mmap在无关进程间共享内存的方式。其无需任何文件支持,但是需要创建或打开一个POSIX共享内存对象。