【Linux】初识进程间通信:建议先读完这篇。
进程之间如何加锁,今天我们需要实现一个售票系统,我们需要对同一个num变量++。
以往我们写过类似的代码,只需要用pthread_mutex_t 这把锁就可以实现一个进程内多个线程互斥。但是我们这里改变需求,假设是你和你的同学需要抢一张票,你们在不同的主机的相同进程抢票,那么要怎么做呢?
pthread_mutex_t要使用,需要pthread_mutexattr_t的帮助,将原先的线程锁换为进程锁。
mmap 这个函数功能是在共享区当中申请一块内存空间。
是将用户空间的一段内存区域映射到内核空间,映射成功之后,用户对这段内存区域的修改可以直接反映到内核空间。
那么我们可以定义一个结构体,mt结构体,其中num标识总票数,mutex是互斥锁,mutexattr是锁的属性。
void *mmap(void *addr, size_t length, int prot, int flags,
int fd, off_t offset);
struct mt
{
int num; //多个进程的票数
pthread_mutex_t mutex;
pthread_mutexattr_t mutexattr;
};
mt 对齐,结构体对象对齐时按照其最大的对齐数进行对齐。 这里推荐把mutexattr写到mutex前面,因为这样总体消耗的内存最小。(56 -》 48)
在共享内存开辟mt空间大小,返回指针给两个进程。两个进程得到的指针值相同。
由于此处是父子进程,不需要依赖文件描述符,fd填写为-1,MAP_SHARED|MAP_ANON的组合标识需要保存的文件并且是匿名的mmap,仅在进程间使用。PROT_READ|PROT_WRITE标识这块共享区的读写权限。
匿名的mmap:
mm = (mt*)mmap(NULL,sizeof(*mm),PROT_READ|PROT_WRITE,MAP_SHARED|MAP_ANON,-1,0);
有名的mmap:
注意 :MAP_SHARED不进行设置的话后续会报段错误。
这引入我们对flags变量的思考:
int fd = open("mt_test",O_CREAT|O_RDWR,0777);// 打开文件,获得一个fd
if( fd == -1 )
{
perror("open file:");
exit(1);
}
int n = ftruncate(fd,sizeof(*mm)); // 截取fd的部分
mm = (mt*)mmap(NULL,sizeof(*mm),PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);
close(fd);
unlink("mt_test"); // 删除文件
两种创建mmap的方式是一种万金油,任意进程都可以使用,另一种适合父子通信。
pthread_mutex默认是线程间通信加的锁,要是想用在进程之间,需要pthread_mutexattr_t *restrict attr 填充这个字段,在属性字段当中修改为PTHREAD_PROCESS_SHARED标识进程间共享。
pthread_mutexattr_init(&mm->mutexattr);
// 初始化 mutex 属性
pthread_mutexattr_setpshared(&mm->mutexattr, PTHREAD_PROCESS_SHARED);
// 修改属性为进程间共享
pthread_mutex_init(&mm->mutex,&mm->mutexattr);
// 初始化一把 mutex 锁
上述工作过后,已经在共享区创建了一个结构体,并且父进程已经接受了指向共享区的指针,fork创建子进程之后父子进程都可以挂接在共享区上面。
接下来,父子实现互斥访问就简单了。使用之前只需要先用mm->mutex进行加锁即可。同理,若是放着的是原子变量,自旋锁,读写锁,也都是可以使用的。
pid = fork();
if( pid == 0 ) // 子进程
{
for( i=0; i<10;i++ )
{
pthread_mutex_lock(&mm->mutex);
(mm->num)++;
printf("-child--------------num++ %d\n",mm->num);
pthread_mutex_unlock(&mm->mutex);
sleep(1);
}
}
else
{
for( i=0;i<10;i++)
{
sleep(1);
pthread_mutex_lock(&mm->mutex);
mm->num += 2;
printf("--------parent------num+=2 %d\n",mm->num);
pthread_mutex_unlock(&mm->mutex);
}
wait(NULL);
}
pthread_mutexattr_destroy(&mm->mutexattr); // 销毁 mutex 属性对象
pthread_mutex_destroy(&mm->mutex); // 销毁 mutex 锁
/*
互斥量 实现 多进程 之间的同步
*/
#include
#include
#include
#include
#include
#include
#include
#include
#include
struct mt
{
int num;
pthread_mutex_t mutex;
pthread_mutexattr_t mutexattr;
};
// 通过父子进程对同一个变量 mm->num 进行++ ,父进程一次+2 ,子进程一次 +1
int main(void)
{
int i;
struct mt* mm;
pid_t pid;
// 创建映射区文件
int fd = open("mt_test",O_CREAT|O_RDWR,0777);
if( fd == -1 )
{
perror("open file:");
exit(1);
}
ftruncate(fd,sizeof(*mm));
mm = (mt*)mmap(NULL,sizeof(*mm),PROT_READ|PROT_WRITE,MAP_SHARED ,fd,0);
close(fd);
unlink("mt_test");
//就是这个mt结构体,当我们创建mt
// 建立映射区 MAP_ANON匿名映射
//mm = (mt*)mmap(NULL,sizeof(*mm),PROT_READ|PROT_WRITE,MAP_SHARED|MAP_ANON,-1,0);
// printf("-------before memset------\n");
//memset(mm,0x00,sizeof(*mm));
// printf("-------after memset------\n");
pthread_mutexattr_init(&mm->mutexattr); // 初始化 mutex 属性
pthread_mutexattr_setpshared(&mm->mutexattr, PTHREAD_PROCESS_SHARED); // 修改属性为进程间共享
pthread_mutex_init(&mm->mutex,&mm->mutexattr); // 初始化一把 mutex 锁
pid = fork();
if( pid == 0 ) // 子进程
{
for( i=0; i<10;i++ )
{
pthread_mutex_lock(&mm->mutex);
(mm->num)++;
printf("-child--------------num++ %d\n",mm->num);
pthread_mutex_unlock(&mm->mutex);
sleep(1);
}
}
else
{
for( i=0;i<10;i++)
{
sleep(1);
pthread_mutex_lock(&mm->mutex);
mm->num += 2;
printf("--------parent------num+=2 %d\n",mm->num);
pthread_mutex_unlock(&mm->mutex);
}
wait(NULL);
}
pthread_mutexattr_destroy(&mm->mutexattr); // 销毁 mutex 属性对象
pthread_mutex_destroy(&mm->mutex); // 销毁 mutex 锁
return 0;
}
信号量我们使用System V的这套接口。在说到具体的实现方案,不得不说分辨一下System V的信号量,Posix V的信号量,Posix还分为有名和无名的信号量。
System V的特点是同时能操作多个信号量,接口使用难度比Posix要复杂,移植性不太好。
Posix 是
解释一下为什么Posix 无名信号量适合线程同步,因为sem_init是将信号集sem_t进行初始化,而若要实现进程间的互斥,需要多个进程看到sem_t,就需要借助其他手段让其他进程看到sem_t。
System V的信号量是随内核的。
Posix有名信号灯的值是随内核持续的。
Posix无名信号灯的持续性却是不定的,因为他是基于内存的,如果基于内存的信号灯是由单个进程内的各个线程共享的,那么该信号灯就是随进程持续的,当该进程终止时它也会消失。
总结:
先介绍一下接口:
int semget(key_t key, int nsems, int semflg);
int semctl(int semid, int semnum, int cmd, …);
搭配SETVAL使用
。这里我们只是实现互斥,所以此处填写1。下方为内核,实际我们编写的时候可以只有int val这个字段。
union semun {
int val;
/* Value for SETVAL */
struct semid_ds *buf;
/* Buffer for IPC_STAT, IPC_SET */
unsigned short *array;
/* Array for GETALL, SETALL */
struct seminfo *__buf;
/* Buffer for IPC_INFO
(Linux-specific) */
};
int semop(int semid, struct sembuf *sops, unsigned nsops);
struct sembuf{
unsigned short sem_num; /* semaphore number */
short sem_op; /* semaphore operation */
short sem_flg; /* operation flags */
}
父进程创建共享内存,创建信号量,由信号量标识当前资源是否被使用,由共享内存将真正的数据(ticket)放到共享区当中。当父子任意进程访问ticket,都需要semop获得信号量,这样就能保证临界资源受到保护了。
需要注意的是,可以将信号量,共享内存在fork之前创建,之后调用接口不会引起写时拷贝。
--*pticket;不要写成 *pticket --; Linux下会出现问题!!!!
semop(semid, &sb, 1) == -1 && EAGAIN == errno
可以用来标识semctl删除信号量,op操作自然会失败,errno为EAGAIN。semop(semid, &sb, 1) == -1
也可以作为判断。#include
using namespace std;
#include
#include
#include
#include
#include
#include
#include
#include
union semun
{
int val; /* value for SETVAL */
struct semid_ds *buf; /* buffer for IPC_STAT, IPC_SET */
unsigned short *array; /* array for GETALL, SETALL */
struct seminfo *__buf; /* buffer for IPC_INFO */
};
int main()
{
int key = ftok("sem_sys.cc", 9000);
int shm_key = ftok(".", 8080);
int shmid = shmget(shm_key, 4096, IPC_CREAT | IPC_EXCL | 0644);
if (shmid < 0)
{
perror("shmget");
return 1;
}
int *pticket = (int *)shmat(shmid, nullptr, 0);
*pticket = 1000; // 1000张票
int semid = semget(key, 1, IPC_CREAT | IPC_EXCL | 0644);
if (semid < 0)
{
perror("semget");
return 2;
}
semun se;
se.val = 1;//标识单个信号量的资源大小,互斥填1,这里就会两态。
// SETVAL IPC_SET
int res = semctl(semid, 0, SETVAL, se);
if (res == -1)
{
perror("semctl");
return 4;
}
pid_t id = fork();
if (id == 0)
{
while (1)
{
// child
struct sembuf sb; // P
sb.sem_flg = 0; //阻塞拿
sb.sem_num = 0; //拿第一个
sb.sem_op = -1; //获取信号量
if (semop(semid, &sb, 1) == -1 && EAGAIN == errno)
break;
// cout << "child " << *pticket_child << endl;
//获得了这个信号量
if (*pticket <= 0)
{
semctl(semid, 0, IPC_RMID);
break;
}
--*pticket;
cout << "child get ticket: " << *pticket << endl;
usleep(1);
// V
struct sembuf sb2;
sb2.sem_flg = 0;
sb2.sem_num = 0;
sb2.sem_op = 1;//释放信号量
semop(semid, &sb2, 1);
}
shmdt(pticket);
}
else
{
while (1)
{
// father
struct sembuf sb; // P
sb.sem_flg = 0;
sb.sem_num = 0;
sb.sem_op = -1;
if (semop(semid, &sb, 1) == -1 && EAGAIN == errno)
break;
//获得了这个信号量
// cout << "father " << *pticket_father << " :" << &pticket_father << endl;
if (*pticket <= 0)
{
semctl(semid, 0, IPC_RMID);
break;
}
--*pticket;
cout << "father get ticket: " << *pticket << " :" << &pticket << endl;
usleep(1);
// V
struct sembuf sb2;
sb2.sem_flg = 0;
sb2.sem_num = 0;
sb2.sem_op = 1;
semop(semid, &sb2, 1);
}
shmdt(pticket);
shmctl(shmid, IPC_RMID, nullptr);
}
return 0;
}
一开始担心会发生写时拷贝写的版本,让子进程让出时间片给父进程创建信号量和共享内存,子进程后进行挂接。
#include
using namespace std;
#include
#include
#include
#include
#include
#include
#include
#include
union semun
{
int val; /* value for SETVAL */
struct semid_ds *buf; /* buffer for IPC_STAT, IPC_SET */
unsigned short *array; /* array for GETALL, SETALL */
struct seminfo *__buf; /* buffer for IPC_INFO */
};
int main()
{
pid_t id = fork();
if (id == 0)
{
usleep(100); //让父进程先启动
int key = ftok("sem_sys.cc", 9000);
int shm_key = ftok(".", 8080);
//child 负责连接
int shmid = shmget(shm_key, 0, 0);//获取共享内存
if(shmid < 0)
{
perror("shmget");
return 1;
}
int *pticket_child = (int *)shmat(shmid, nullptr, 0);//获取ticket
int semid = semget(key, 1, 0);//获取信号量
//cout << "child semid " << semid <
if(semid < 0)
{
perror("semget");
return 2;
}
while (1)
{
// child
struct sembuf sb; // P
sb.sem_flg = 0; //阻塞拿
sb.sem_num = 0; //拿第一个
sb.sem_op = -1; //拿一个
if (semop(semid, &sb, 1) == -1 && EAGAIN == errno)
break;
//cout << "child " << *pticket_child << endl;
//获得了这个信号量
if (*pticket_child <= 0)
{
semctl(semid, 0, IPC_RMID);
break;
}
--*pticket_child;
cout << "child get ticket: " << *pticket_child << endl;
usleep(1);
// V
struct sembuf sb2;
sb2.sem_flg = 0;
sb2.sem_num = 0;
sb2.sem_op = 1;
semop(semid, &sb2, 1);
}
shmdt(pticket_child);
}
else
{
int key = ftok("sem_sys.cc", 9000);
int shm_key = ftok(".", 8080);
int shmid = shmget(shm_key, 4096, IPC_CREAT | IPC_EXCL | 0644);
if(shmid < 0)
{
perror("shmget");
return 1;
}
int *pticket = (int *)shmat(shmid, nullptr, 0);
*pticket = 1000; // 1000张票
int semid = semget(key, 1, IPC_CREAT | IPC_EXCL | 0644);
//cout << "father semid " << semid <
if(semid < 0)
{
perror("semget");
return 2;
}
semun se;
se.val = 1;
// SETVAL IPC_SET
int res = semctl(semid, 0, SETVAL, se);
if(res == -1)
{
perror("semctl");
return 4;
}
int *pticket_father = pticket;
while (1)
{
// father
struct sembuf sb; // P
sb.sem_flg = 0;
sb.sem_num = 0;
sb.sem_op = -1;
if (semop(semid, &sb, 1) == -1 && EAGAIN == errno)
break;
//获得了这个信号量
//cout << "father " << *pticket_father << " :" << &pticket_father << endl;
if (*pticket_father <= 0)
{
semctl(semid, 0, IPC_RMID);
break;
}
--*pticket_father;
cout << "father get ticket: " << *pticket_father << " :" << &pticket_father << endl;
usleep(1);
// V
struct sembuf sb2;
sb2.sem_flg = 0;
sb2.sem_num = 0;
sb2.sem_op = 1;
semop(semid, &sb2, 1);
}
shmdt(pticket_father);
shmctl(shmid,IPC_RMID,nullptr);
}
return 0;
}
【Linux】初识进程间通信
之前写的这篇文章由叙述,管道在65536内是支持原子性的,小于PIPE_BUF都能保证原子性,读写只能一端运行,内部就实现了互斥同步,要双向通信就开两个即可。
参考:
被遗忘的桃源——flock 文件锁
进程间锁:进程间pthread_mutex,文件锁
进程间加锁目前我所知道的以上三种方式已经成列,由于能力有限,若有错误,欢迎评论区指出。