一、线程的同步互斥机制

1.1 引入目的

  1. 由于多个线程共用进程的资源,所有可以通过全局资源完成多个进程的之间消息的传递
  2. 临界资源:多个线程共同使用的全局资源称为临界资源
  3. 临界区:访问临界资源的代码段称为临界区
  4. 多个进程访问临界资源时,会产生相互抢占的现象,该线程称为竞态,为了防止竞态的产生,引入了同步互斥机制
  5. 互斥:同一时刻,只允许一个线程使用临界资源,其他线程处于等待状态,直到拥有临界资源的线程释放了临界资源的使用权,没有先后顺序
  6. 同步:多个线程有顺序的访问临界资源

1.2 互斥

  1. 互斥机制中引入了互斥锁
  2. 互斥锁的本质也是一个特殊的临界资源,当某个线程抢到该锁资源后,其他线程只能处于等待状态,直到该线程释放锁资源
  3. 对于互斥锁的操作分别为:创建互斥锁、获取锁资源、释放锁资源、销毁互斥锁
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
30
31
32
33
34
#include <pthread.h>
1、创建互斥锁,定义一个互斥锁变量

//pthread_mutex_t fastmutex = PTHREAD_MUTEX_INITIALIZER; //静态初始化
pthread_mutex_t mutex;
2、动态初始化互斥锁

int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr);
功能:初始化锁资源
参数1:互斥锁地址
参数2:互斥锁的属性,一般填NULL
返回值:总是返回0

3、上锁

int pthread_mutex_lock(pthread_mutex_t *mutex);
功能:给临界区上锁
参数:互斥锁的地址
返回值:成功返回0,失败返回错误码

4、解锁

int pthread_mutex_unlock(pthread_mutex_t *mutex);
功能:释放锁资源的使用
参数:互斥锁地址
返回值:成功返回0,失败返回错误码

5、销毁锁

int pthread_mutex_destroy(pthread_mutex_t *mutex);
功能:销毁锁资源
参数:互斥锁地址
返回值:成功返回0,失败返回错误码

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
#include<myhead.h>

char buf[128]; //全局数组,临界资源

//1、创建一个互斥锁
pthread_mutex_t mutex;

//定义分支线程
void *task(void *arg)
{
while(1)
{
//3、获取锁资源
pthread_mutex_lock(&mutex);

printf("分支线程中:buf = %s\n", buf);
strcpy(buf, "I love China\n");

//4、释放锁资源
pthread_mutex_unlock(&mutex);
}
}


int main(int argc, const char *argv[])
{
//定义线程号变量
pthread_t tid;

//2、初始化互斥锁
pthread_mutex_init(&mutex, NULL);

//创建线程
if(pthread_create(&tid, NULL, task, NULL) != 0)
{
printf("tid create error\n");
return -1;
}

//主线程
while(1)
{
//3、获取锁资源
pthread_mutex_lock(&mutex);

printf("主线程中buf = %s\n", buf); //访问临界资源
strcpy(buf, "hello world\n");

//4、释放锁资源
pthread_mutex_unlock(&mutex);
}



pthread_join(tid, NULL); //阻塞回收线程资源
//5、销毁锁资源
pthread_mutex_destroy(&mutex);

return 0;
}

1.3 同步机制之无名信号量

  1. 同步:多个线程有顺序的执行
  2. 无名信号量:本质上也是一个临界资源,维护了一个value值,每个线程要执行时,先申请该value值,申请成功时,将value值减1,如果申请时,value值为0,则该线程在申请处阻塞,直到另一个线程将该无名信号量的value值增加到大于0.
  3. 线程同步多用于生产者消费者模型:生生产者生产数据,消费者消费数据
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
30
31
32
33
34
35
36
37
38
39
40
41
#include <semaphore.h>

1. 初始化信号量
sem_t semaphore;
sem_init 函数用于对信号量进行动态初始化
int sem_init(sem_t *sem, int pshared, unsigned int value);
参数:
- sem: 指向信号量变量的指针
- pshared: 表示信号量是否应在进程之间共享的标志(对于线程为0,对于进程为非零)
- value: 信号量的初始值
返回值:成功返回0,失败返回-1
示例:
sem_init(&semaphore, 0, 1); // 初始化一个初始值为1的信号量

2. 等待(减少)信号量
int sem_wait(sem_t *sem);
功能:减少信号量的值。如果值大于0,则立即减少并立即返回。
如果值为0,则该函数阻塞,直到信号量的值大于0
参数:
- sem: 指向信号量变量的指针
返回值:成功返回0,失败返回-1
示例:
sem_wait(&semaphore); // 在信号量上等待

3. 发送(增加)信号量
int sem_post(sem_t *sem);
功能:增加信号量的值。如果有等待的线程,则其中一个被解除阻塞。
参数:
- sem: 指向信号量变量的指针
返回值:成功返回0,失败返回-1
示例:
sem_post(&semaphore); // 发送信号表明信号量可用

// 4. 销毁信号量
// int sem_destroy(sem_t *sem);
// 功能:销毁信号量并释放相关资源。
// 参数:
// - sem: 指向信号量变量的指针
// 返回值:成功返回0,失败返回-1
// 示例:
// sem_destroy(&semaphore); // 销毁信号量
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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
#include<myhead.h>

//1、创建一个无名信号量
sem_t sem;

//生产者线程
void *task1(void *arg)
{
while(1)
{
sleep(2);
printf("我生产了一辆特斯拉\n");

//4、释放资源
sem_post(&sem);
}
}

//消费者线程
void *task2(void *arg)
{
while(1)
{
//3、申请资源,如果没有资源,则在该处阻塞
sem_wait(&sem);

printf("我消费了一辆特斯拉\n");
}
}



/********************主线程******************/
int main(int argc, const char *argv[])
{
//创建两个线程
pthread_t tid1,tid2;

//2、初始化无名信号量
sem_init(&sem, 0, 0);
//第一个0:表示用于线程之间的通信
//第二个0:表示value初始值为0

//创建生产者线程
if(pthread_create(&tid1, NULL, task1, NULL) != 0)
{
printf("tid1 create error\n");
return -1;
}
//创建消费者线程
if(pthread_create(&tid2, NULL, task2, NULL) != 0)
{
printf("tid2 create error\n");
return -1;
}


//主线程回收资源
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);

//5、销毁无名信号量
sem_destroy(&sem);

return 0;
}

1.4 同步机制之条件变量

1> 条件变量本质上也是一个临界资源,维护了一个队列,当消费者要想指向前,先进入等待队列中,直到生产者唤醒后,才能执行

2> 由于多个消费者线程要进入等待队列时,可能产生竞态,为了解决该竞态,同样要引入互斥机制

3> 条件变量表示一个生产者和多个消费者之间的同步关系,而消费者和消费者之间没有同步关系

3> 条件变量的API

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
30
31
32
33
34
35
1、创建条件变量
pthread_cond_t cond;

2、初始化条件变量
int pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t *cond_attr);
功能:初始化给定的条件变量
参数1:条件变量的地址
参数2:条件变量的属性,一般填NULL
返回值:成功返回0,失败返回一个错误码

3、消费者线程进入等待队列
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
功能:进入等待队列,等待被唤醒
参数1:条件变量的地址
参数2:互斥锁变量地址
返回值:成功返回0,失败返回一个错误码

4、唤醒消费者线程
int pthread_cond_signal(pthread_cond_t *cond);
功能:通知等待队列中的第一个线程进入唤醒状态
参数:条件变量地址
返回值:成功返回0,失败返回一个错误码


int pthread_cond_broadcast(pthread_cond_t *cond);
功能:唤醒等待队列中的所有线程
参数:条件变量地址
返回值:成功返回0,失败返回一个错误码

5、销毁条件变量
int pthread_cond_destroy(pthread_cond_t *cond);
功能:销毁条件变量
参数:条件变量地址
返回值:成功返回0,失败返回一个错误码

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
#include<myhead.h>
//1、定义条件变量
pthread_cond_t cond;

//11、定义互斥锁变量
pthread_mutex_t mutex;


//创建生产者线程
void *task1(void*arg)
{
/*
int num = 4;
while(num--)
{
sleep(2);
printf("%#lx:生产了一辆特斯拉\n", pthread_self());

//4、唤醒消费者线程
pthread_cond_signal(&cond);
}*/

sleep(5);
printf("%#lx:生产了四辆特斯拉\n", pthread_self());
//唤醒所有的等待线程
pthread_cond_broadcast(&cond);

pthread_exit(NULL); //退出线程

}


//创建消费者线程
void *task2(void*arg)
{
//33、获取锁资源
pthread_mutex_lock(&mutex);

//3、消费者进入等待队列等待生产者唤醒
pthread_cond_wait(&cond, &mutex);
//1>将消费者线程放入等待队列
//2>解锁
//3>当前线程阻塞等待
//
//4>被其他线程唤醒
//5>再次上锁



printf("%#lx:消费了一辆特斯拉\n", pthread_self());

//44、解锁
pthread_mutex_unlock(&mutex);

pthread_exit(NULL); //退出线程

}



/*************************主程序*************************/
int main(int argc, const char *argv[])
{

//2、初始化条件变量
pthread_cond_init(&cond, NULL);

//22、初始化互斥锁
pthread_mutex_init(&mutex, NULL);

//创建一个生产者和多个消费者线程
pthread_t tid1,tid2,tid3,tid4,tid5;
//创建生产者线程
if(pthread_create(&tid1, NULL, task1, NULL) != 0)
{
printf("生产者创建失败\n");
return -1;
}

//创建多个消费者线程
if(pthread_create(&tid2, NULL, task2, NULL) != 0)
{
printf("消费者创建失败\n");
return -1;
}

if(pthread_create(&tid3, NULL, task2, NULL) != 0)
{
printf("消费者创建失败\n");
return -1;
}
if(pthread_create(&tid4, NULL, task2, NULL) != 0)
{
printf("消费者创建失败\n");
return -1;
}
if(pthread_create(&tid5, NULL, task2, NULL) != 0)
{
printf("消费者创建失败\n");
return -1;
}

printf("tid1=%#lx, tid2=%#lx, tid3=%#lx, tid4=%#lx, tid5=%#lx\n",\
tid1,tid2,tid3,tid4,tid5);

//回收线程资源
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
pthread_join(tid3, NULL);
pthread_join(tid4, NULL);
pthread_join(tid5, NULL);

//5、销毁条件变量
pthread_cond_destroy(&cond);

//55、销毁锁资源
pthread_mutex_destroy(&mutex);

return 0;
}

1.5 同步机制之读写锁

1> 读写锁是一种特殊的互斥锁,用于解决多个线程同时对共享资源进行读操作的问题

2> 当多个线程同时对共享资源进行读操作时,可以使用读写锁进行同步,提高效率

3> 读写锁的API:

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
30
31
32
33
cCopy code1、创建读写锁
pthread_rwlock_t rwlock;

2、初始化读写锁
int pthread_rwlock_init(pthread_rwlock_t *rwlock, const pthread_rwlockattr_t *attr);
功能:初始化读写锁
参数1:读写锁的地址
参数2:读写锁的属性,一般填NULL
返回值:成功返回0,失败返回一个错误码

3、获取读锁
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
功能:获取读锁
参数:读写锁的地址
返回值:成功返回0,失败返回一个错误码

4、获取写锁
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
功能:获取写锁
参数:读写锁的地址
返回值:成功返回0,失败返回一个错误码

5、释放锁
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
功能:释放锁
参数:读写锁的地址
返回值:成功返回0,失败返回一个错误码

6、销毁读写锁
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
功能:销毁读写锁
参数:读写锁的地址
返回值:成功返回0,失败返回一个错误码
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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
#include <myhead.h>

//1、定义读写锁
pthread_rwlock_t rwlock;

//生产者线程
void *write_task(void *arg)
{
while (1)
{
sleep(2);
printf("写线程:%#lx写入数据\n", pthread_self());

//4、释放写锁
pthread_rwlock_unlock(&rwlock);
}
}

//消费者线程
void *read_task(void *arg)
{
while (1)
{
//3、获取读锁
pthread_rwlock_rdlock(&rwlock);

printf("读线程:%#lx读取数据\n", pthread_self());

//4、释放读锁
pthread_rwlock_unlock(&rwlock);

sleep(1);
}
}

/*******************主线程*******************/
int main(int argc, const char *argv[])
{

//2、初始化读写锁
pthread_rwlock_init(&rwlock, NULL);

//创建多个读线程和写线程
pthread_t tid1, tid2, tid3, tid4, tid5;
//创建写线程
if (pthread_create(&tid1, NULL, write_task, NULL) != 0)
{
printf("写线程创建失败\n");
return -1;
}

//创建多个读线程
for (int i = 0; i < 4; ++i)
{
if (pthread_create(&tid2, NULL, read_task, NULL) != 0)
{
printf("读线程创建失败\n");
return -1;
}
}

printf("tid1=%#lx, tid2=%#lx, tid3=%#lx, tid4=%#lx, tid5=%#lx\n", tid1, tid2, tid3, tid4, tid5);

//回收线程资源
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
pthread_join(tid3, NULL);
pthread_join(tid4, NULL);
pthread_join(tid5, NULL);

//5、销毁读写锁
pthread_rwlock_destroy(&rwlock);

return 0;
}

二、进程间的通信

2.1 通信概念

1> 多个线程之间的信息通信可以使用全局变量来完成,只需注意同步互斥机制即可

2> 多个进程之间的信息通信,不可用使用全局变量完成,因为多个进程之间用户空间是独立的,每个进程拥有自己的全局变量

3> 可以使用外部文件完成多个进程之间通信,一个进程向文件中写入数据,另一个进程从文件中读取数据。但是由于进程的调度是时间片轮询机制,不确定哪个进程先执行,所以该方案需要在多进程同步的基础上完成

4> 由于多个进程之间用户空间是独立的,但是内核空间是共享的,所以可以利用内核空间完成通信,一个进程向内核空间写入数据,另一个进程可以从内核空间取出数据

img

5> 通信方式

1、内核提供了三种通信方式:无名管道、有名管道、信号

2、System V提供了三种通信方式:消息队列、共享内存、信号量(信号灯集)

3、套接字通信

2.2 管道

1> 在内核空间创建出一个特殊的文件(管道文件)

2> 对于管道文件中的数据操作是一次性的,当管道中的数据被读取后,就不存在了

3> 管道分为无名管道和有名管道:无名管道仅用于亲缘进程间的通信,而有名管道,既可以用于亲缘进程间通信,也可以用于非亲缘进程间通信

4> 管道的通信是半双工通信方式:

Plain Text

自动换行

1
2
3
单工:只能A进程向B进程发送数据
半双工:同一时刻,只能A向B发数据或B向A发数据
全双工:同一时刻,AB进程可以互发消息

5> 管道遵循先进先出的原则:先写入的数据会先读取出来

6> 一个管道被打开后,会产生两端,分别是读端和写端,当两端全部都被关闭后,管道在内核中就消失了

7> 由于管道存在内核空间,对管道的操作只能使用系统调用(文件IO),而且不能使用lseek移动光标

2.3 无名管道

1> 无名管道:顾名思义就是没有名字的管道,不存在于文件系统中,管道文件创建出来后,在内存中存放

2> 由于在内存中存放,没有文件系统中的名字,所以,一个进程创建出来的无名管道文件,其他进程是没有办法打开的,所以该方式不适用于非亲缘进程间通信

3> 无名管道,只适用于亲缘进程间通信,并且,需要在创建子进程之前将管道文件打开

4> 无名管道的api

1
2
3
4
5
6
7
8
       #include <unistd.h>

int pipe(int pipefd[2]);
功能:创建一个无名管道,并通过参数返回管道文件的两端,pipefd[0]表示管道的读端,pipefd[1]表示管道的写端
参数:要返回管道文件两端的文件描述符数组
返回值:成功返回0,失败返回-1并置位错误码


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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
#include<myhead.h>

int main(int argc, const char *argv[])
{
//定义进程号变量
pid_t pid;

//创建一个无名管道
int pipefd[2];
if(pipe(pipefd) != 0)
{
perror("pipe error");
return -1;
}

printf("pipefd[0]=%d, pipefd[1]=%d\n", pipefd[0], pipefd[1]);//3,4

//创建子进程
pid = fork();

if(pid > 0)
{
//父进程,拥有管道文件的两个文件描述符
//关闭读端
close(pipefd[0]);

char buf[128] = "hello world";

//将数据写入到管道文件中
write(pipefd[1], buf, sizeof(buf));

//关闭写端
close(pipefd[1]);


}else if(pid == 0)
{
//子进程,也有用管道文件的两个文件描述符
//关闭写段
close(pipefd[1]);

//从管道文件中读取数据
char rbuf[128] = "";

read(pipefd[0], rbuf, sizeof(rbuf));

printf("子进程中收到:%s\n", rbuf);

//关闭读端
close(pipefd[0]);

//退出子进程
exit(EXIT_SUCCESS);


}else
{
perror("fork error");
return -1;
}


wait(NULL); //回收子进程资源

return 0;
}

练习:创建一个无名管道,父进程循环向管道文件中写入数据,子进程循环从管道文件中读取数据,直到写入"quit"后退出进程

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
#include<myhead.h>

int main(int argc, const char *argv[])
{
//定义进程号变量
pid_t pid;

//创建一个无名管道
int pipefd[2];
if(pipe(pipefd) != 0)
{
perror("pipe error");
return -1;
}

printf("pipefd[0]=%d, pipefd[1]=%d\n", pipefd[0], pipefd[1]);//3,4

//创建子进程
pid = fork();

if(pid > 0)
{
//父进程,拥有管道文件的两个文件描述符
//关闭读端
close(pipefd[0]);

char buf[128] = "";

while(1)
{
//清空数组
memset(buf, 0, sizeof(buf)); //bzero(buf, sizeof(buf));

//获取数据
read(0, buf, sizeof(buf));
buf[strlen(buf)-1] = '\0'; //将换行改成'\0'

//将数据写入到管道文件中
write(pipefd[1], buf, sizeof(buf));

if(strcmp(buf, "quit") == 0)
{
break;
}
}

//关闭写端
close(pipefd[1]);


}else if(pid == 0)
{
//子进程,也有用管道文件的两个文件描述符
//关闭写段
close(pipefd[1]);

//从管道文件中读取数据
char rbuf[128] = "";

while(1)
{
//清空数组
bzero(rbuf, sizeof(rbuf));

read(pipefd[0], rbuf, sizeof(rbuf));

printf("子进程中收到:%s\n", rbuf);

//判断
if(strcmp(rbuf, "quit") == 0)
{
break;
}
}

//关闭读端
close(pipefd[0]);

//退出子进程
exit(EXIT_SUCCESS);


}else
{
perror("fork error");
return -1;
}


wait(NULL); //回收子进程资源

return 0;
}

5> 管道的特性

1、可以使用无名管道完成自己跟自己通信

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
30
31
32
#include<myhead.h>

int main(int argc, const char *argv[])
{
//创建一个无名管道
int pipefd[2];

if(pipe(pipefd) != 0)
{
perror("pipe error");
return -1;
}

//定义一个容器
char wbuf[128] = "hello world";

//将数据写入到管道中
write(pipefd[1], wbuf, sizeof(wbuf));

//定义一个容器
char rbuf[128] = "";
read(pipefd[0], rbuf, sizeof(rbuf));

printf("rbu = %s\n", rbuf);

//关闭文件描述符
close(pipefd[0]);
close(pipefd[1]);

return 0;
}

2、管道的大小为:64K

Plain Text

自动换行

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
30
31
32
#include<myhead.h>

int main(int argc, const char *argv[])
{
//创建一个无名管道
int pipefd[2];

if(pipe(pipefd) != 0)
{
perror("pipe error");
return -1;
}


char ch = 'A'; //定义一个字符数据
int count = 0;

while(1)
{
write(pipefd[1], &ch, 1); //将一字节数据写入到管道中

count++;
printf("count = %d\n", count);
}

//关闭文件描述符
close(pipefd[0]);
close(pipefd[1]);

return 0;
}

3、管道的读写特点

当读端存在时:写管道有多少写多少,直到写满64k后,在write处阻塞

当读端不存在时:写管道写入数据后,会导致管道破裂

当写端存在时:读管道有多少读多少,没有数据的话,在read处阻塞

当写端不存在时:读管道有多少读多少,没有数据的话,不会在read处阻塞

2.4 有名管道

1> 有名管道:顾名思义就是有名字的管道,存储在文件系统中

2> 有名管道的操作也是一次性的,数据被读取后,就没有了

3> 虽然存储在外部磁盘上,但是,不用于存储信息,只是用于进程间通信

4> 有名管道既可以用于亲缘进程间通信,也可以用于非亲缘进程间的通信

5> 有名管道的api

1
2
3
4
5
6
7
8
       #include <sys/types.h>
#include <sys/stat.h>

int mkfifo(const char *pathname, mode_t mode);
功能:创建一个有名管道文件
参数1:有名管道文件的文件名称
参数2:有名管道的权限,最终权限:mode&~umask
返回值:成功返回0,失败返回-1并置位错误码

6> 案例实现

create.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include<myhead.h>

int main(int argc, const char *argv[])
{
//创建有名管道文件
if(mkfifo("./myfifo", 0664) != 0)
{
perror("mkfifo error");
return -1;
}

printf("myfifo create success\n");


//使用getchar阻塞一下
getchar();

system("rm myfifo");



return 0;
}

snd.c

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
30
31
32
33
34
35
36
37
38
39
40
41
42
#include<myhead.h>

int main(int argc, const char *argv[])
{
//以只写的形式打开管道文件
int wfd = -1;
if((wfd = open("./myfifo", O_WRONLY)) == -1)
{
perror("open error");
return -1;
}

printf("写端打开成功\n");

//向管道文件中循环写入数据
char wbuf[128] = "";
while(1)
{
bzero(wbuf, sizeof(wbuf)); //清空内容

//从终端输入数据
printf("请输入>>>>>");
fflush(stdout); //刷新行缓冲区
read(0, wbuf, sizeof(wbuf));
wbuf[strlen(wbuf)-1] = 0; //将回车换成'\0'

//将数据写入管道中
write(wfd, wbuf, sizeof(wbuf));

//判断
if(strcmp(wbuf, "quit") == 0)
{
break;
}
}

//关闭文件
close(wfd);

return 0;
}

recv.c

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
30
31
32
33
34
35
36
37
38
39
#include<myhead.h>

int main(int argc, const char *argv[])
{
//以只写的形式打开管道文件
int rfd = -1;
if((rfd = open("./myfifo", O_RDONLY)) == -1)
{
perror("open error");
return -1;
}

printf("读端打开成功\n");

//从管道文件中循环读取数据
char rbuf[128] = "";
while(1)
{
bzero(rbuf, sizeof(rbuf)); //清空内容

//从管道中读取数据
read(rfd, rbuf, sizeof(rbuf));

printf("收到消息:%s\n", rbuf);


//判断
if(strcmp(rbuf, "quit") == 0)
{
break;
}
}

//关闭文件
close(rfd);

return 0;
}

2.5 信号

2.5.1 信号基本概念

1> 信号是软件模拟硬件中的中断,中断是硬件实现的,信号是软件实现的

2> 中断:停止当前正在进行的事情,去执行其他事情,执行结束后,继续执行自己的事

3> 一个进程可以给另一个进程发送信号;用户也可以给一个进程发送信号;内核可以向进程发送信号

4> 信号通信,属于异步通信:两个进程各干各的,互不影响

5> 信号处理方式:默认(一般是杀死进程)、忽略、捕获(捕获信号去做另一件事)

2.5.2 信号的种类及功能

可以通过kill -l指令查看所有的信号

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
 1) SIGHUP	 2) SIGINT	 3) SIGQUIT	 4) SIGILL	 5) SIGTRAP
6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1
11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM
16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP
21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ
26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR
31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3
38) SIGRTMIN+4 39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8
43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7
58) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2
63) SIGRTMAX-1 64) SIGRTMAX

特殊的信号:
SIGKILL(9):杀死指定进程信号,该信号,既不能被捕获,也不能被忽略
SIGHUP(1):当进程所在的终端关闭后,终端进程会给当前终端中的每个进程发送该信号,默认操作杀死进程
SIGINT(2):当用户按下ctrl + c后,进程会收到该信号
SIGQUIT(3):退出指定的进程,用户键入ctrl + \时产生
SIGSEGV(11):当指针使用不当或堆栈溢出时,内核空间发射该信号,表示段错误
SIGCHLD(17):当子进程退出后,会向父进程发送该信号,表示我已经死了
SIGCONT(18):让暂停的进程继续运行
SIGSTOP(19)、SIGTSTP(20):让进程暂停执行
注意:SIGKILL和SIGSTOP信号,既不能被捕获,也不能被忽略

2.5.3 信号绑定函数(signal)

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <signal.h>

typedef void (*sighandler_t)(int);

sighandler_t signal(int signum, sighandler_t handler);
功能:将信号与信号处理方式绑定到一起
参数1:要绑定的信号号
参数2:信号处理方式
SIG_IGN:忽略
SIG_DFL:默认
有参无返回值函数的地址:信号处理函数
返回值:成功返回信号处理函数,失败返回SIG_ERR,并置位错误码

1> signal函数的实例

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
#include<myhead.h>

//定义信号处理函数
void handler(int signum)
{
if(signum == SIGINT) //表明要处理2号信号
{
printf("用户按下了ctrl + c键\n");
}

if(signum == SIGTSTP) //处理暂停信号
{

printf("用户按下了ctrl + z键\n");
}
}

/****************************主程序********************/
int main(int argc, const char *argv[])
{

/*将SIGINT信号忽略
if(signal(SIGINT, SIG_IGN) == SIG_ERR)
{
perror("signal error");
return -1;
}
*/

/*将SIGINT信号默认处理
if(signal(SIGINT, SIG_DFL) == SIG_ERR)
{
perror("signal error");
return -1;
}
*/

//将SIGINT信号捕获
if(signal(SIGINT, handler) == SIG_ERR)
{
perror("signal error");
return -1;
}

//绑定SIGTSTP信号 ctrl + z
if(signal(SIGTSTP, handler) == SIG_ERR)
{
perror("signal error");
return -1;
}



while(1)
{
sleep(1);
printf("我真的还想再活五百年\n");
}

return 0;
}

2> 尝试捕获SIGSTOP信号

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
#include<myhead.h>

//定义信号处理函数
void handler(int signum)
{
if(signum == SIGINT) //表明要处理2号信号
{
printf("用户按下了ctrl + c键\n");
}

if(signum == SIGTSTP) //处理暂停信号
{

printf("用户按下了ctrl + z键\n");
}


if(signum == SIGSTOP) //处理暂停信号
{

printf("用户按下了ctrl + z键\n");
}
}

/****************************主程序********************/
int main(int argc, const char *argv[])
{
/*尝试忽略SIGSTOP信号
if(signal(SIGSTOP, SIG_IGN) == SIG_ERR)
{
perror("signal error");
return -1;
}
*/

/*尝试捕获SIGSTOP信号
if(signal(SIGSTOP, handler) == SIG_ERR)
{
perror("signal error");
return -1;
}
*/

//尝试默认处理SIGSTOP信号
if(signal(SIGSTOP, SIG_DFL) == SIG_ERR)
{
perror("signal error");
return -1;
}

while(1)
{
sleep(1);
printf("我真的还想再活五百年\n");
}

return 0;
}

img

3> 使用信号的方式以非阻塞的形式回收僵尸进程(SIGCHLD)

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
30
31
32
33
34
35
36
37
38
39
#include<myhead.h>

//定义信号处理函数
void handler(int signum)
{
if(signum == SIGCHLD)
{
//以非阻塞的形式回收僵尸进程,直到所有的僵尸进程全部回收结束
while(waitpid(-1, NULL, WNOHANG) >0 );
}
}


int main(int argc, const char *argv[])
{


//捕获子进程发来的SIGCHLD信号
if(signal(SIGCHLD, handler) == SIG_ERR)
{
perror("signal error");
return -1;
}

for(int i=0; i<10; i++)
{
if(fork() == 0)
{
exit(EXIT_SUCCESS);
}
}


while(1);


return 0;
}

4> 使用闹钟信号完成一个定时器(SIGALRM),该信号由alarm函数设置的时间超时后产生

1
2
3
4
5
6
7
       #include <unistd.h>

unsigned int alarm(unsigned int seconds);
功能:启动一个定时器,当定时器超时后,会发送一个SIGALRM信号
参数:超时时间,以秒为单位
返回值:如果之前没有设置定时器,则返回0,如果已经设置了,则返回上一个定时器的剩余时间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include<myhead.h>

int main(int argc, const char *argv[])
{
printf("%d\n", alarm(10)); //0

sleep(5);


printf("%d\n", alarm(10)); //5

while(1);

return 0;
}

使用SIGALRM信号模拟斗地主出牌场景

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
30
31
32
33
34
35
36
37
38
39
40
#include<myhead.h>

//定义信号处理函数
void handler(int signo)
{
if(signo == SIGALRM)
{
printf("系统已经随机为您出一张牌\n");
alarm(5);
}
}



int main(int argc, const char *argv[])
{

char ch;

//将SIGALRM信号绑定到信号处理函数中
if(signal(SIGALRM, handler) == SIG_ERR)
{
perror("signal error");
return -1;
}

while(1)
{
alarm(5);

printf("请出牌:");
scanf("%c", &ch);
getchar();

printf("您出的牌为:%c\n", ch);
}

return 0;
}

2.5.4 信号发送函数(kill、raise)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
       #include <signal.h>

int kill(pid_t pid, int sig);
功能:向指定的进程或进程组内发射信号
参数1:信号号
>0: 表示向特定的进程发送信号
=0:向当前进程所在的进程组内发射信号
=-1:向所有进程发送信号
<-1:向指定进程组(组ID为pid的绝对值)发送信号
参数2:要发送的信号号
返回值:成功返回0,失败返回-1并置位错误码

#include <signal.h>

int raise(int sig);
功能:向当前进程发送信号:kill(getpid(), sig)
参数:要发送的信号号
返回值:成功返回0,失败返回非0数字

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
#include<myhead.h>

//定义信号处理函数
void handler(int signo)
{
if(signo == SIGUSR1)
{
printf("逆子何至于此\n");

raise(SIGKILL); //自杀
}
}


int main(int argc, const char *argv[])
{

//定义进程号
pid_t pid = fork();

if(pid > 0)
{
//父进程

//将SIGUSR1信号绑定
if(signal(SIGUSR1, handler) == SIG_ERR)
{
perror("signal error");
return -1;
}

while(1)
{
printf("我真的还想再活五百年\n");
sleep(1);
}

}else if(pid == 0)
{
//子进程
printf("人生得意须尽欢,莫使金樽空对月\n");

sleep(3);

printf("我看透了红尘, 父亲跟我一起走吧\n");

//向父进程发送一个信号
kill(getppid(), SIGUSR1);

exit(EXIT_SUCCESS); //退出进程


}else
{
perror("fork error");
return -1;
}


return 0;
}

2.6 System V进程间通信(IPC对象)

1> 种类:

消息队列:在内核空间维护了一个队列,所有进程都可以向队列中存放数据,也可以从队列中取出数据

共享内存:将物理内存映射出来,共所有进程使用

信号量(信号灯集):主要完成进程的同步工作

2> 关于IPC的相关指令

ipcs: 查看所有IPC对象的详细信息

ipcs -q:只查看消息队列的详细信息

ipcs -m:只查看共享内存的详细信息

ipcs -s:只查看信号量的详细信息

ipcrm -q/-m/-s ID:表示删除指定的消息队列、共享内存、信号量

3> 关于系统5提供的进程间通信对象,都需要使用一个key值

2.7 消息队列

1> 原理图

img

2> 有关消息队列的API

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
1、创建key值
#include <sys/types.h>
#include <sys/ipc.h>

key_t ftok(const char *pathname, int proj_id); //ftok("/", 't')
功能:创建IPC通信的秘钥,用于创建消息队列、共享内存、信号灯集使用
参数1:文件路径(必须是已经存在的路径),可以是任意一个
参数2:是一个随机值
返回值:一个四字节的整数,由两个参数组成,其中前8位由给定的文件的设备号提供, 中间16位由文件的inode号提供, 最后8位由第二个参数的底8位提供
失败返回-1并置位错误码

2、创建消息队列
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>

int msgget(key_t key, int msgflg);
功能:通过给定的key值在内核空间创建一个消息队列的通信对象
参数1:key值,可以由ftok函数产生,也可以是IPC_PRIVATE,如果是IPC_PRIVATE,表示该通信方式只适用于亲缘进程间
参数2:消息队列的模式位
IPC_CREAT:表示创建一个消息队列,如果消息队列存在,则直接打开消息队列,如果不存在则创建消息队列
IPC_EXCL:表示如果消息队列存在,则创建报错
权限:0664
例如:mesget(key, IPC_CREAT|0664)
返回值:成功返回创建出来的消息队列的id号,失败返回-1并置位错误码

3、向消息队列中存放数据
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>

int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
功能:向指定下的消息队列中存放消息
参数1:消息队列的id号
参数2:消息的起始地址
struct msgbuf {
long mtype; /* message type, must be > 0 */ 消息类型,用于后期其他进程从消息队列中选择消息进行取出
char mtext[1]; /* message data */ 消息数据
};
参数3:要存放消息正文的大小,不包含消息类型
参数4:是否阻塞
IPC_NOWAIT:非阻塞
0:表示阻塞
返回值:成功返回0,失败返回-1并置位错误码


4、从消息队列中取消息

ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
功能:从给定的消息队列中取出一条消息
参数1:消息队列id
参数2:要存放消息的容器
参数3:要取出消息的正文大小
参数4:要取出的消息类型
=0:表示取出消息队列中的第一个数据
>0:表示读取消息队列中类型为msgtyp的第一条消息
<0:表示从消息队列中取出一个消息,消息类型是小于或等于给定的msgtyp的绝对值中的第一个
5 --> 10 --> 4 --> 100 --> 20
type:-10
取出消息为:5
参数5:是否阻塞
IPC_NOWAIT:非阻塞
0:表示阻塞
返回值:成功返回实际读取的字节的个数,失败返回-1并置位错误码

5、消息队列的控制函数
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>

int msgctl(int msqid, int cmd, struct msqid_ds *buf);
功能:消息队列的控制函数
参数1:要操作的消息队列的id号
参数2:要执行的操作
IPC_RMID:表示删除指定的消息队列(常用),此时,第三个参数可以忽略填NULL
IPC_STAT:将消息队列的信息获取下来,并放入buf指向的一个结构体变量中
IPC_SET:表示设置消息队列的属性,要设置的信息在buf中
struct msqid_ds结构体类型:
struct msqid_ds {
struct ipc_perm msg_perm; /* Ownership and permissions */ 当前消息队列的所属用户和权限
time_t msg_stime; /* Time of last msgsnd(2) */ 最新一次存放消息的时间
time_t msg_rtime; /* Time of last msgrcv(2) */ 最新一次取消息的时间
time_t msg_ctime; /* Time of last change */ 最新一次状态更改的时间
unsigned long __msg_cbytes; /* Current number of bytes in 当前队列中已用字节数
queue (nonstandard) */
msgqnum_t msg_qnum; /* Current number of messages 当前消息队列中消息的个数
in queue */
msglen_t msg_qbytes; /* Maximum number of bytes 消息队列所能容纳的最大字节数默认(16K)
allowed in queue */
pid_t msg_lspid; /* PID of last msgsnd(2) */ 最新存放消息的进程进程号
pid_t msg_lrpid; /* PID of last msgrcv(2) */ 最新取消息的进程的进程号
};
关于第一个成员的结构体:
struct ipc_perm {
key_t __key; /* Key supplied to msgget(2) */ 消息队列的key值
uid_t uid; /* Effective UID of owner */ 当前用户操作的用户id
gid_t gid; /* Effective GID of owner */ 当前操作的组id
uid_t cuid; /* Effective UID of creator */ 创建消息队列进程的用户id
gid_t cgid; /* Effective GID of creator */ 创建消息队列进程的组id
unsigned short mode; /* Permissions */ 权限
unsigned short __seq; /* Sequence number */ 队列号
};

返回值:成功返回0,失败返回-1并置位错误码

发送端实现

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
#include<myhead.h>

//定义消息结构体类型
struct msgbuf
{
long mtype; //消息类型
char mtext[1024]; //消息正文
};

//定义一个宏,表示正文大小
#define SIZE (sizeof(struct msgbuf) - sizeof(long))


int main(int argc, const char *argv[])
{
//1、创建key值
key_t key = 0;
if((key = ftok("/", 't')) == -1)
{
perror("fork error");
return -1;
}

printf("key = %#x\n", key);

//2、使用key值创建一个消息队列
int msqid = 0;
if((msqid=msgget(key, IPC_CREAT|0664)) == -1)
{
perror("msgget error");
return -1;
}

printf("msqid = %d\n", msqid);

//此时就已经在内核空间创建出消息队列了
//定义一个消息类型的容器
struct msgbuf buf = {.mtype=100};

//3、循环向消息队列中存放数据
while(1)
{
printf("请输入要存放的数据:");
scanf("%s", buf.mtext);
getchar(); //吸收回车

//将消息存放到消息队列中
msgsnd(msqid, &buf, SIZE, 0);

printf("发送成功\n");

if(strcmp(buf.mtext, "quit") == 0)
{
break;
}
}




return 0;
}

接收端实现

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
#include<myhead.h>

//定义消息结构体类型
struct msgbuf
{
long mtype; //消息类型
char mtext[1024]; //消息正文

};

//定义一个宏,表示正文大小
#define SIZE (sizeof(struct msgbuf) - sizeof(long))


int main(int argc, const char *argv[])
{
//1、创建key值
key_t key = 0;
if((key = ftok("/", 't')) == -1)
{
perror("fork error");
return -1;
}

printf("key = %#x\n", key);

//2、使用key值创建一个消息队列
int msqid = 0;
if((msqid=msgget(key, IPC_CREAT|0664)) == -1)
{
perror("msgget error");
return -1;
}

printf("msqid = %d\n", msqid);

//此时就已经在内核空间创建出消息队列了
//定义一个消息类型的容器
struct msgbuf buf;

//3、循环向消息队列中存放数据
while(1)
{
//从消息队列中读取消息
msgrcv(msqid, &buf, SIZE, 0, 0);
//第一个0表示无视类型,每次都取第一个消息
//第二个0表示阻塞形式接收消息

printf("收到消息为:%s\n", buf.mtext);

if(strcmp(buf.mtext, "quit") == 0)
{
break;
}
}

//4、删除消息队列
if(msgctl(msqid, IPC_RMID, NULL) == -1)
{
perror("msgctl error");
return -1;
}




return 0;
}

2.8 共享内存

1> 所谓共享内存,是将物理内存映射到不同的进程中,不同进程直接对映射出来的共享内存进行操作,无需进行用户空间和内核空间的切换

2> 原理图

img

3> 共享内存的相关API

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
1、创建key值
#include <sys/types.h>
#include <sys/ipc.h>

key_t ftok(const char *pathname, int proj_id); //ftok("/", 't')
功能:创建IPC通信的秘钥,用于创建消息队列、共享内存、信号灯集使用
参数1:文件路径(必须是已经存在的路径),可以是任意一个
参数2:是一个随机值
返回值:一个四字节的整数,由两个参数组成,其中前8位由给定的文件的设备号提供, 中间16位由文件的inode号提供, 最后8位由第二个参数的底8位提供
失败返回-1并置位错误码

2、申请物理内存,创建出共享内存段
#include <sys/ipc.h>
#include <sys/shm.h>

int shmget(key_t key, size_t size, int shmflg);
功能:申请size空间的物理内存,映射出共享内存段,size是4096的倍数
参数1:创建共享内存的key值
参数2:要申请的大小,4096的倍数
参数3:消息队列的模式位
IPC_CREAT:表示创建一个共享内存,如果共享内存存在,则直接打开共享内存,如果不存在则创建共享内存
IPC_EXCL:表示如果共享内存存在,则创建报错
权限:0664
例如:shmget(key, IPC_CREAT|0664)
返回值:成功返回共享内存的id,失败返回-1并置位错误码

3、将共享内存映射到用户空间
#include <sys/types.h>
#include <sys/shm.h>

void *shmat(int shmid, const void *shmaddr, int shmflg);
功能:将共享内存地址映射到用户空间
参数1:共享内存id
参数2:共享内存的起始页地址,一般填NULL,让系统自动选择合适的对齐页地址
参数3:操作标识位
SHM_RDONLY:表示对共享内存只读功能
0:表示既可以读也可以写
返回值:成功返回映射的地址,失败返回(void *)-1,并置位错误码

4、取消共享内存的映射

int shmdt(const void *shmaddr);
功能:取消共享内存段的用户空间地址的映射
参数:用户空间映射的地址
返回值:成功返回0,失败返回-1并置位错误码

5、共享内存的控制函数
#include <sys/ipc.h>
#include <sys/shm.h>

int shmctl(int shmid, int cmd, struct shmid_ds *buf);
功能:根据给定的cmd的值确定执行方案
参数1:共享内存id
参数2:cmd控制
IPC_STAT:获取状态
IPC_SET:设置状态
IPC_RMID:删除共享内存
参数3:要设置的结构体起始地址
返回值:成功返回0,失败返回-1并置位错误码

1> 发送端

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
#include<myhead.h>
#define PAGE_SIZE 4096

int main(int argc, const char *argv[])
{
//1、创建key值
key_t key = -1;
if((key = ftok("/", 't')) == -1)
{
perror("ftok error");
return -1;
}
printf("key = %#x\n", key);

//2、将物理内存创建出共享内存段
int shmid = 0;
if((shmid = shmget(key, PAGE_SIZE, IPC_CREAT|0664)) == -1)
{
perror("shmget error");
return -1;
}
printf("shmid = %d\n", shmid);

//3、将共享内存段地址映射到用户空间
//NULL表示让系统自动选择页的分段
//0表示当前进程对共享内存具有读写功能
char *addr = (char *)shmat(shmid, NULL, 0);
if(addr == (void *)-1)
{
perror("shmat error");
return -1;
}
printf("addr = %p\n", addr);

//4、操作共享内存
//char buf[128] = "";
while(1)
{
fgets(addr, PAGE_SIZE, stdin); //从终端输入数据
addr[strlen(addr) - 1] = '\0'; //将换行换成'\0'

if(strcmp(addr, "quit") == 0)
{
break;
}
}


//5、取消映射

while(1);

return 0;
}

2> 接收端

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
#include<myhead.h>
#define PAGE_SIZE 4096

int main(int argc, const char *argv[])
{
//1、创建key值
key_t key = -1;
if((key = ftok("/", 't')) == -1)
{
perror("ftok error");
return -1;
}
printf("key = %#x\n", key);

//2、将物理内存创建出共享内存段
int shmid = 0;
if((shmid = shmget(key, PAGE_SIZE, IPC_CREAT|0664)) == -1)
{
perror("shmget error");
return -1;
}
printf("shmid = %d\n", shmid);

//3、将共享内存段地址映射到用户空间
//NULL表示让系统自动选择页的分段
//0表示当前进程对共享内存具有读写功能
char *addr = (char *)shmat(shmid, NULL, 0);
if(addr == (void *)-1)
{
perror("shmat error");
return -1;
}
printf("addr = %p\n", addr);

//4、操作共享内存
//char buf[128] = "";
while(1)
{

printf("共享内存中的数据为:%s\n", addr);
sleep(1);

if(strcmp(addr, "quit") == 0)
{
break;
}
}


//5、取消映射
if(shmdt(addr) == -1)
{
perror("shmdt error");
return -1;
}


//6、删除共享内存
if(shmctl(shmid, IPC_RMID, NULL) == -1)
{
perror("shmctl error");
return -1;
}

return 0;
}

2.9 信号量(信号灯集)

1> 信号灯集主要完成进程间同步工作,将多个信号灯,放在一个信号灯集中,每个信号灯控制一个进程

2> 每个灯维护了一个value值,当value值等于0时,申请该资源的进程处于阻塞状态,直到其他进程将该灯中维护的value值增加到大于0

3> 原理图

img

4> 有关信号灯集的API

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
1、创建key值
#include <sys/types.h>
#include <sys/ipc.h>

key_t ftok(const char *pathname, int proj_id); //ftok("/", 't')
功能:创建IPC通信的秘钥,用于创建消息队列、共享内存、信号灯集使用
参数1:文件路径(必须是已经存在的路径),可以是任意一个
参数2:是一个随机值
返回值:一个四字节的整数,由两个参数组成,其中前8位由给定的文件的设备号提供, 中间16位由文件的inode号提供, 最后8位由第二个参数的底8位提供
失败返回-1并置位错误码

2、创建信号灯集
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>

int semget(key_t key, int nsems, int semflg);
功能:创建一个信号灯集
参数1:key值,可以通过ftok创建出来,也可以使用IPC_PRIVATE
参数2:信号灯集中信号灯的个数,每一个信号灯控制一个进程
参数3:信号灯集的模式位
IPC_CREAT:表示创建一个信号灯集,如果信号灯集存在,则直接打开信号灯集,如果不存在则创建信号灯集
IPC_EXCL:表示如果信号灯集存在,则创建报错
权限:0664
例如:shmget(key, IPC_CREAT|0664)
返回值:成功返回信号灯集的ID,失败返回-1并置位错误码

3、信号灯集控制函数
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>

int semctl(int semid, int semnum, int cmd, ...);
功能:信号灯集的控制操作
参数1:信号灯集的ID
参数2:表示要操作的信号灯的编号,从0开始
参数3:操作命令
IPC_RMID:删除信号灯集,如果参数为IPC_RMID时,参数2和参数4可以忽略
IPC_STAT:获取信号灯集的属性
IPC_SET:设置信号灯集的属性
SETVAL:表示要设置信号灯的值
GETVAL:表示要获取信号灯的值
SETALL:表示对所有信号灯进行设置
GETALL:表示获取所有信号灯的值
参数4:一个可变参数,取决于cmd,如果需要改参数时,应该提供一个共用体,定义如下
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) */
};
说明:
1)如果cmd为SETVAL,则共用体只需要使用整形变量即可
2)如果cmd为GETALL, SETALL,则需要提供一个一维数组,表示给全部的信号灯进行值的操作
3)如果cmd为IPC_STAT, IPC_SET ,则需要定义一个结构体变量,将地址赋值到当前共用体成员中
struct semid_ds {
struct ipc_perm sem_perm; /* Ownership and permissions */
time_t sem_otime; /* Last semop time */
time_t sem_ctime; /* Last change time */
unsigned long sem_nsems; /* No. of semaphores in set */
};

struct ipc_perm {
key_t __key; /* Key supplied to semget(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 */
unsigned short __seq; /* Sequence number */
};

返回值:成功返回0,失败返回-1并置位错误码
举个例子:将信号灯集中的0号灯的值设置为1
union semun su; //定义一个共用体变量
su.val = 1; //使用整形变量的成员
semctl(semid, 0, SETVAL, su); //将0号灯的值设置为1
再举个例子:删除信号灯集
semctl(semid, 0, IPC_RMID);

4、申请和释放信号灯的资源函数
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>

int semop(int semid, struct sembuf *sops, size_t nsops);
功能:完成对指定信号灯的申请和释放资源
参数1:信号灯集id
参数2:要执行的操作等的指针
unsigned short sem_num; /* semaphore number */ 信号灯编号
short sem_op; /* semaphore operation */ 申请还是释放操作,-1表示申请资源, 1表示释放资源
short sem_flg; /* operation flags */ 阻塞还是非阻塞,0表示阻塞,IPC_NOWAIT表示非阻塞

参数3:操作的灯的个数
返回值:成功返回0,失败返回-1并置位错误码
举个例子:对0号灯进行申请资源操作
struct sembuf buf;
buf.sem_num = 0; //表示要操作0号灯
buf.sem_op = -1; //表示申请资源
buf.sem_flg = 0; //表示阻塞申请
semop(semid, &buf, 1);





1> 将信号灯集的相关函数二次封装

sem.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#ifndef __SEM_H__
#define __SEM_H__

//创建信号灯集并初始化,semcount表示灯的个数
int create_sem(int semcount);

//申请资源操作,semno表示灯的编号
int P(int semid, int semno);

//释放资源操作,semno表示灯的编号
int V(int semid, int semno);

//删除信号灯集
int delete_sem(int semid);


#endif

sem.c

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
#include <myhead.h>
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 set_semno_value(int semid, int semno)
{
int val;

printf("请输入第%d号灯的值:", semno);
scanf("%d", &val);

//定义一个共用体变量
union semun su;
su.val = val;
//调用控制函数
if(semctl(semid, semno, SETVAL, su)==-1)
{
perror("semctl error");
return -1;
}

return 0;
}


//创建信号灯集并初始化,semcount表示灯的个数
int create_sem(int semcount)
{
//1、创建key值
key_t key = 0;
if((key = ftok("/", 'k')) == -1)
{
perror("ftok error");
return -1;
}

//2、通过key值创建信号灯集
int semid = 0;
if((semid = semget(key, semcount, IPC_CREAT|IPC_EXCL|0664))==-1)
{
//对错误码进行判断,如果错误码为EEXIST
if(errno == EEXIST)
{
//说明信号灯集已经存在,直接打开即可,无需初始化
semid = semget(key, semcount, IPC_CREAT|0664);
return semid;
}

perror("semget error");
return -1;
}

//3、初始化信号灯集中的灯
for(int i=0; i<semcount; i++)
{
//给semid信号灯集中的i号灯设置值
set_semno_value(semid, i);
}

//4、返回信号灯集id
return semid;
}

//申请资源操作,semno表示灯的编号
int P(int semid, int semno)
{
//定义操作结构体
struct sembuf buf;
buf.sem_num = semno; //要操作的信号灯的编号
buf.sem_op = -1; //表示申请资源操作
buf.sem_flg = 0; //阻塞申请资源

//执行申请资源操作
if(semop(semid, &buf, 1) == -1)
{
perror("semop error");
return -1;
}

return 0;
}

//释放资源操作,semno表示灯的编号
int V(int semid, int semno)
{
//定义操作结构体
struct sembuf buf;
buf.sem_num = semno; //要操作的信号灯的编号
buf.sem_op = 1; //表示申请资源操作
buf.sem_flg = 0; //阻塞申请资源

//执行申请资源操作
if(semop(semid, &buf, 1) == -1)
{
perror("semop error");
return -1;
}

return 0;
}

//删除信号灯集
int delete_sem(int semid)
{
if(semctl(semid, 0, IPC_RMID) == -1)
{
perror("semctl error");
return -1;
}

return 0;

}

shmsnd.c

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
#include<myhead.h>
#define PAGE_SIZE 4096
#include"sem.h"

int main(int argc, const char *argv[])
{

//11、创建并初始化一个信号灯集
int semid = create_sem(2);

//1、创建key值
key_t key = -1;
if((key = ftok("/", 't')) == -1)
{
perror("ftok error");
return -1;
}
printf("key = %#x\n", key);

//2、将物理内存创建出共享内存段
int shmid = 0;
if((shmid = shmget(key, PAGE_SIZE, IPC_CREAT|0664)) == -1)
{
perror("shmget error");
return -1;
}
printf("shmid = %d\n", shmid);

//3、将共享内存段地址映射到用户空间
//NULL表示让系统自动选择页的分段
//0表示当前进程对共享内存具有读写功能
char *addr = (char *)shmat(shmid, NULL, 0);
if(addr == (void *)-1)
{
perror("shmat error");
return -1;
}
printf("addr = %p\n", addr);

//4、操作共享内存
//char buf[128] = "";
while(1)
{

//22、申请0号灯的资源
P(semid, 0);

fgets(addr, PAGE_SIZE, stdin); //从终端输入数据
addr[strlen(addr) - 1] = '\0'; //将换行换成'\0'

//33、释放1号灯的资源
V(semid, 1);

if(strcmp(addr, "quit") == 0)
{
break;
}

}


//5、取消映射

while(1);

return 0;
}

shmrcv.c

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
#include<myhead.h>
#define PAGE_SIZE 4096
#include"sem.h"

int main(int argc, const char *argv[])
{

//11、创建信号灯集
int semid = create_sem(2);


//1、创建key值
key_t key = -1;
if((key = ftok("/", 't')) == -1)
{
perror("ftok error");
return -1;
}
printf("key = %#x\n", key);

//2、将物理内存创建出共享内存段
int shmid = 0;
if((shmid = shmget(key, PAGE_SIZE, IPC_CREAT|0664)) == -1)
{
perror("shmget error");
return -1;
}
printf("shmid = %d\n", shmid);

//3、将共享内存段地址映射到用户空间
//NULL表示让系统自动选择页的分段
//0表示当前进程对共享内存具有读写功能
char *addr = (char *)shmat(shmid, NULL, 0);
if(addr == (void *)-1)
{
perror("shmat error");
return -1;
}
printf("addr = %p\n", addr);

//4、操作共享内存
//char buf[128] = "";
while(1)
{
//22、申请1号灯的资源
P(semid, 1);

printf("共享内存中的数据为:%s\n", addr);


//33、释放0号灯的资源
V(semid, 0);

if(strcmp(addr, "quit") == 0)
{
break;
}

}


//5、取消映射
if(shmdt(addr) == -1)
{
perror("shmdt error");
return -1;
}


//6、删除共享内存
if(shmctl(shmid, IPC_RMID, NULL) == -1)
{
perror("shmctl error");
return -1;
}

//44、删除信号灯集
delete_sem(semid);

return 0;
}