共享内存:
正常来说,两个进程被父亲生下来之后,即使是亲兄弟,他们两个内存也是独立的。
如果提前约定:在内存中有一个空间,在以后兄弟都可以在那里拿数据。(临界区)
除了父进程可以为子进程创建共享内存,两个完全不相关的进程也可以通过某种联系,创建共享内存。
有亲缘关系的进程,和无亲缘关系的进程
亲缘关系:父亲,孩子,亲兄弟
- shm share_memory
亲缘关系:父亲 + 孩子 / 亲兄弟之间
shmget 分配一个系统内存共享段
#include <sys/ipc.h>//ipc,进程间通讯
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);
// size 多大,shmflg 类型
返回值与key有关,key相同,返回的id相同
返回的是共享内存段的id
一个共享内存段的大小,等同于上取整到n * page_size的大小(一般是4k)
当key 中取值为IPC_PRIVATE 或者 shmflg 中设置了IPC_PRIVATE 那么就会建立一个共享内存,否则不会有共享内存创建。
如果shmflg 中设定了IPC_CREAT | IPC_EXCL 但是这个收共享内存段已经存在,会创建失败。
shmflg:
IPC_CREATE: 创建一个段,如果这个flag 没有被用,那么shmget 会找到一个与key 值有关的段,然后检查用户是否有权限来使用这个段。
IPC_EXCL : 伴随IPC_CREAT 使用,确保这个调用来创建段,如果段已经存在了,那么调用失败。
将id获取地址:shmat
这个函数返回一个区域的id,我们无法直接访问内存,所以我们需要id 转换成地址。 - shmat : 通过id 找到地址
void *shmat(int shmid, const void *shmaddr,int shmflg);
int shmdt(const void *shmaddr);
shmat 通过shmid 来获得共享内存段的地址空间
相同的key 拿到的地址可能不同,因为这个地址,是自己空间的地址,附着到的地址被shmaddr 指定。
shmat 中shmaddr 的集中情况- 如果shamaddr 为空, 那么自动自己选择一个合适的页来存放段。
- 如果shmaddr 不是空,但是 shmflg 中设置了SHM_RND ,那么存放在shmaddr 上的地址向下取整来靠近SHMLBA
- 否则,shmaddr 一定是一个页大小的整数倍。
执行成功的shmat 调用会更新成员shimid_ds 结构体:- shm_atime 被设置为最近的时间
- shm_lpid 被设置进程id
- shm_nattch 增加1
执行成功shmdt 调用会更新: - shm_Dtime 被设置为最近的时间
- shm_lpid 被设置进程id
- shm_nattch 减1
shmat 执行成功,返回共享段的附着地址,失败的话返回-1
shmdt 执行成功,返回0,失败的话返回-1
- ftok
ket_t ftok(const *pathname, int proj_id); // 将路径名和一个 project id 转换成一个key
这个key 返回一个id 然后通过这个id 使用shmget 得到一个段id 然后通过shmat 得到地址。
//代码版本1
#include "head.h"
#define INS 5
struct Num {
int now;
int sum;
};
struct Num *share_memory;
int main() {
pid_t pid;
int x = 0, shmid;
key_t key = ftok(".", 2021);
if ((shmid = shmget(key, sizeof(struct Num), IPC_CREAT | IPC_EXCL)) < 0) {
perror("shmget");
exit(1);
}
share_memory = (struct Num*) shmat(shmid, NULL, 0);
if (share_memory == NULL) {
perror("shmat");
exit(1);
}
for (int i = 1; i <= INS ;i++) {
if ((pid == fork()) < 0) {
perror("fork");
}
x = i;
if (pid == 0) break;
}
return 0;
}
但是这样的代码运行第一次可以,运行第二次的时候,共享段已经被创建,需要删除文件才能继续运行。
用ipcs 查看共享段。
ipcrm 删除共享段
// 代码版本2, 我们先忽略共享段的问题
#include "head.h"
#define INS 5
struct Num {
int now;
int sum;
};
struct Num *share_memory;
void do_add() {
while (1) {
if (share_memory->now > 100) break;
share_memory->now++;
share_memory->sum += share_memory->now;
}
return ;
}
int main() {
pid_t pid;
int x = 0, shmid;
key_t key = ftok(".", 2021);
if ((shmid = shmget(key, sizeof(struct Num), IPC_CREAT | IPC_EXCL)) < 0) {
perror("shmget");
exit(1);
}
share_memory = (struct Num*) shmat(shmid, NULL, 0);
if (share_memory == NULL) {
perror("shmat");
exit(1);
}
share_memory->now = 0;
share_memory->sum = 0;
for (int i = 1; i <= INS ;i++) {
if ((pid == fork()) < 0) {
perror("fork");
}
x = i;
if (pid == 0) break;
}
if (pid == 0) {
do_add();
} else {
for (int i = 1; i <= INS; i++) {
wait(NULL);
}
}
printf("share_memory->sum = %d\n", share_memory->sum);
return 0;
}
运行之后会发现段错误,访问到了自己不该访问的问题
可能在哪里出现?
我们只有在share_memory 时候访问到地址。。。
所以在父进程为share_memory 进程赋值之后做了一次输出,发现的确没有输出,出现了错误。确定了段错误的位置。
所以追溯到share_memory 之后, perror 输出一下,发现提示permission deny
于是注意到 在声明id 的时候,没有赋予可读可写的权限,于是 在flag 改为 IPC_CREAT | IPC_EXCL | 0666
#include "head.h"
#define INS 5
struct Num {
int now;
int sum;
};
struct Num *share_memory;
void do_add(int x) {
while (1) {
if (share_memory->now > 1000) break;
share_memory->sum += share_memory->now;
share_memory->now++;
printf("Child : %d, share_memory->now : %d, share_memory->sum : %d\n", \
x, share_memory->now, share_memory->sum);
usleep(100);
}
return ;
}
int main() {
pid_t pid;
int x = 0, shmid;
key_t key = ftok(".", 2021);
if ((shmid = shmget(key, sizeof(struct Num), IPC_CREAT | IPC_EXCL | 0666)) < 0) {
perror("shmget");
exit(1);
}
share_memory = (struct Num*) shmat(shmid, NULL, 0);
if (share_memory < 0) {
perror("shmat");
exit(1);
}
share_memory->now = 0;
share_memory->sum = 0;
for (int i = 1; i <= INS ;i++) {
if ((pid = fork()) < 0) {
perror("fork");
}
x = i;
if (pid == 0) break;
}
if (pid == 0) {
do_add(x);
} else {
for (int i = 1; i <= INS; i++) {
wait(NULL);
}
}
printf("share_memory->sum = %d, My Pid : %d\n", share_memory->sum, getpid());
return 0;
}
上面这个代码可以成功的执行出,在一核的机器上只有一个cpu 有极小的可能出错,但是还有有可能,所以为了避免这种可能,我们采用锁结构。
一核 一次只运行一个进程,为什么还会有小的可能出错?
在do_add 中,now 值自增和 sum 累加的操作不是原子的,有一种可能是,now 刚刚自增,但是时间片到了,运行下一个进程,这样导致了一个漏加的可能性。
加锁的本质是给资源加锁, 锁住的是资源,不是进程.
互斥锁:
我们可以用线程互斥锁。
线程互斥锁
mutex:互斥
pthread_mutex_init:线程互斥锁初始化
pthread_mutex_unlock:线程互斥锁解锁
pthread_mutex_destroy:线程互斥锁销毁
pthread_mutex_lock:线程互斥锁的锁
pthread_mutex_trylock:线程互斥锁的尝试锁
-
pthread_mutex_init :
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t mutexattr)
锁指针,锁属性指针 -
pthread_mutexattr_
pthread_mutexattr_setpshared : set process shared
int pthread_mutexattr_setpshared(const pthread_mutexattr_t *arr, int pshared);
pthread_mutexattr_init : 互斥锁属性初始化
int pthread_mutexattr_init(pthread_mutexattr_t *attr);
这把锁为了让所有进程都看到,所以这把锁声明在共享区。
// 加锁之后的代码
#include "head.h"
#define INS 5
struct Num {
int now;
int sum;
pthread_mutex_t mutex;
};
struct Num *share_memory;
void do_add(int x) {
while (1) {
pthread_mutex_lock(&share_memory->mutex);
if (share_memory->now > 1000) {
pthread_mutex_unlock(&share_memory->mutex);
break;
};
share_memory->sum += share_memory->now;
share_memory->now++;
pthread_mutex_unlock(&share_memory->mutex);
printf("Child : %d, share_memory->now : %d, share_memory->sum : %d\n", \
x, share_memory->now, share_memory->sum);
usleep(100);
}
return ;
}
int main() {
pid_t pid;
int x = 0, shmid;
key_t key = ftok(".", 2021);
if ((shmid = shmget(key, sizeof(struct Num), IPC_CREAT | IPC_EXCL | 0666)) < 0) {
perror("shmget");
exit(1);
}
share_memory = (struct Num*) shmat(shmid, NULL, 0);
if (share_memory < 0) {
perror("shmat");
exit(1);
}
share_memory->now = 0;
share_memory->sum = 0;
// 锁初始化
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_setpshared(&attr, 1);
pthread_mutex_init(&(share_memory->mutex), &attr);
for (int i = 1; i <= INS ;i++) {
if ((pid = fork()) < 0) {
perror("fork");
}
x = i;
if (pid == 0) break;
}
if (pid == 0) {
do_add(x);
} else {
for (int i = 1; i <= INS; i++) {
wait(NULL);
}
}
shmdt(share_memory);
perror("shmdt");
printf("share_memory->sum = %d, My Pid : %d\n", share_memory->sum, getpid());
return 0;
}
使用锁,需要初始化一个锁属性,然后在需要的地方放置锁,初始化锁
锁的条件变量 pthread_cond_…
锁条件变量是一个同步发生的设备,允许线程们当遇到某个条件去挂起或者放弃CPU.
条件变量的基本操作是:1.发出一个信号,2.然后等待这个信号,并在挂起的线程的会在收到这个信号执行.
类比:朋友叫我吃饭,我不饿,饿的时候再吃.朋友这个线程会被挂起,然后我饿的时候,发出一个信号,饿!, 朋友收到这个信号,然后运行.
条件变量一定是在锁的基础上的,这个是为了避免信号发出在等待这个信号的线程之前.(避免我说完饿了,朋友才开始等着去接收这个信号,于是错过了信号).
pthread_cond_init 初始化一个条件变量.初始化这个条件变量可能需要cond_attr(条件变量属性), 如果没有这个属性,也就是cond_attr 被设置为空的话,那么Linux 线程支持没有属性的条件变量,因此条件属性是被忽视的.
可以用PTHREAD_COND_INITIALIZE
静态的初始化条件变量.
条件信号(pthread_cond_signal
)只会重启一个正在等待的线程,如果没有线程等待这个条件变量,那么什么都不会发生,如果很多个条件变量在等你,那么有一个被重启,不过不确定是哪一个…
条件变量广播(pthread_cond_broadcast
)重启所有等待的线程,没有等待的情况下同上.
等待条件变量(pthread_cond_wait
) 会自动的解互斥锁(自动执行pthread_unloock_mutex
),然后等待条件信号响应,线程会被挂起,不占用任何时间,直到信号响应.在等待条件变量之前,互斥锁一定是被锁上的,在被唤醒之后重新获得互斥锁.
所以上述整体流程大概是:先加锁->发现需要等待信号:解锁->等待->唤醒->加锁.
上述的解锁操作是自动执行的,所以,如果所有的线程等待这个信号来占据锁,需要保证在加锁和等待条i按变量的时候,条件变量没有发信号,也就是原子性.
条件超时(pthread_cond_timedwait
) 顾名思义,超时之后就不等了
条件销毁(pthread_cond_destory
)
// 完整无BUG 代码
#include "head.h"
#define INS 5
struct Num {
int now;
int sum;
pthread_mutex_t mutex;
pthread_cond_t cond;
};
struct Num *share_memory;
void do_add(int x) {
while (1) {
pthread_mutex_lock(&share_memory->mutex);
// printf("Child : %d Begin wait!\n", x);
pthread_cond_wait(&share_memory->cond, &share_memory->mutex);
int flag = 0;
for (int i = 0; i < 100; i++) {
if (share_memory->now > 1000) {
pthread_mutex_unlock(&share_memory->mutex);
usleep(100); //小小休息一下,防止在等待之前接收信号
pthread_cond_signal(&share_memory->cond);
// printf("Child %d exit\n" , x);
shmdt(share_memory); // 每个子进程在退出之前要把自己的附着删掉
printf("Child %d exit\n", x);
exit(0);
break;
};
share_memory->sum += share_memory->now;
share_memory->now++;
printf("Child : %d, share_memory->now : %d, share_memory->sum : %d\n", \
x, share_memory->now, share_memory->sum);
fflush(stdout);
}
pthread_mutex_unlock(&share_memory->mutex);
pthread_cond_signal(&share_memory->cond);
}
return ;
}
int main() {
pid_t pid;
int x = 0, shmid;
key_t key = ftok(".", 2021);
if ((shmid = shmget(key, sizeof(struct Num), IPC_CREAT | IPC_EXCL | 0666)) < 0) {
perror("shmget");
exit(1);
}
share_memory = (struct Num*) shmat(shmid, NULL, 0);
if (share_memory < 0) {
perror("shmat");
exit(1);
}
share_memory->now = 0;
share_memory->sum = 0;
// 锁初始化
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_setpshared(&attr, 1);
//初始化环境变量
pthread_condattr_t c_attr;
pthread_condattr_init(&c_attr);
pthread_condattr_setpshared(&c_attr, 1);
pthread_cond_init(&share_memory->cond, &c_attr);
pthread_mutex_init(&(share_memory->mutex), &attr);
for (int i = 1; i <= INS ;i++) {
if ((pid = fork()) < 0) {
perror("fork");
}
x = i;
if (pid == 0) break;
}
if (pid == 0) {
do_add(x);
} else {
usleep(500);// 不休息的话,可能发信号的时候,子进程还没有等待
printf("Parent!\n");
pthread_cond_signal(&share_memory->cond);
for (int i = 1; i <= INS; i++) {
wait(NULL);
}
}
printf("share_memory->sum = %d, My Pid : %d\n", share_memory->sum, getpid());
shmdt(share_memory);// 删除父进程的附着部分
shmctl(shmid, IPC_RMID, NULL); // 只有在shmctl 中才能删除共享段。
perror("shmctl");
return 0;
}
出现file exits 是因为我们在操作系统中创建的共享段没有删除,只是除去了共享段的所有附着进程.
shmctl 可以控制共享段部分
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
通过cmd 的部分来控制shmid指向的共享段。
struct shmid_ds {
struct ipc_perm shm_perm; /* Ownership and permissions */
size_t shm_segsz; /* Size of segment (bytes) */
time_t shm_atime; /* Last attach time */
time_t shm_dtime; /* Last detach time */
time_t shm_ctime; /* Last change time */
pid_t shm_cpid; /* PID of creator */
pid_t shm_lpid; /* PID of last shmat(2)/shmdt(2) */
shmatt_t shm_nattch; /* No. of current attaches */
...
};
在shmat 和 shmdt 中提到了shm_atime
, shm_dtime
, shm_nattch
struct ipc_perm {
key_t __key; /* Key supplied to shmget(2) */
uid_t uid; /* Effective UID of owner */
gid_t gid; /* Effective GID of owner */
uid_t cuid; /* Effective UID of creator */
gid_t cgid; /* Effective GID of creator */
unsigned short mode; /* Permissions + SHM_DEST and
SHM_LOCKED flags */
unsigned short __seq; /* Sequence number */
};
cmd :
IPC_STAT
: 从内核中拷贝shmid 所内涵的信息,到这个函数的第三个参数中,在执行这个命令的时候的前提是该进程对共享段可读.IPC_SET
: 声明好一个shmid_ds信息,然后执行这个命令,会向内核中写入shmid的信息,在这个时候会更新shm_ctime,但不是所有内容都可以改变,可以改变的有shm_perm.uid
,shm_perm.gid
,和shm_perm.mode
后9位, 只有当前这个进程的所属用户或者权限更高的用户(比如 root) c爱可以改变这些值.IPC_RMID
: 摧毁标记段,这个段在摧毁前需要保证最后一个附着进程也需要解除附着,调用者必须是创造这个共享段的进程,或者是更权威的进程,在这个命令中 buf 这个参数被忽略.调用者必须保证这个段最终被摧毁,否则这个内存页会一直保留在内存的交换区.IPC_INFO
: 返回系统共享内存的宽度限制和参数,这个buf 的数据结构是shmimfo
所以用的时候需要进行一个类型转换.
struct shminfo {
unsigned long shmmax; /* Maximum segment size */
unsigned long shmmin; /* Minimum segment size;
always 1 */
unsigned long shmmni; /* Maximum number of segments */
unsigned long shmseg; /* Maximum number of segments
that a process can attach;
unused within kernel */
unsigned long shmall; /* Maximum number of pages of
shared memory, system-wide */
};
SHM_INFO
: 返回一个shm_info
结构,包含了系统资源被共享段使用的信息.
struct shm_info {
int used_ids; /* # of currently existing
segments */
unsigned long shm_tot; /* Total number of shared
memory pages */
unsigned long shm_rss; /* # of resident shared
memory pages */
unsigned long shm_swp; /* # of swapped shared
memory pages */
unsigned long swap_attempts;
/* Unused since Linux 2.4 */
unsigned long swap_successes;
/* Unused since Linux 2.4 */
};
以下命令为Linux 独有命令
1.SHM_STAT
: 返回一个shmid_ds
数据结构,对比与IPC_STAT
不同的是,shmid 不是shmget 返回的共享段的id, 而是在内核中,共享段的索引
通过以下命令调用者可以阻止或者允许共享段的交换(换出内存)
SHM_LOCK
: 阻止共享段的交换SHM_UNLOCK
: 解锁该段,允许他交换.