进程是Linux操作系统环境的基础,它控制着系统上几乎所有的活动。进程基础概念
- 复制进程映像的fork系统调用和替换进程映像的exec系列系统调用
- 僵尸进程以及如何避免僵尸进程
- 进程间通信 最简单的方式: 管道
- 3种system V进程间通信方式: 信号量、消息队列和共享内存。
- 在进程间传递文件描述符的通用方法:通过unix本地域socket传递特殊的辅助数据。
一 管道
管道是父进程和子进程通信的常用手段。
int pipe(int pipefd[2]);
管道能在父子进程间传递数据,利用的是fork调用之后两个管道文件描述符(fd[0]和fd[1])都保持打开。一对这样的文件描述符只能保证父、子进程间一个方向的数据传输,父进程和子进程必须有一个关闭读端,另外一个关闭写端。
如果要实现父子进程之间的双向数据传输 我们可以用socketpair函数。
int socketpair(int d, int type, int protocol, int sv[2]);
该函数返回的文件描述符是全双工通信,所以只用打开一个就可以实现父子进程间的双向数据传输,但是注意一点,如果父进程用sv[0]写入,则子进程一定要用sv[1]读出。
在之前博客统一事件源中就用到了该函数的例子,感兴趣的可以去看看。
二 信号量
当多个进程同时访问系统上的某个资源时,比如同时写一个数据库的某条记录,或者同时修改文件,就需要考虑进程的同步问题,以确保任一时刻只有一个进程可以拥有对资源的独占式访问。通常,程序对共享资源的访问的代码只是很短的一段,但就是这一段代码引发了进程之间的竞态条件。我们称这段代码为关键代码段,或者临界区。对进程同步,也就是确保任一时刻只有一个进程能进入关键代码段。
信号量的提出是并发编程领域迈出的重要一步。
假设有信号量SV。
- P(sv),如果sv的值大于0,就将它减1;如果sv的值为0, 则挂起进程的执行。
- V(sv),如果有其它进程因为等待sv而挂起,则唤醒之;如果没有,则将sv加1。
如上图所示,当关键代码段可用时,二进制信号量sv的值为1,进程A和B都有机会进入关键代码段。如果此时进程A执行了P(sv)操作将sv减1,则进程B若再次执行P(sv)就会被挂起。直到进程A离开代码段,并执行V(sv)操作将SV加1,关键代码段才重新变得可用。如果此时进程B因为等待SV而被挂起,则它将被唤醒,并进入关键代码段。
会用到如下三个函数.
#include<sys/sem.h>
int semget(key_t key, int num_sems, int sem_flags);
/*
key参数是一个键值,用来标识一个全局唯一的信号量集,就像文件名全局唯一的标识一个文件一样。
要通过信号量通信的进程需要使用相同的键值来创建/获取该信号量。
num_sems参数指定要创建/获取的信号量集中信号量的数目。如果是创建信号量,则该值必须被指定;
如果是获取,则可以把它设置为0.
sem_flags参数指定一组标志。它低端的9个比特是该信号量的权限,其格式和含义都与系统调用open的mode参数相同。
semget成功时返回一个正整数值,它是信号量集的标识符;semget失败返回-1,并设置errno。
*/
int semop(int sem_id, struct sembuf* sem_ops, size_t num_sem_ops);
/*
sem_id参数就是sem_get函数返回的值。
struct sembuf
{
unsigned short int sem_num;
short int sem_op;
short int sem_flg;
};
*/
-
其中,sem_num成员是信号集中信号量的编号,0表示信号量集中的第一个信号量。
-
sem_op成员指定操作类型,其可选值为正整数、0和负整数。每种类型的操作行为又受到sem_flg成员的影响。
-
sem_flg的可选值是IPC_NOWAIT和SEM_UNDO。IPC_NOWAIT的含义是,无论信号量操作是否成功,semop调用都将立即返回,类似于非阻塞I/O操作。
SEM_UNDO的含义是,当进程退出时取消正在进行的semop操作。
sem_op和sem_flg将按照如下方式来影响semop的行为
1、如果sem_op大于0,则semop将被操作信号量的值semval增加sem_op。
2、如果sem_op等于0,则表示这是一个等待0操作。该操作要求调用进程对操作信号量集拥有读权限。如果此时信号量的值为0,则调用立即返回。
3、sem_op小于0,则表示对信号量值进行减操作,既期望获得信号量。该操作要求调用进程对被操作信号量集拥有写权限。如果信号量的值semval大于或等于sem_op的绝对值,则semop操作成功,调用进程立即获得信号量,并且系统将该信号量的semval值减去sem_op的绝对值。 -
第三个参数num_sem_ops指定要执行的操作个数,既sem_ops数组中的元素个数。semop对sem_ops中的每个成员按照数组顺序依次执行操作,该过程为原子操作。
看上面的标准解释可能云里雾里。简单的描述总结一下就是: 当我们想要获得这个信号量的时候我们可以将sem_op的值设为小于0,
这个时候内核就会检查信号量的semval值,如果绝对值大于等于sem_op我们就允许这段代码继续运行,这个时候有其它的进程想要接着获得该信号量则会被阻塞住,
当我们运行完这段代码后,就将sem_op的值设置为大于0,将其semval增加为大于0,这个时候其它进程会竞争然后获得该信号量。这就是用信号量实现进程同步的原理。
需要注意的是所有进程sem_op的正负值一定要为相反数。
看完解释如果还不是很懂,大家可以看看下面的例子,然后再结合概念进行理解。
int semctl(int sem_id, int sem_num, int command, ...);
/*
sem_id参数是由semget调用返回的信号量集标识符,用以指定被操作的信号量集。
sem_num参数指定被操作的信号量在信号量集中的编号。
command参数指定要执行的命令。
有的命令需要第四个参数。第四个参数的类型由用户自己定义,但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命令
};
SETVAL: 将信号量的semval值设置为semun.val,同时内核数据中的semid_ds.sem_ctime被更新
IPC_STAT:将信号量集相关联的内核数据结构复制到semun.buf中
IPC_SET:将semmun.buf中的部分成员复制到信号量集关联的内核数据结构中,同时内核数据中的semid_ds.ctime被更新
*/
简单的例子
semget的调用者可以给其它key参数传递一个特殊的键值IPC_PRIVATE(其值为0), 这样无论信号量是否已经存在,semget都将创建一个新的信号量。使用该键值创建的信号量并非像它的名字一样是进程私有的。其它进程,尤其是子进程,也有方法来访问这个信号量。
编译时记得 -lrt
#include<iostream>
#include<unistd.h>
#include<sys/wait.h>
#include<sys/sem.h>
union semun
{
int val;
struct semid_ds* buf;
unsigned short int* array;
struct seminfo* _buf;
};
//op为-1时执行获取信号量操作,op为1时执行释放信号量操作
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; //将信号值设置为1
semctl(sem_id, 0, SETVAL, sem_un);
pid_t pid = fork();
if(pid < 0)
return -1;
else if(pid == 0) //子进程运行
{
std::cout << "chlid try to get binary sem \n";
/*在父、子进程间共享IPC_PRIVATE信号量的关键就在于二者都可以操作该信号量的标识符sem_id*/
//因为信号值为1,所以想要获得信号量就让sem_op小于0,且信号值减sem_op的绝对值要小于等于0。
pv(sem_id, -1);
std::cout << "child get the sem and would release it after 5 seconds \n";
sleep(5);
//当处理完成后,将信号量恢复。
pv(sem_id, 1);
exit(0);
}
else
{
std::cout << "parent try to get binary sem\n";
pv(sem_id, -1);
sleep(5);
std::cout << "parent get the sem and would release it after 5 seconds\n";
pv(sem_id, 1);
std::cout << "parent will sleep 3 seconds\n";
sleep(3);
std::cout << "parent weak and will exit\n";
}
waitpid(pid, NULL, 0); //回收子进程结束的资源 避免僵尸进程的产生
semctl(sem_id, 0, IPC_RMID, sem_un); //删除信号量
return 0;
}
三 共享内存
共享内存的POSIX方法
mmap函数:
#include<sys/mman.h>
void* mmap(void* start, size_t length, int prot, int flags, int fd, off_t offset);
int munmmap(void* start, size_t length);
具体可以参考这里
利用它的MAP_ANONYMOUS标志我们可以实现父、子进程之间的匿名共享内存。通过打开同一个文件,mmap也可以实现无关进程之间的内存共享。Linux提供了另外一种利用mmap在无关进程之间共享内存的方式。这种方式无须任何文件的支持,但它需要先使用如下函数来创建或打开一个POSIX共享内存对象。
#include<sys/mman.h>
#include<sys/stat.h>
#include<fcntl.h>
int shm_open(const char* name, int oflag, mode_t mode);
shm_open的使用方法和open系统调用完全相同。
- name参数指定要创建/打开的共享内存对象。从可移植性的角度考虑,该参数应该使用"/somename"的格式:
- oflag参数指定创建方式。它可以是下列标志中的一个或者多个按位或
O_RDONLY。以只读方式打开共享内存
O_RDWR。以可读、可写方式打开共享内存
O_CREAT。如果共享内存不存在,则创建之。此时mode参数的最低9位将指定该共享内存对象的访问权限。共享内存被创建的时候,其初始长度为0。
O_EXCL。和O_CREAT一起使用,如果有name指定的共享内存已经存在,则shm_open调用返回错误,否则就创建一个新的共享内存对象。
O_TRUNC。如果共享内存对象已经存在,则把它截断,使其长度为0。 - shm_open调用返回一个文件描述符。该文件描述符可用于后续的mmap调用,从而将共享内存关联到调用进程。shm_open失败返回-1,并设置errno。
打开一个共享内存,最后也和打开文件一样关闭它。
int shm_unlink(const char* name);
该函数将name参数指定的共享内存对象标记为等待删除。当使用该共享内存对象的进程都使用munmap函数将其从进程中分离之后,系统将销毁这个共享内存对象所占据的资源。
下面代码是一个简单例子,使用了共享内存信号量和管道。
#include<sys/mman.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<iostream>
#include<string.h>
#include<unistd.h>
#include<sys/sem.h>
#include<sys/wait.h>
#include<memory>
#include<sys/types.h>
static const char* shm_name = "/my_shm"; //共享内存文件名
void pv(int sem_id, int op); //用信号量控制进程间同步
class share_mem
{
public:
share_mem();
~share_mem()
{
shm_unlink(shm_name);
}
bool write_mem(char* str); //向共享内存中写入数据
bool read_mem(int len); //从共享内存中读取数据
public:
int pipefd[2]; //通知主/子进程有数据可读/可写
private:
char write_buf[1024];
char read_buf[1024];
int shmfd; //共享内存描述符
std::shared_ptr<char*> sha_me; //指向共享内存的首地址
//信号量
int sem_id;
union semun
{
int val;
struct semid_ds* buf;
unsigned short int* array;
struct seminfo* _buf;
}sem_un;
};
share_mem::share_mem()
{
pipe(pipefd); //创建管道
bzero(write_buf, sizeof(write_buf));
bzero(read_buf, sizeof(read_buf));
//创建共享内存
shmfd = shm_open(shm_name, O_CREAT | O_RDWR | O_EXCL, 0666);
//改变共享内存大小
ftruncate(shmfd, 1024);
char* temp = (char*)mmap(NULL, 1024, PROT_READ | PROT_WRITE, MAP_SHARED, shmfd, 0);
sha_me = std::make_shared<char*>(temp);
//创建信号量
sem_id = semget(IPC_PRIVATE, 1, 0666);
sem_un.val = 1; //将信号量设置为1
semctl(sem_id, 0, SETVAL, sem_un); //将信号量的semval值设置为1
}
//当op为-1时执行获取信号量操作,-1为释放信号量操作
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);
}
//向共享内存写入数据
bool share_mem::write_mem(char* str)
{
//像共享内存中写入数据,保证进程同步,op为-1 想要获取信号量
pv(sem_id, -1);
if(strlen(str) > 1023)
{
std::cout << "data is too long, the share_mem can't recv this data\n";
return false;
}
else
{
for(int i = 0; i <= strlen(str); ++i)
{
(*sha_me)[i] = str[i];
}
std::cout << *sha_me << std::endl;
}
//将信号量加1,解除对信号量控制
pv(sem_id, 1);
}
//读取数据
bool share_mem::read_mem(int len)
{
pv(sem_id, -1);
for(int i = 0; i <= len; ++i)
read_buf[i] =(*sha_me)[i];
std::cout << "I read buf: " << read_buf << std::endl;
pv(sem_id, 1);
}
int main()
{
share_mem sha;
pid_t pid = fork();
if(pid < 0)
{
std::cout << "pid is false\n";
}
else if(pid == 0)
{
close(sha.pipefd[0]);
char* str = "hello word";
sha.write_mem(str);
int flag = strlen(str);
//通知主进程,共享内存中的数据写入,你可以读取
write(sha.pipefd[1], (char*)&flag, 4);
}
else
{
close(sha.pipefd[1]);
int flag;
//如果共享内存中没有数据,就将阻塞住
read(sha.pipefd[0], (char*)&flag, 4);
sha.read_mem(flag);
}
int status;
wait(&status);
return 0;
}
结果演示