多进程编程
2021-07-23 02:01:51 #网络编程 

僵尸进程

僵尸态:子进程结束运行后,内核没有立即释放该进程的进程表表项,用以满足父进程后续对子进程退出信息的查询

在子进程结束之后、父进程读取退出状态之前(父进程正常运行),称之为僵尸态;父进程退出之后(下文)、子进程退出之前,称之为僵尸态

父进程结束或异常终止,子进程仍在运行时,改变其 PPID 为 1(init 进程),由 init 进程等待子进程结束

一般通过wait()waitpid()处理僵尸进程

1
2
3
4
#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指定等待的子进程,取值为-1 则表示如同wait()一样等待任意子进程结束;参数stat_loc保存退出状态信息;参数options指定控制行为,一般取值为WNOANG,表示非阻塞调用

相关参考


进程间通信

管道

父子进程通过管道传递数据,且管道只能用于有关联的两个进程之间。本质上是pipe()通过管道文件描述符fd[1]向内核缓冲区写入数据,fd[0]从内核缓冲区读出数据,为半双工方式。缓冲区大小有限,写满或者读空时,需要等待读出或者写入

管道容量的默认大小是 65536B,可以通过fcntl()修改容量

1
2
#include <unistd.h>
int pipe(int fd[2]);

image-20210723123510225

通过构建两个管道实现双向通信;或者通过 socket 编程提供的sockpair()

1
2
3
#include <sys/types.h>
#include <sys/socket.h>
int socketpair(int domain, int type, int protocol, int fd[2]);

前三个参数与socket()的参数含义一致,但domain只能使用 UNIX 本地域协议族AF_UNIX;文件描述符fd是既可读又可写的


有名管道

保存在文件系统中

1
2
3
# mkfifo myPipe
# ls -l myPipe
prw-r--r-- 1 z z 0 5月 23 12:17 myPipe

信号量

用户进程通过使用 OS 提供的一对原语来对信号量进行操作,实现进程同步与互斥

原语:一种特殊的程序段,执行时不可被中断,由关/开中断指令实现

一对原语:

  1. wait(S)原语,可以理解为函数,信号量 S 是调入的参数,可以写作 P(S),相当于进入临界区
  2. signal(S)原语,可以写作 V(S),相当于退出临界区

信号量可以看作变量(整数或者复杂的记录型变量),可以用信号量来表示系统中某种资源的数量,例如一台打印机的信号量初值为 1

可以看作一种计数器,用来为多个进程提供对共享数据对象的访问

❗ 通过semget()将某个信号量生成信号量标识符,通过semop()执行 P、V 操作,通过semctl()控制信号量的某些属性


semget()创建一个新的信号量集,或获得一个已存在的信号量集

1
2
#include <sys/sem.h>
int semget(key_t key, int num_sems, int sem_flags);

成功时返回一个正整数值,作为信号量集的标识符;错误时返回-1 并设置 errno

key:用来标识一个全局唯一的信号量集,它代表程序可能使用的某个资源,通过信号量通信的进程使用相同的key来 | 创建 | 获取 | 该信号量。通过semget()key,由系统生成一个信号量标识符,程序通过信号量标识符来使用信号量

num_sems:指定要 | 创建 | 获取 | 的信号量集中信号量的数目。如果是创建信号量,则该值必须被指定,一般都是 1;如果是获取已经存在的信号量,则可以把它设置为 0

sem_flags:指定一组标志。作为参数的信号量不存在时,想要创建一个新的信号量,可以将sem_flags和值IPC_CREAT按位或操作;而IPC_CREAT | IPC_EXCL可以创建一个新的、唯一的信号量,如果信号量已存在,报错

下图为sem_flags可以的取值,一般直接设置为0666

image-20210723160659513

例:

1
2
// 创建信号量
int sem_id = semget((key_t)1234, 1, 0666|IPC_CREAT);

semctl()控制信号量信息

1
2
#include <sys/sem.h>
int semctl(int sem_id, int sem_num, int command, ...);

sem_id:表示semget()返回的信号量集的标识符

sem_num:表示信号量集中信号量的编号,0 为第一个。一般来说都设置为 0

command:表示要执行的命令,第三个参数取值SETVAL(把信号量初始化为一个已知的值)或IPC_RMID(删除一个已经无需继续使用的信号量标识符);第四个参数通常是一个union semum结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
union semun {
int val;
struct semid_ds* buf;
unsigned short* array;
struct seminfo* __buf;
};

struct seminfo {
int semmap;
int semmni;
int semmns;
int semmnu;
int semmsl;
int semopm;
int semume;
int semusz;
int semvmx;
int semasm;
};

一般需要利用semctl()初始化信号量,这是使用前必须做的

例:

1
2
3
4
5
6
7
8
// 创建信号量
int sem_id = semget((key_t)1234, 1, 0666|IPC_CREAT);

union semmun sem_union;
sem_union.val = 1;
if(semctl(sem_id, 0, SETVAL, sem_union) == -1) {
return -1; // 初始化失败
}

semop()改变信号量的值,即 P、V 操作,实际上是对内核变量的操作

1
2
#include <sys/sem.h>
int semop(int sem_id, struct sembuf* sem_ops, size_t num_sem_ops);

sem_idsemget()返回的信号量集的标识符

sem_ops:指向sembuf结构体数组

1
2
3
4
5
struct sembuf {
unsigned short int sem_num;
short int sem_op; // 操作类型
short int sem_flg; // IPC_NOWAIT 或 SEM_UNDO
};

sem_num:表示信号量集中信号量的编号,0 为第一个。一般来说,传入的单个信号量,此时设置为 0;如果使用信号量集,根据编号设置

sem_op:表示操作类型。一般用1表示 V 操作(即退出临界区);用-1表示 P 操作(即进入临界区)

sem_flgIPC_NOWAITSEM_UNDOIPC_NOWAIT表示无论调用是否成功,立刻返回,类似于非阻塞 I/O;SEM_UNDO表示操作系统跟踪信号,进程未释放信号量而终止时,操作系统释放信号量

num_sem_opssem_ops中信号量的个数

例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 创建信号量
int sem_id = semget((key_t)1234, 1, 0666|IPC_CREAT);

union semmun sem_union;
sem_union.val = 1;
if(semctl(sem_id, 0, SETVAL, sem_union) == -1) {
return -1; // 初始化信号量失败
}

struct sembuf sem_b;
sem_b.sem_num = 0;
sem_b.sem_op = -1; // 此时表示P操作
sem_b.sem_flg = SEM_UNDO;
if(semop(sem_id, &sem_b, 1) == 1) {
// 执行P操作,进入临界区
// 在临界区中执行操作
}

// 准备退出临界区
sem_b.sem_op = 1; // 此时表示V操作
if(semop(sem_id, &sem_b, 1) != 1) {
// 执行V操作,退出临界区
printf("退出临界区失败");
return -1;
}

if(semctl(sem_id, 0, IPC_RMID, sem_union) == -1) {
return -1; // 删除信号量失败
}

共享内存

在通信进程的虚拟内存空间中,拿出一块虚拟地址空间,映射到相同的物理内存中

image-20210523123011884

  • 一般利用 | 信号量 | 记录锁 | 互斥量 | 来同步访问共享内存

❗ 通过shmget()申请一段新的共享内存,通过shmat()关联,shmdt分离;shmctl()控制共享内存的某些属性


shmget()创建一个新的共享内存,或获得一个已存在的共享内存

1
2
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);

成功时返回一个正整数值,作为共享内存的标识符;错误时返回-1 并设置 errno

key:用来标识一个全局唯一的共享内存,通过shmget()key,由系统生成一个共享内存标识符,程序通过共享内存标识符来使用共享内存

size:以 B 为单位指定需要共享的内存容量

shmflg:与信号量中semget()中的参数semflg类似,也是可以利用IPC_CREAT做按位或操作

例:

1
2
3
// 创建共享内存记录结构体shared_test
struct shared_test* ptr;
int sem_id = shmget((key_t)1234, sizeof(shared_test), 0666|IPC_CREAT);

shmat()在进程中关联新创建的共享内存;shmdt()在进程中分离共享内存

1
2
3
#include <sys/shm.h>
void* shmat(int shm_id, const void* shm_addr, int shmflg);
int shmdt(const void* shm_addr);

shm_idshmget()中返回的共享内存标识符

shm_addr:指定共享内存关联到当前进程的地址,一般设置为 0,表示由系统决定

shmflg:一组标志位,一般设置为 0,配合shm_addr设置为 0,来由系统决定关联地址

shmat()返回指向共享内存所关联的地址的指针;失败返回-1。类似于malloc()申请动态内存

shmdt()由关联动态内存的指针,在本进程中分离动态内存(只是本进程不能使用,并不是删除动态内存)

例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 创建共享内存记录结构体shared_test
struct shared_test* ptr;
int sem_id = shmget((key_t)1234, sizeof(shared_test), 0666|IPC_CREAT);

void* shm_ptr = shmat(shm_id, 0, 0); // 系统决定关联地址
if(shm_ptr == (void*)-1) {
return -1;
}
ptr = (struct shared_test*)shm_ptr;
// 针对内存的读写操作
// 其他进程也是如此
if(shmdt(ptr) == -1) {
return -1; // 分离共享内存失败
}

shmctl()控制共享内存的属性

1
2
#include <sys/shm.h>
int shmctl(int shm_id, int command, struct shmid_ds* buf);

shm_idshmget()中返回的共享内存标识符

command:表示要执行的命令

IPC_STAT:复制共享内存的当前关联值到shmid_ds

IPC_SET:当进程拥有权限时,设置shmid_ds中的属性到共享内存的关联值中

IPC_RMID:删除当前共享内存

buf:指向用于复制或更改属性的shmid_ds

例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 创建共享内存记录结构体shared_test
struct shared_test* ptr;
int sem_id = shmget((key_t)1234, sizeof(shared_test), 0666|IPC_CREAT);

void* shm_ptr = shmat(shm_id, 0, 0); // 系统决定关联地址
if(shm_ptr == (void*)-1) {
return -1;
}
ptr = (struct shared_test*)shm_ptr;
// 针对内存的读写操作
// 其他进程也是如此
if(shmdt(ptr) == -1) {
return -1; // 分离共享内存失败
}

if(shmctl(shm_id, IPC_RMID, 0) == -1) {
return -1; // 删除共享内存失败
}

❗:一般在操作共享内存时,可以搭配信号量实现读写的互斥访问;但是有的程序可以放弃使用信号量,让多个进程同时进行读操作,提高效率,这就是进程同步问题的读者-写者问题


消息队列

直接通信方式:发送进程利用发送原语将消息直接发送到接收进程的消息缓冲队列,由接收进程利用接收原语接收消息

间接通信方式:发送进程利用发送原语将消息发送到中间实体(信箱),接收进程利用接收原语将信箱中属于自己的消息接收;消息的消息头中包含了发送和接收进程的 ID 等内容,不用担心接收错误

image-20210523122619246

  • 消息队列可以独立于发送进程和接收进程而存在
  • 接收程序可以选择接收消息队列中的特定类型的信息

❗ 通过msgget()创建一个消息队列;利用msgsnd()挂载消息到队列上,利用smgrcv()从队列上摘取消息;msgctl()控制消息队列的某些属性


msgget()创建一个消息队列,或者获取一个已有的消息队列

1
2
#include <sys/msg.h>
int msgget(key_t key, int msgflg);

成功时返回一个正整数值,作为消息队列的标识符;错误时返回-1 并设置 errno

key:用来标识一个全局唯一的消息队列,通过msgget()key,由系统生成一个消息队列标识符,程序通过消息队列标识符来使用消息队列

msgflg:与信号量中semget()中的参数semflg类似,也是可以利用IPC_CREAT做按位或操作;当消息队列存在时,IPC_CREAT被忽略,返回标识符

例:

1
2
// 创建消息队列
int msg_id = msgget((key_t)1234, 0666|IPC_CREAT);

msgsnd()挂载消息到队列上,smgrcv()从队列上摘取消息

1
2
3
4
5
6
7
8
9
#include <sys/msg.h>
int msgsnd(int msg_id, const void* msg_ptr, size_t msg_sz, int msgflg);
int msgrcv(int msg_id, const void* msg_ptr, size_t msg_sz, long int msgtype,
int msgflg);

struct msgbuf {
long mtype; // 消息类型
char mtext[512]; // 消息数据
};

msg_idmsgget()中返回的消息队列标识符

msg_ptr:指向一个准备 | 发送 | 存储 | 的消息,消息必须被定义为msgbuf这种结构

msg_sz:表示消息数据的长度。如上所示则长度为 512;如设置为 0 则表示没有消息

msgflg:一组标志位,可以按位或

msgsnd()中:默认为 0,消息队列满时将阻塞,直到成功;若设置为IPC_NOWAIT,表示为非阻塞方式,消息队列满时将立即返回并设置 errno

msgrcv()中:设置为IPC_NOWAIT,表示非阻塞方式;设置为MSG_NOERROR,表示消息长度超过msg_sz时截断;设置为MSG_EXCEPT,且msgtype大于 0,表示接收消息队列中第一个非msgtype类型的消息

msgtype:指定接收何种类型的消息

等于 0:读取消息队列中的第一个消息

大于 0:读取消息队列中第一个类型为msgtype类型的消息(msgflg指定了MSG_EXECPT的情况下则不同)

小于 0:读取消息队列中第一个类型值比msgtype小的消息

两个系统调用成功时返回 0;失败则返回-1 并设置 errno

例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
struct msgbuf {
long mtype;
char mtext[512];
};

// 创建消息队列
int msg_id = msgget((key_t)1234, 0666|IPC_CREAT);

char* buffer = "Hello world";
struct msgbuf data;
data.mtype = 1;
strncpy(&data.mtext, buffer, strlen(buffer)+1);

if(msgsnd(msg_id, (void*)&data, 512, 0) == -1) {
// 阻塞方式发送信息
return -1; // 发送消息失败
}

if(msgrcv(msg_id,(void*)&data, 512, 0, 0) == -1) {
// 接收消息队列中的第一个消息
return -1; // 接收消息失败
}

msgctl()控制消息队列的某些属性

1
2
#include <sys/msg.h>
int msgctl(int msg_id, int command, struct msgid_ds* buf);

msg_idmsgget()中返回的消息队列标识符

command:表示要执行的命令

IPC_STAT:复制消息队列的当前关联值到msgid_ds

IPC_SET:当进程拥有权限时,设置msgid_ds中的属性到消息队列的关联值中

IPC_RMID:删除当前消息队列

buf:指向用于复制或更改属性的msgid_ds

例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
struct msgbuf {
long mtype;
char mtext[512];
};

// 创建消息队列
int msg_id = msgget((key_t)1234, 0666|IPC_CREAT);

char* buffer = "Hello world";
struct msgbuf data;
data.mtype = 0;
strncpy(&data.mtext, buffer, strlen(buffer)+1);

if(msgsnd(msg_id, (void*)&data, 512, 0) == -1) { // 阻塞方式发送信息
return -1; // 发送消息失败
}

if(msgrcv(msg_id,(void*)&data, 512, 0, 0) == -1) {
return -1; // 接收消息失败
}

if(msgctl(msg_id, IPC_RMID, 0) == -1){
return -1; // 删除消息队列失败
}

在进程间传递文件描述符

父进程打开的文件,通过fork()后将文件描述符复制到子进程中,但是子进程传递到父进程则不行

要点在于,传递文件描述符,并不是简单的将文件描述符的值复制给另一个进程,而是通过文件描述符指向相同的文件。进程有file_struct中的指针数组的索引做文件描述符,应当在进程间传递这样的结构

可以通过 UNIX 域 socket 在进程间传送,参考sockpair()