一:线程
2:线程
1: 线程的概念
- LWP: light weight process 轻量级的进程,本质仍是进程(在Linux环境下)
- 线程:有独立的PCB,但没有独立的地址空间(共享
- [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qMx9LxmJ-1583042698972)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20191113092552967.png)]
- 区别:在于是否共享地址空间。 独居(进程);合租(线程)。
- Linux下:
- 线程:最小的执行单位
- 进程:最小分配资源单位,可看成是只有一个线程的进程。
2: linux 内核线程的实现原理
-
// 进程和线程的关系 1. 轻量级进程(light-weight process),也有PCB,创建线程使用的底层函数和进程一样,都是clone 2. 从内核里看进程和线程是一样的,都有各自不同的PCB,但是PCB中指向内存资源的三级页表是相同的 3. 进程可以蜕变成线程 4. 线程可看做寄存器和栈的集合 5. 在linux下,线程最是小的执行单位;进程是最小的分配资源单位
6.线程默认共享数据段、代码段等地址空间,常用的是全局变量。而进程不共享全局变量,只能借助mmap。
+ // 查看 LWP 号, `ps -LF pid` : 指定查看线程的lwp 号
### 2.1: 三级映射
+ `进程PCB --> 页目录(可看成数组,首地址位于PCB中) --> 页表 --> 物理页面 --> 内存单元`
+ 进程: `对于进程来说,相同的地址(同一个虚拟地址)在不同的进程中,反复使用而不冲突。原因是他们虽虚拟址一样,但,页目录、页表、物理页面各不相同。相同的虚拟址,映射到不同的物理页面内存单元,最终访问不同的物理页面。`
+ 线程:`线程不同!两个线程具有各自独立的PCB,但共享同一个页目录,也就共享同一个页表和物理页面。所以两个PCB共享一个地址空间`
### 2.2 : 原理
+ `无论是创建进程的fork,还是创建线程的pthread_create,底层实现都是调用同一个内核函数clone`
+ `Linux内核是不区分进程和线程的。只在用户层面上进行区分`
+ 线程所有操作函数 pthread_* 是库函数,而非系统调用
### 2.3 :线程的优缺点
```c
//优点: 1. 提高程序并发性 2. 开销小 3. 数据通信、共享数据方便
//缺点: 1. 库函数,不稳定 2. 调试、编写困难、gdb不支持 3. 对信号支持不好
3:常用的函数
-
1:
pthread_t pthread_self(void)
-
作用:获取线程的 ID ; 对应;getpid( ) 函数。
-
线程ID类型:pthread_t; 本质:linux 下为 无符号的整数(%lu)
-
线程ID线程内部识别,(两个进程间, 线程(ID, 允许相同)
-
-
-
2:
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void*(*start_routinue)(*void), void *arg)
- 返回值:
- 成功:0;
- 失败:错误号 errno
- Linux环境下,所有线程特点,失败均直接返回错误号
- 作用:
- 创建子线程
- 参数
- 参数1:传出参数,保存系统为我们分配好的线程
- 参数2:通常传NULL,表示使用线程默认属性。若想使用具体属性也可以修改该参数
NULL给attr参数,表示线程属性取缺省值
- 参数3:函数指针,指向线程主函数(线程体),该函数运行结束,则线程结
- 参数4:线程主函数执行期间所使用的参数。
- 注意
- 注意:链接线程库 -lpthread
- pthread_create的错误码不保存在errno中,因此不能直接用perror(3)打印错误信息,可以先用strerror(3)把错误码转换成错误信息再打印
- 返回值:
-
3:
void pthread_eixt(void *retual)
- 作用:
- 将单个线程退出
- 参数
- retval表示线程退出状态,通常传NULL
- 结论:
- 线程中,禁止使用exit函数,会导致进程内所有线程全部退出
- 三种方式的退出对比
- exit: 将进程退出
- return:返回到调用者那里去
- pthread_exit():将调用该函数的线程退出
- 作用:
-
4:
int phthread_join(pthread_t thread, void **retval)
-
作用:
- 阻塞等待线程退出, 获取线程的状态
-
参数
- thread:线程ID (【注意】:不是指针)
- 存储线程结束状态
// retval 的非空用法 /* 调用该函数的线程将挂起等待,直到id为thread的线程终止。 thread线程以不同的方法终止,通过pthread_join得到的终止状态是不同的*/ 1.如果thread线程通过return返回,retval所指向的单元里存放的是thread线程函数的返回值。 2.如果thread线程被别的线程调用pthread_cancel异常终止掉,retval所指向的单元里存放的是常数PTHREAD_CANCELED。 3.如果thread线程是自己调用pthread_exit终止的,retval所指向的单元存放的是传给pthread_exit的参数。 4.如果对thread线程的终止状态不感兴趣,可以传NULL给retval参数。
-
返回值
- 成功:0;失败:错误号
-
-
5:
int pthread_datch(pthread_t thread)
- 返回值
- 成功:0;失败:错误号
- 作用:实现线程分离
- 线程分离状态:指定该状态,线程主动与主控线程断开关系
- 线程结束后,其退出状态不由其他线程获取,而直接自己自动释放
- 网络、多线程服务器常用
- 分离属性设置利用pthread_create( ) 函数进行, 第二个参数的设置分离属性。
- 注意:
- 但是线程也可以被置为detach状态,****这样的线程一旦终止就立刻回收它占用的所有资源,而不保留终止状态****
- 一般情况下,线程终止后,其终止状态一直保留到其它线程调用pthread_join获取它的状态为止
- 不能对一个已经处于detach状态的线程调用pthread_join,这样的调用将返回EINVAL错误
- 返回值
-
6:
int pthread_cannel (pthread_t thread)
函数- 作用:
- 杀死(取消线程)
- 注意
- 线程的取消并不是实时的,而有一定的延时。需要等待线程到达某个取消点(检查点)
- 通常是一些系统调用creat,open,pause,close,read,write… 执行命令man 7 pthreads可以查看具备这些取消点的系统调用列表
- 可粗略认为一个系统调用(进入内核)即为一个取消点。如线程中没有取消点,可以通过调用pthread_testcancel函数自行设置一个取消点。
被取消的线程, 退出值定义在Linux的pthread库中。常数PTHREAD_CANCELED的值是-1。可在头文件pthread.h中找到它的定义:***\*#define PTHREAD_CANCELED ((void \*) -1)\*******\*。\****因此当我们对一个已经被取消的线程使用pthread_join回收时,得到的返回值为-1
- 返回值
- 成功:0;失败:错误号
- 作用:
-
终止线程的三种方式:
- 从主函数中return , 这种方式对主控线程不适用。(从main 函数中 return 相当于调用了 exit)
- 一个线程可以调用 pthread_cannel 终止同一个进程的另一个进程
- 线程可以调用 pthread_exit()终止自己。
// 属性设置 1. 建 线程属性对象 pthread_attr_t attr; 2. 初始化线程属性对象 pthread_attr_init(&attr); 3. 设置线程属性为分离态 pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); 4. 使用新属性创建线程 pthread_create(&tid, &attr, tfn, NULL); 5. 销毁线程属性对象 pthread_attr_destroy(&attr);
3: 守护进程
-
定义 :Daemon(精灵)进程,是Linux中的后台服务进程,通常独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。一般采用以d结尾的名字。
-
如:预读入缓输出机制的实现;ftp服务器;nfs服务器等
-
创建守护进程,最关键的一步是调用setsid函数创建一个新的Session,并成为Session Leader
// 创建守护进程模型
1.创建子进程,父进程退出
所有工作在子进程中进行形式上脱离了控制终端
2.在子进程中创建新会话
setsid()函数
使子进程完全独立出来,脱离控制
3.改变当前目录位置
chdir()函数
防止占用可卸载的文件系统
也可以换成其它路径
4.重设文件权限掩码
umask()函数
防止继承的文件创建屏蔽字拒绝某些权限
增加守护进程灵活性
5.关闭文件描述符
继承的打开文件不会用到,浪费系统资源,无法卸载
6.开始执行守护进程核心工作守护进程退出处理程序模型
4:会话常用函数
// 创建一个会话需要注意以下6点注意事项:
1.调用进程不能是进程组组长,该进程变成新会话首进程(session header)
2.该进程成为一个新进程组的组长进程。
3.需有root权限 (ubuntu不需要)
4.新会话丢弃原有的控制终端,该会话没有控制终端
5.该调用进程是组长进程,则出错返回
6.建立新会话时,先调用fork, 父进程终止,子进程调用setsid()
-
pid_t setsid(void)
- 成功:返回调用进程的会话 ID ; 失败:-1, 设置 errno
-
pid_t getsid(pid_t pid)
- 成功: 返回调用进程的会话 ID; 失败: -1, 设置:errno
- pid 为 0 的时候, 表示查看当前进程 session ID.
5:线程同步
5.1:线程同步的定义和作用
- 当有多个控制流, 访问同一共享资源时, 如果对方访问顺序不加以控制, 就会产生数据混乱(有时间有关错误有关), 保证访问顺序, 添加同步机制。
- 同步即协同步调, 按照指定的顺序依次进行。
- 作用:
- 避免数据混乱, 解决与时间有关的错误。
- 不仅仅线程需要同步, 进程间和信号间也需要同步机制。
- 线程同步的适用环境“多个控制流, 共同操作一个共享资源时”
5.2:数据混乱的原因
-
1:资源共享(独自的资源不会)
-
2:调度随机(意味着数据访问会出现竞争)
-
3:线程之间缺乏同步机制。
-
解决方案:
- 前两条不能改变, 欲提高效率, 传递数据, 资源必须共享, 资源共享, 必会出现竞争,只要存在竞争关系, 就会出现数据混乱。
- 所以从第三条解决问题, 使多个线程在访问共享资源的时候, 出现互斥。
-
5.3:实现线程同步的方案:
1:使用互斥锁
-
互斥量的定义:
-
每个线程对资源操作前都尝试先加锁, 成功之后才能操作, 操作结束解锁。资源依旧共享, 线程之间也还是存在竞争。
-
但是通过“锁”就将资源的访问变成互斥锁, 而后与时间有关的错误就不会发生了。
-
注意:
- 同一时刻, 只能有一个线程持有该锁。
- 即使有了, mutex , 如果有线程不按照规则访问数据, 依然会遭程数据的混乱。
-
主要函数
-
pthread_mutex_t
- 类型,其本质是一个结构体。为简化理解,应用时可忽略其实现细节,简单看成整数对待
- pthread_mutex_t mutex; 变量mutex只有两种取值1、0。
-
pthread_mutex_init( )
-
作用
- :初始化一个互斥锁 -> 初值可以看做是1
-
函数原型:
int pthread_mutex_init(pthread_mutex_t * restrict mutex, const pthread_mutexattr_t *restrict attr)
-
参数:
-
参1:传出参数,调用时应传 &mutex
- restrict关键字:只用于限制指针,告诉编译器,所有修改该指针指向内存中内容的操作,只能通过本指针完成。不能通过除本指针以外的其他变量或指针修改
-
参2:互斥量属性。是一个传入参数,通常传NULL,选用默认属性(线程间共享)
- 初始化的两种方式
// 1. 静态初始化 如果互斥锁 mutex 是静态分配的(定义在全局,或加了static关键字修饰), 可以直接使用宏进行初始化。 pthead_mutex_t muetx = PTHREAD_MUTEX_INITIALIZER; // 2. 动态初始化 2.局部变量应采用动态初始化。 pthread_mutex_init(&mutex, NULL)
-
-
-
pthread_mutex_destroy( )
- 作用:
- 销毁一个互斥锁
- 函数原型
int pthread_mutex_destroy(pthread_mutex_t *mutex)
- 互斥类型都一样, 返回值
- 成功 :0 ;
- 失败:errno; 线程会处于阻塞状态。
- 作用:
-
pthread_mutex_lock( )
- 作用:
- 加锁
加锁失败, 线程会处于阻塞状态,阻塞到持有该互斥量的其他线程解锁为止
- 函数原型
- int pthread_mutex_lock(pthread_mutex_t * mutex)
- 作用:
-
pthread_mutex_unlock( )
- 作用:
- 解锁
主动解锁函数,***\*同时将阻塞在该锁上的所有线程\*******\*全部唤醒\****,至于哪个线程先被唤醒,取决于优先级、调度。默认:先阻塞、先唤醒。
- 函数原型
- int pthread_mutex_unlock(pthread_mutex_t * mutex)
- 作用:
-
pthread_mutex_trylock( )
- 作用
- 尝试加锁
- 尝试加锁失败:
直接返回错误号(如:EBUSY),不阻塞
- 函数原型
- int pthread_mutex_trylock(pthread_mutex_t * mutex)
- 作用
-
int fprintf(FILE *stream, const char *format, ...); // 文件描述符
======================================================================
int sprintf(char *str, const char *format, ...); // 指
针
死锁
-
1:线程试图对同一个互斥量进行加锁两次
-
2:线程 1 拥有 A 锁, 请求获得 B锁; 线程2 拥有 B 锁, 请求获得 A 锁。
结论:在访问时, 进行加锁, 访问结束进行解锁, 锁的”粒度“ 越小越好
2:使用读写锁
-
与互斥量相比较, 读写锁允许更高的并发性, 写独占, 读共享
-
读写锁的状态:读写锁只有一把
- 1:读模式下, 加锁状态(读锁)
- 2:写模式下, 加锁状态(写锁)
-
读写锁的特性
- 1:读写锁是“写模式加锁”时,
- 解锁前, 所有对该锁的加锁的线程都会阻塞。
- 2:读写锁是”读模式加锁“时,
- 如果线程以读的模式对其加锁成功;
- 如果线程以写模式加锁时会阻塞。
- 3:读写锁是“读模式加锁”时, 既有试图以写模式加锁的线程, 也有试图以读模式加锁的进程。
- 读写锁会阻塞随后的读模式锁的请求, 优先满足写模式锁。
- 读锁和写锁并行阻塞时, 写锁的优先级高于写模式。
- 4:补充:
- 读写锁也叫共享-独占锁:
- 当以读写锁以读的模式锁住时, 它是以共享模式锁住的
- 当读写锁以写模式锁住时, 它是以独占的模式锁住的。
- 读写锁也叫共享-独占锁:
- 5:适用场景
- 读写锁非常适用于,数据结构读的次数远大于写的情况。
- 1:读写锁是“写模式加锁”时,
-
主要的函数
- pthread_rwlock_init( )
- 作用:
- 初始化一把锁
- 函数原型
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr)
- 参数 2: 表示读写锁的属性, 通常是默认属性, 传 NULL 即可
- 作用:
- pthread_rwlock_destroy( )
- 作用:
- 销毁一把锁
- 函数原型
int pthread_rwlock_destroy(pthread_rwlock_t* rwlock)
- 作用:
- pthread_rwlock_rdlock( )
- 作用:
- 以读的方式请求读写锁)(读锁)
- 函数原型
int pthread_rwlock_rdlock(pthread_rwlock_t* rwlock)
- 作用:
- pthread_rwlock_wrlock( )
- 作用:
- 以写的方式请求一把锁(写锁)
- 函数原型
int pthread_rwlock_wrlock(pthread_rwlock_t* rwlock)
- 作用:
- pthread_rwlock_tryrdlock( )
- 作用:
- 非阻塞以读的方式进行请求读写锁(非阻塞请求读锁)
- 函数原型
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock)
- 作用:
- pthread_rwlock_trywrlock( )
- 作用:
- 非阻塞以写是方式进行请求写锁(非阻塞请求写锁)
- 函数原型
int pthread_rwlock_trywrlock(pthread_rwlock_t * rwlock)
- 作用:
- pthread_rwlock_unlock( )
- 作用
- 解锁
- 函数原型
int pthread_rwlock(pthread_rwlock_t * rwlock)
- 作用
- pthread_rwlock_init( )
-
读写锁变量函数
- pthread_rwlock_t 类型
- pthread_rwlock_t rwlock;
3:条件变量
-
优势:
- 相比较 mutex 而言, 条件变量可以减少竞争。
- 直接使用 mutex时, 除了生产者和消费者之间要竞争互斥量以外, 消费者之间也要竞争互斥量, 但是如果汇聚(链表中)没有数据, 消费者之间的竞争互斥锁是没有意义。
- 有了条件变量机制, 只有生产者生产之后,才能引起消费者之间的竞争, 提高程序的效率。
- 相比较 mutex 而言, 条件变量可以减少竞争。
-
定义:
- 条件变量本身不是锁, 可以造成现车阻塞。
- 通常与互斥锁配合使用, 给多线程提供一个会合的场所。
-
主要的函数应用
-
pthread_cond_init( )
-
作用:
- 初始化一个条件变量
-
函数原型
int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr *restrict attr)
- 参数 2 :attr 表示变量属性, 通常传入默认值, 传 NULL 即可
-
也可以使用静态初始化的方法, 初始化条件变量
- pthread_cond_t cond = PTHREAD_COND_INITIALIZER; // initializer : 初始化设定值。
-
-
pthread_cond_destroy( )
- 作用:
- 销毁一个条件变量
- 函数原型
int pthread_cond_destroy(pthread_cond_t * cond)
- 作用:
-
pthread_cond_wait( )
- 作用:
- 阻塞等待一个条件变量
- 函数原型
int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t * restrict mutex)
- 参数:
- 参数1 : 阻塞等待的条件变量 cond(参数 1) 满足的
- 参数2: 释放已掌握的互斥锁(解锁互斥量)相当于:pthread_mutex_unlock(&mutex);
- 1, 2 两步为一个原子操作
- 当被唤醒,pthread_cond_wait 函数返回时, 解除阻塞并重新申请获取互斥锁的 pthread_mutex_lock(&mutex);
- 作用:
-
pthread_cond_timedwait( )
-
作用:
- 限时阻塞一个条件变量
-
函数原型
int pthread_cond_timedwait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex, const strcut timespec* restrict abstime)
-
参数
-
参3: 参看man sem_timedwait函数,查看struct timespec结构体。 struct timespec { time_t tv_sec; /* seconds */ 秒 long tv_nsec; /* nanosecondes*/ 纳秒 } 形参abstime:绝对时间。 如:time(NULL)返回的就是绝对时间。而alarm(1)是相对时间,相对当前时间定时1秒钟。 struct timespec t = {1, 0}; pthread_cond_timedwait (&cond, &mutex, &t); 只能定时到 1970年1月1日 00:00:01秒(早已经过去) 正确用法: time_t cur = time(NULL); 获取当前时间。 struct timespec t; 定义timespec 结构体变量t t.tv_sec = cur+1; 定时1秒 pthread_cond_timedwait (&cond, &mutex, &t); 传参 参APUE.11.6线程同步条件变量小节 在讲解setitimer函数时我们还提到另外一种时间类型: struct timeval { time_t tv_sec; /* seconds */ 秒 suseconds_t tv_usec; /* microseconds */ 微秒 };
-
-
-
pthread_cond_signal( )
- 作用:
- 唤醒至少一个阻塞在条件变量上的线程
- 函数原型
int pthread_cond_signal(pthread_cond_t *cond)
- 作用:
-
pthread_cond_broadcast( )
- 作用:
- 唤醒全部阻塞在条件上的线程
- 函数原型
int pthread_cond_broadcast(pthread_cond_t *cond)
- 作用:
- 以上六个函数的返回值都是 :成功返回 0, 失败直接返回错误号。
- pthread_cond_t 类型, 用于定义条件变量
- pthread_cond_t cond;
-
@@@@@主要的应用:生产者消费者条件变量模型@@@@@
#include <stdlib.h>
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
// 链表作为共享数据, 需要被互斥量保护
struct msg
{
struct msg *next;
int num;
};
struct msg * head;
// 静态初始化 一个条件变量, 一个互斥量
pthread_cond_t has_product = PTHREAD_COND_INITIALIZER;
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
void * producer(void *arg)
{
struct msg * mp;
for (;;)
{
mp = (struct msg *)malloc(sizeof(struct msg));
mp->num = rand() % 1000 + 1; // 模拟生产一个产品
pthread_mutex_lock(&lock);
mp->next = head;
head = mp;
pthread_mutex_unlock(&lock);
// 将等待在该条件变量上的一个线程唤醒 pthread_cond_wait( );
pthread_cond_signal(&has_product);
printf("-------Product111------------%d\n", mp->num);
sleep(3);
}
}
void * consumer(void *arg)
{
struct msg *mp;
for(;;)
{
pthread_mutex_lock(&lock);
while (head == NULL)
{
pthread_cond_wait(&has_product, &lock); // 满足条件, 进行条件变量的阻塞线程
}
mp = head;
head = mp->next; // 模拟消费者消费一个产品
pthread_mutex_unlock(&lock);
printf("------ Consumer %lu -----%d\n", pthread_self(), mp->num);
free(mp);
sleep(1);
}
}
int main(int argc, char* argv[])
{
pthread_t pid, cid;
srand(time(NULL));
pthread_create(&pid, NULL, producer, NULL);
pthread_create(&cid, NULL, consumer, NULL);
pthread_join(pid, NULL); // 阻塞等待线程退出
pthread_join(cid, NULL);
return 0;
}
/* 互斥锁是一把锁, 一个线程进行, 加锁时, 另一个线程只能阻塞等待在本把 mutex 上, 直到前一个线程解锁。*/
4:信号量
-
使用的原因
- 1:进化版的互斥锁(1->N)
- 2:由于互斥锁的粒度比较大, 多个线程间对某一个对象的部分数据进行共享时, 使用互斥锁是没有办法实现的, 只能将数据对象锁住,
- 3:虽然使用互斥锁达到了多线程操作共享数据时,保证数据正确性的目的, 确无形中导致线程并发性的下降。
- 4:导致:线程的并行, 变成了串行,(与直接使用单线程无异)
-
优点
-
是一种相对折中的处理方式, 既能保证同步, 数据不混乱, 又能提高线程的并发性
-
主要应用的函数
-
sem_init( )
- 作用
- 初始化一个信号量
- 函数原型
int sem_init(sem_t *sem, int pshared, unsigned int value)
- 函数参数
- 参数1: sem 信号量
- 参数2: pshared 取 0用于线程间; 取非 0 (一般为1)用于进程间
- 参数3: value 指定信号量初值
- 作用
-
sem_destroy( )
- 作用
- 销毁一个信号量
- 函数原型
int sem_destroy(sem_t * sem)
- 作用
-
sem_wait( )
- 作用:
- 给信号加锁 - -
- 函数原型
int sem_wait(sem_t * sem)
- 作用:
-
sem_trywait( )
- 作用
- 尝试对信号量加锁。
- 函数原型
int sem_trywait(sem_t *sem)
- 作用
-
sem_tiemdwait( )
-
作用
- 限时尝试对信号量加锁
-
函数原型
-
int sem_timedwait(sem_t sem, const struct timespec *abs_timeout)
-
abs_timeout 采用的是绝对时间
-
定时1秒: time_t cur = time(NULL); 获取当前时间。 struct timespec t; 定义timespec 结构体变量t t.tv_sec = cur+1; 定时1秒 t.tv_nsec = t.tv_sec +100; sem_timedwait(&sem, &t); 传参
-
-
-
sem_post( )
- 作用:
- 给信号量解锁 + +
- 函数原型
int sem_post(sem_t * sem)
- 作用:
-
-
以上的函数的返回值: 成功: 0; 失败: -1, 和 errno
- sem_t 类型 本质仍是结构体 :应用期间可简单看做整数(类型与文件描述符)
- sem_t sem, 规定 信号量 sem 不能 < 0; == 头文件 <semaphore.h>==
- 信号量的初值, 决定了占用信号量的线程的个数。
@@@@@@主要的应用: 生产者消费者信号量模型@@@@@@
// 环形消费者和生产者模式
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <semaphore.h>
#include <pthread.h>
#define NUM 2
int queue[NUM];
sem_t blank_number, product_number;
void * product(void *arg)
{
int i = 0;
while (1)
{
// sem_wait( ) 将信号量 blank_number 进行 —— ;信号量为 0 时, 进行阻塞等待
sem_wait(&blank_number);
queue[i] = rand() % 1000 + 1;
printf("--------Product----%d\n", queue[i]);
// sem_post( ) 将信号量进行 product_number ++
sem_post(&product_number);
i = (i + 1) % NUM;
sleep(1);
}
}
void * consumer(void *arg)
{
int i = 0;
while (1)
{
sem_wait(&product_number);
queue[i] = rand() % 1000 + 1;
printf("--------Consumer------%d\n", queue[i]);
sem_post(&blank_number);
i = (i + 1) % NUM;
sleep(1);
}
}
int main(int argc, char* argv[])
{
pthread_t pid, cid;
srand(time(NULL)); // 创建一个时间种子
// 1:初始化信号量
sem_init(&blank_number, 0, NUM); // blank_number 信号量 为5;
sem_init(&product_number, 0, 0); // product_number信号量 0
// 2:创建线程
pthread_create(&pid, NULL, product, NULL);
pthread_create(&cid, NULL, consumer, NULL);
// 3:等待阻塞线程结束, 第二个参数为状态
pthread_join(pid, NULL);
pthread_join(cid, NULL);
/* 5: 实现线程分离,
pthread_detach(&pid);
pthread_detach(&cid);
***\*这样的线程一旦终止就立刻回收它占用的所有资源,而不保留终止状态\****
// 而pthread_join(pid, NULL), 是保留了 线程终止后的状态,
所以, pthread_detach() 和 pthread_join() 函数不能同时使用。
*/
// 4:销毁信号变量
sem_destroy(&blank_number);
sem_destroy(&product_number);
return 0;
}
二:进程
$ ps ajx :表示查看系统中的进程
-
参数 a: 表示不仅列当前的进程,也列出所有的的其他用户的进程
-
参数 x:表示不仅列有控制终端的进程,也列出所有无控制终端的进程
-
参数 j:表示列出与作业控制相关的信息
-
1:进程基础知识
-
定义:
- 进程:运行起来的程序, 活的。 占用系统资源(内存,cpu)。
- 程序:死的, 只占用磁盘空间。 不消耗系统资源。—> 一个程序中可以有很多进程。<至少一个>
-
并发和并行
- 并发:利用 cpu 高速的运算能力, 快速在多个进程间中切换, 达到同时执行多个程序的目的。(宏观并行, 微观串行)(cpu 的分时复用)
- 并行:利用硬件设备(cpu), 实现同时执行多个程序的目的。
-
CPU
- [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CuwC2QCU-1583042698975)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20191114212215471.png)]
- MMU
- [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-B3CHIXpo-1583042698977)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20191114212311681.png)]
-
进程控制块 PCB
-
本质结构, 位于内核空间。
-
作用:用来描述进程的信息。
-
进程控制块的主要内容:
-
1. 进程 id 2. 进程切换时保存 cpu 寄存器数据。 3. 进程状态: 初始、就绪、运行、挂起、终止 4. 虚拟进程地址空间相关信息。 5. 当前进程的工作目录 6. umask掩码 7. 文件描述符表 8. 与信号相关的信息。
-
-
进程状态
-
1. 初始态:进程初始化 2. 就绪态:等待cpu时间片 3. 运行态:独占cpu,运算。 4. 挂起态:等待除CPU以外的其他资源。主动放弃cpu使用权。 5. 终止态:进程运行结束。
-
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lEauTTUk-1583042698979)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20191114212719335.png)]
-
-
-
环境变量
- 定义
- 是指在操作系统中用来指定操作系统运行环境的一些参数
- 特征
- 1:字符串(本质)
- 2:有统一的格式 :名= 值【:值】
- 3:值用来描述进程环境的信息
- 存储形式
- 与命令行参数相似, char *[]数组,数组名environ,内部存储字符串,NULL作为哨兵结尾。
- 使用形式:
- 与命令行参数类似
- 加载位置
- 与命令行参数类似, 位于用户区, 高于 stack 的起始位置。
- 引入环境变量时, 须声明环境变量
- 定义
-
常用环境变量
-
环境变量的字符串都是 name = value 的形式。
- 格式:name 由大写字母加下划线组成
- name 的部分叫做环境变量, value 的部分叫做环境变量的值
- 环境变量定义了进程运行时的环境,
-
一些重要的环境变量
- PATH
- 可执行文件的搜索路径:如:ls : 将可执行文件一般放在 bin目录下。
- SHELL
- 当前 shell, 它的值:/bin/bash
- TERM
- 当前终端类型,在图形界面终端下它的值通常是xterm,终端类型决定了一些程序的输出显示方式,比如图形界面终端可以显示汉字,而字符终端一般不行。
- LANG
- 语言和locale,决定了字符编码以及时间、货币等信息的显示格式
- HOME
- 当前用户主目录的路径,很多程序需要在主目录下保存配置文件,使得每个用户在运行该程序时都有自己的一套配置。
- PATH
-
环境变量常用的函数
1:getenv函数 作用:获取环境变量值 函数原型:char *getenv(const char *name); 返回值:成功:返回环境变量的值;失败:NULL (name不存在) 2:setenv函数 作用:设置环境变量的值 函数原型:int setenv(const char *name, const char *value, int overwrite); 返回值:成功:0;失败:-1 参数:overwrite 取值: 1:覆盖原环境变量 0:不覆盖。(该参数常用于设置新环境变量,如:ABC = haha-day-night) 3:unsetenv函数 作用:删除环境变量name的定义 函数原型:int unsetenv(const char *name); 返回值:成功:0;失败:-1 注意事项;name不存在仍返回0(成功),当name命名为"ABC="时则会出错。
-
2:进程控制的函数
-
fork()
-
作用
- 创建一个子进程
-
函数原型
- pid_t fork(void )
-
返回值:
- 失败返回-1,
- 成功返回
- 父进程返回子进程的ID(非负)
- 子进程返回零
-
pid_t : 类型
- 表示进程的ID,
- 表示 -1 , 它是由符号位整形
- 注意:
- 不是fork函数能返回两个值,而是fork后,fork函数变为两个,父子需【各自】返回一个<进程控制块的操作>
- 表示进程的ID,
-
getpid( )
- 作用;
- 获取当前进程的ID
- 函数原型
- pid_t getpid(void);
- 作用;
-
getppid( )
- 作用:
- 获取当前进程的父进程
- 函数原型
- pid_t getppid(void);
- 作用:
-
getuid( )
- 获取当前进进程使用用户 ID
- uid_t getuid(void);
-
getgid( );
- 获取当前进的使用的用户组ID
- gid_t getgid(void);
-
getegid( )
- 获取当前进程的有效用户组ID
- gid_t getegid(void);
-
-
系统函数和库函数的区分
- 是否访问内核的数据结构
- 是否访问外部的硬件资源
- 两者有其一:系统函数
- 二者均无:库函数
-
@@@@@@@
// 循环创建子进程 int main(int argc, char *argv[]) { int i; for (i = 0; i < 5; i++) { if (fork() == 0) break; } if (5 == i) { sleep(5); printf("I'm parent. pid = %d\n", getpid()); } else { sleep(i); printf("--%d th--child-- pid = %d\n", i+1, getpid()); } return 0; }
3:查看进程信息的命令
- ps aux | grep 关键词
- ps ajx 查看进程 pid 和 父进程 pid (进程组id, 会话id)
- kill -9 进程id ; 杀死指定的进程
+- 进程终止, 0-4G进程地址空间消失, 依然在内核中残留pcb (保留进程终止的相关信息)
4:父子共享进进程
-
父子进程相同
1. 全局变量 2. .data 数据段 3. .text 代码段 4. heap 堆区 5. stack 栈区 6. 用户 ID 7. 宿主目录 8. 进程工作目录 9. 信号的处理方式
-
父子进程不同
1. 进程 ID 2. fork() 返回值 3. 进程运行时间 4. 各自父进程 5. errno 6. 闹钟(定时器) 7. 未决信号集
-
父子进程的原则
- 读时共享, 写时独占。—全局变量
- 这样设计,无论子进程执行父进程的逻辑还是执行自己的逻辑都能节省内存开销
- 父子进程共享
- 文件描述符
- mmap 共享内存的映射区。
- fork 之后父进程先执行还是子进程先执行不确定。
- 取决于内核使用的调度算法。
- 读时共享, 写时独占。—全局变量
5: exec 函数族
-
execlp( )
- 作用:
- 借助 PATH 环境变量, 调用系统的可执行文件
- 函数原型
int execlp(const char *file, const char *arg, ...(char *) NULL);
- 参数
- 参数1:可执行文件(路径使用环境变量 PATH)
- 参数2:可执行文件对应的参数(argv[0], 开始)
- 返回值
- 成功;不返回
- 失败:返回 -1
- 作用:
-
execl()
- 作用:
- 传递路径, 启动可执行文件
- 函数原型
int execl(const char*path, const char* arg, .../(char*) NULL);
- 参数
- 参数1:可执行文件名(带路径(相对, 绝对));
- 参数2:可执行程序 对应的参数(argv[0], 开始)
- 返回值
- 成功;不返回
- 失败:返回 -1
- 作用:
6:两种常见的进程类型
- 孤儿进程
- 父进程终止先于子进程, 子进程变成孤儿进程。
- 孤儿进程有 init 进程收养。
- 子进程结束由 init 进程回收。
- 僵尸进程
- 父进程未终止, 子进程终止。
- 父进程不进行回收, 此时子进程处于僵尸状态.
- // 用wait() 或者 waitpid( )进行回收之后, 就不会出现僵尸进程
- 注意:僵尸进程不能是用 kill 进行回收。
7: wait 函数
-
wait( ) 父进程进行调用
-
解决问题
-
一个进程关闭文件描述符的时候, 释放用户空间的分配内存, 但是它的pcb还保留着, 内核在其中保存了一些信息。
-
正常终止,则保留退出状态;异常终止:保存着导致进程终止的信号是什么?
-
一个进程终止时,父进程可以调用 wait() 或者 waitpid( ) 获取信息, 彻底清除这个进程。
-
-
作用:
- 阻塞等待子进程死亡
- 回收子进程残留pcb
- 获取子进程退出状态(正常,异常)
-
函数原型
pid_t wait(int *wstatus);
- 参数:传出参数:用来记录子进程的状态,
- 传NULL, 只回收, 不获取子进程的状态
-
强调
- 一次 wait( ) 函数调用, 只能回收一个子进程
- 多个子进程的回收, 手动添加 while 机制
-
获取子进程的退出状态
- 正常终止
- 判断 WIFEXITED(status)
- 上宏为真。调用 WEXITSTATUS(status) 宏,获取 子进程的退出。
- 异常终止
- 判断 WIFSIGNALED(status)
- 上宏为真。调用 WTERMSIG(status) 获取,杀死子进程的信号编号
- 正常终止
int main(void) { pid_t pid, wpid; int status; pid = fork(); if (pid == -1 ) { perror("fork"); exit(1); } else if (pid > 0) { wpid = wait(&status); if(wpid == -1) { perror("wait err"); exit(1); } if (WIFEXITED(status)) { // 为真,说明子进程正常终止 printf("子进程退出值为:%d\n",WEXITSTATUS(status)); } else if (WIFSIGNALED(status)) { // 为真,说明子进程由信号杀死 printf("杀死子进程的信号为:%d\n", WTERMSIG(status)); } } else if (pid == 0) { printf("child pid = %d, parentID=%d\n", getpid(), getppid()); sleep(15); return 75; } return 0; }
-
-
waitpid( )
-
作用
- 指定进程回收
-
函数原型
pid_t waitpid(pid_t pid, int *wstatus, int options)
-
参数
-
参数1:指定子进程回收
-
> 0 : 指定回收子进程的 pid
-
-1 : 任意子进程
-
-
参数2:
- 等价于:wait 参数
-
参数3:
- WNOHANG – 不挂起(不阻塞) // 子进程可能还在运行
- 传 0 , 挂起(阻塞); // 等待子进程死亡
-
-
返回值
- > 0 : 成功回收进程 pid
- 0 : 参数3 指定WNOHANG ,并且没有可回收的子进程
- -1 ; 回收失败(没有子进程)
-
// 非阻塞 --- 回收所有子进程
// 非阻塞回收就是, 不等待子进程的结束, 执行后面的程序。
int main(int argc, char *argv[])
{
int i;
pid_t pid, tmpid, wpid;
for (i = 0; i < 5; i++) {
pid = fork();
if (i == 3) {
tmpid = pid; // 保存待回收的进程pid
}
if (pid == 0)
break;
}
if (5 == i) {
//while ((wpid = waitpid(-1, NULL, WNOHANG)) != -1) { // waitpid(-1, NULL, 0) == wait(NULL)
while ((wpid = waitpid(0, NULL, WNOHANG)) != -1) { // waitpid(-1, NULL, 0) == wait(NULL)
if (wpid > 0) {
printf("wpid = %d\n", wpid);
} else if (wpid == 0) {
printf("子进程正在运行\n");
sleep(1);
}
}
printf("所有子进程回收完毕\n");
} else {
sleep(i);
printf("--%d th--child-- pid = %d\n", i+1, getpid());
}
return 0;
}
=======@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@=
// 阻塞回收所有子进程
// 子进程结束一个回收一个, 没有就和wait()一样处于等待的状态。
int main(int argc, char *argv[])
{
int i;
pid_t pid, tmpid, wpid;
for (i = 0; i < 5; i++) {
pid = fork();
if (i == 3) {
tmpid = pid; // 保存待回收的进程pid
}
if (pid == 0)
break;
}
if (5 == i) {
while ((wpid = waitpid(-1, NULL, 0)) != -1) { // waitpid(-1, NULL, 0) == wait(NULL)
printf("wpid = %d\n", wpid);
}
printf("所有子进程回收完毕\n");
} else {
// sleep(i);
printf("--%d th--child-- pid = %d\n", i+1, getpid());
}
return 0;
}
==非阻塞和阻塞回收的返回值==:
如果回收成功是:回收子进程的地址;
失败:-1,
非阻塞:返回值== 0;表示处于没有可以会受到子进程。
8:进程同步
1: IPC(inter process communication):
- 进程间通信实现的原理
- 利用各个进程 共享内核空间的特性, 利用内核空间的缓存区完成数据的传递
- 一个进程数据从用户空间拷贝到内存缓冲区,另一个进程从内核缓冲区把数据读走,
- [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RglCTH72-1583042698982)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20191115141038046.png)]
2:进程间通信的方式
1: 管道
-
使用:于有血缘关系的进程之间, 完成数据的传递。
-
实现方式:利用系统函数:pipe函数创建一个管道
- 有两个文件描述符的引用, 一个读端(fd[0]),一个写端(fd[1])
- 规定:数据从管道的写端流入管道, 从读端流出管道
- 实现原理:管道实为内核使用环形队列机制, 借助内核缓冲区(4k) :内核缓冲区大小。
-
优点:
- 使用简单, 相比套接字, 信号
-
缺点
- 数据不能进程自己写,自己读。
- 管道中数据不可反复读取。一旦读走,管道中不再存在。
- 采用半双工通信方式,数据只能在单方向上流动
- 常见的通信方式:单工通道(遥控器), 半双工通道(短息, 不等同时), 全双工通道的(电话, 同时通信)。
- 双向通信需要建立两个管道。
- 只能在有公共祖先的进程间使用管道
- 可以采用有名管道 fifo 实现, 非血缘关系的信息传递。
-
管道缓冲区大小
- 查看当前系统中创建管道文件所对应的内核缓冲区大小。
ulimilt -a
- 查看当前系统中创建管道文件所对应的内核缓冲区大小。
1.1 :pipe()
- 作用:创建管道
- 函数原型
int pipe(int pipefd[2])
- 返回值
- 成功 0 : 函数调用成功返回r/w两个文件描述符
- 失败 -1 并设置:errno
- 特点
- 无需open,但需手动close
- 规定:fd[0] → r, 对应标准输入 和 1 一样
- 向管道文件读写数据其实是在读写内核缓冲区
- 父子进程:
- 父进程调用pipe函数创建管道,得到两个文件描述符fd[0]、fd[1]指向管道的读端和写端
- 父进程调用fork创建子进程,那么子进程也有两个文件描述符指向同一管道
- 父进程关闭管道读端,子进程关闭管道写端。父进程可以向管道中写入数据,子进程将管道中的数据读出。由于管道是利用环形队列实现的,数据从写端流入管道,从读端流出,这样就实现了进程间通信
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VEGpefKt-1583042698986)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20191116152409708.png)]
//管道的几种情况:
// 使用管道需要注意以下4种特殊情况(假设都是阻塞I/O操作,没有设置O_NONBLOCK标志)
1. 如果所有指向管道写端的文件描述符都关闭了(管道写端引用计数为0),而仍然有进程从管道的读端读数据,那么管道中剩余的数据都被读取后,再次read会返回0,就像读到文件末尾一样。
2. 如果有指向管道写端的文件描述符没关闭(管道写端引用计数大于0),而持有管道写端的进程也没有向管道中写数据,这时有进程从管道读端读数据,那么管道中剩余的数据都被读取后,再次read会阻塞,直到管道中有数据可读了才读取数据并返回。
3. 如果所有指向管道读端的文件描述符都关闭了(管道读端引用计数为0),这时有进程向管道的写端write,那么该进程会收到信号SIGPIPE,通常会导致进程异常终止。当然也可以对SIGPIPE信号实施捕捉,不终止进程。具体方法信号章节详细介绍。
4. 如果有指向管道读端的文件描述符没关闭(管道读端引用计数大于0),而持有管道读端的进程也没有从管道中读数据,这时有进程向管道写端写数据,那么在管道被写满时再次write会阻塞,直到管道中有空位置了才写入数据并返回。
//总结:
/*① 读管道: 1. 管道中有数据,read返回实际读到的字节数。
2. 管道中无数据:
(1) 管道写端被全部关闭,read返回0 (好像读到文件结尾)
(2) 写端没有全部被关闭,read阻塞等待(不久的将来可能有数据递达,此时会让出cpu)
② 写管道: 1. 管道读端全部被关闭, 进程异常终止(也可使用捕捉SIGPIPE信号,使进程不终止)
2. 管道读端没有全部关闭:
(1) 管道已满,write阻塞。
(2) 管道未满,write将数据写入,并返回实际写入的字节数。*/
1.2 : fifo 有名管道
-
特性;
- 可以实现多个读端多个写端
- 无血缘关系之间的进程也能实现通信
-
是linux 基础文件类型的一种,
- 实际 fifo文件在磁盘中没有数据块,仅仅用来标识内核中的一条通道,
-
各个进程可以打开这个文件进行 read/write,
- 实际是在读写内核通道, 这样就是实现了进程间通信。
// 创建方式
1: 命名:mkfifo 管道名
2:库函数
int mkfifo(const char *pathnanme, mode_t mode);
返回值:成功:0 失败 -1;
// 使用mkfifo创建了一个FIFO,就可以使用open打开它,常见的文件I/O函数都可用于fifo。
如:close、read、write、unlink等。
- 有名管道以 only_read 的方式打开, 只有读端时, 会存在阻塞, 直到有写端打开时, 才解除阻塞。
- 如:cat ----打开文件时, 是以只读打开文件, 进行了阻塞。
- 有名管道写端有内容, 关闭管道, 进程退出,
程序
#include <stdio.h>
#include <sys/wait.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <pthread.h>
void sys_err(const char *str)
{
perror(str);
exit(1);
}
int main(int argc, char *argv[])
{
int n, fd[2];
pid_t pid;
char *str = "hello pipe\n";
char buf[4096] = {0};
// 先 创建 管道
int ret = pipe(fd);
if (ret == -1)
sys_err("pipe err");
// 再 fork 子进程.
pid = fork();
if (pid == -1)
sys_err("fork err");
else if (pid == 0) { // 子进程
close(fd[1]); // 关闭写端
n = read(fd[0], buf, 4096);
write(STDOUT_FILENO, buf, n);
close(fd[0]);
} else if (pid > 0) { // 父进程 -- 写
close(fd[0]); // 关闭读端
write(fd[1], str, strlen(str));
wait(NULL);
close(fd[1]);
}
return 0;
}
=======================================================================================
@@@@ 2: 父子进程间通信 //子进程通信,实现 ls | wc -l
#include <stdio.h>
#include <sys/wait.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <pthread.h>
void sys_err(const char *str)
{
perror(str);
exit(1);
}
int main(int argc, char *argv[])
{
int fd[2];
pid_t pid;
// 先 创建 管道
int ret = pipe(fd);
if (ret == -1)
sys_err("pipe err");
// 再 fork 子进程.
pid = fork();
if (pid == -1)
sys_err("fork err");
else if (pid == 0) { // 子进程 -- wc -l
close(fd[1]);
dup2(fd[0], STDIN_FILENO);
execlp("wc", "wc", "-l", NULL);
sys_err("exec wc err");
} else if (pid > 0) { // 父进程 -- ls
close(fd[0]);
dup2(fd[1], STDOUT_FILENO);
execlp("ls", "ls", NULL);
sys_err("exec ls err");
}
return 0;
}
=====================================================================================
3: 兄弟进程间通信://兄弟进程间通信 ls | wc -l
#include <stdio.h>
#include <sys/wait.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <pthread.h>
void sys_err(const char *str)
{
perror(str);
exit(1);
}
int main(int argc, char *argv[])
{
int fd[2], i;
pid_t pid;
// 先 创建 管道
int ret = pipe(fd);
if (ret == -1)
sys_err("pipe err");
// 再 fork 子进程. 兄 -- 0, 弟 -- 1
for (i = 0; i < 2; i++) { // 表达式2,父进程专用出口
pid = fork();
if (pid == -1)
sys_err("fork err");
if (pid == 0) // 子进程的 出口
break;
}
// 借助 循环因子 i,判断父进程/兄/弟
if (2 == i) { // 父
close(fd[0]);
close(fd[1]);
wait(NULL);
wait(NULL);
} else if (i == 0){ // 兄 -- ls
close(fd[0]);
dup2(fd[1], STDOUT_FILENO);
execlp("ls", "ls", NULL);
sys_err("execlp ls err");
} else if (i == 1){ // 弟 -- wc -l
close(fd[1]);
dup2(fd[0], STDIN_FILENO);
execlp("wc", "wc", "-l", NULL);
sys_err("execlp wc err");
}
return 0;
}
2:信号
3:共享内存映射(无血缘关系)
- 优点:
- 使用映射区完成通信简单, 父子进程之间通信
- 支持多个读端和多个写端
- 共享内存中的数据可以反复读取。
- 劣势:
- 要open 一个 temp 文件, 创建好了,再unlink, close 比较麻烦
- 可以直接使用匿名映射来代替<只能用于有血缘关系的进程>
- 要open 一个 temp 文件, 创建好了,再unlink, close 比较麻烦
3.1 :存储映射的实现:
-
存储映射I/O (Memory-mapped I/O) 使一个磁盘文件与存储空间中的一个缓冲区相映射
-
于是当从缓冲区中取数据,就相当于读文件中的相应字节。于此类似,将数据存入缓冲区,则相应的字节就自动写入文件
- 在不适用read和write函数的情况下,使用地址(指针)完成I/O操作
-
首先应通知内核,将一个指定文件映射到存储区域中 -> 通过 mmap() 函数实现
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-k9D8soh4-1583042698995)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20191116155459995.png)]
// 1:mmap 函数
// 指定文件映射到存储区域中
void *mmap(void *adrr, size_t length, int port, int flags, int fd, off_t offset);
返回值:
成功:返回创建的映射区首地址;
失败:MAP_FAILED宏
参数:
addr: 建立映射区的首地址,由 Linux内核指定, 使用时直接传NULL
length: 创建映射区的大小 @@ 任意
port: 映射区的权限:
+PROT_READ,
+PROT_WRITE,
+PROT_READ|PROT_WRITE
flags: 标志位参数(设定更新物理区域, 设置共享, 创建匿名映射区)
+ MAP_SHARED : 会将映射区所做的操作反映射到物理磁盘上
+ MAP_PRIVATE: 映射区所做的修改不会反映射到物理磁盘上。
fd: 用来建立映射区的文件描述符
offset: 映射文件的偏移(4k 的整数倍)
=============================================================
// 2: munmap() 函数
munmap 函数 释放内存空间
同malloc函数申请内存空间类似的,mmap建立的映射区在使用结束后也应调用类似free的函数来释放。
int munmap(void *addr, size_t length); 成功:0; 失败:-1
============================================================
// 3:mmap 使用的注意事项
1. 用来创建映射区的文件大小,不能是 0 字节。 报错。Invalid argument
2. 传入 mmap 的 len <= 文件实际大小。否则,报错。 总线错误 (核心已转储)
3. mmap 建立映射区,默认需求 读权限。
4. mmap 建立映射区的指定的读写权限,应该 <= 文件的读写权限
5. mmap 映射区建立成功,fd 文件就可以关闭。写映射、读映射区,用内存地址。不用fd。
6. offset 参数,必须传 4096(mmu映射的单位) 的整数倍。
7. 对返回的地址越界访问, 有可能出正确结果,但是,数据无效。不推荐。
8. 对返回的地址修改,不能正常使用 munmap 释放映射区。( malloc 、free特性一致。)
9. flags参数指定 MAP_SHARED 选项,在映射区所做的修改会反应到 物理磁盘文件上。
10. flags参数指定 MAP_PRIVATE 选项,不会反应到 物理磁盘文件上。
11. flags参数指定 MAP_PRIVATE 选项, Open 文件时的权限, 和 映射区权限,无影响关系。
============================================================
// 4:创建共享内存,修改共享内存,反应到物理磁盘文件
#include <stdio.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <pthread.h>
void sys_err(const char *str)
{
perror(str);
exit(1);
}
int main(int argc, char *argv[])
{
char *p = NULL;
int ret, len;
int fd = open(argv[1], O_RDWR|O_CREAT|O_TRUNC, 0644);
if (fd == -1)
sys_err("open err");
// 拓展文件大小
ret = ftruncate(fd, 20); // lseek(fd, 19, SEEK_END); write(fd, "\0", 1);
if (ret == -1)
sys_err("ftruncate err");
// 获取文件大小
len = lseek(fd, 0, SEEK_END);
if (ret == -1)
sys_err("lseek err");
// 创建映射区
p = mmap(NULL, len, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
if (p == MAP_FAILED) {
sys_err("mmap err");
}
// 写 -- mmap映射区 -- 文件
strcpy(p, "hello\n");
// 读 -- mmap映射区 -- 文件
printf("%s\n", p);
// 释放映射区
ret = munmap(p, len);
if (ret == -1)
sys_err("munmap err");
close(fd);
return 0;
}
3.2: mmap( ) 函数实现父子间通信
-
父子等有血缘关系的进程之间也可以通过mmap建立的映射区来完成数据通信。但相应的要在创建映射区的时候指定对应的标志位参数flags:
-
MAP_PRIVATE: (私有映射) 父子进程各自独占映射区;
-
MAP_SHARED: (共享映射) 父子进程共享映射区;
-
-
结论:****父子进程共享:1. 打开的文件 2. mmap建立的映射区(但必须要使用MAP_SHARED)****
// mmap 也可以实现非血缘关系之间的进程通信
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/wait.h>
int var = 100;
int main(void)
{
int *p;
pid_t pid;
int fd;
fd = open("temp", O_RDWR|O_CREAT|O_TRUNC, 0644);
if(fd < 0){
perror("open error");
exit(1);
}
unlink("temp"); //删除临时文件目录项,使之具备被释放条件.
ftruncate(fd, 4);
p = (int *)mmap(NULL, 4, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
//p = (int *)mmap(NULL, 4, PROT_READ|PROT_WRITE, MAP_PRIVATE, fd, 0);
if(p == MAP_FAILED){ //注意:不是p == NULL
perror("mmap error");
exit(1);
}
close(fd); //映射区建立完毕,即可关闭文件
pid = fork(); //创建子进程
if(pid == 0){
*p = 2000; // xie 共享内存
var = 1000; // 写时复制
printf("child, *p = %d, var = %d\n", *p, var);
} else {
sleep(1);
printf("parent, *p = %d, var = %d\n", *p, var);// var 读时共享 // *p du 共享内存
wait(NULL);
int ret = munmap(p, 4); //释放映射区
if (ret == -1) {
perror("munmap error");
exit(1);
}
}
return 0;
}
3.3:匿名映射区
- 只能用于有血缘关系的进程
// 1:作用:
解决临时文件的建立, 可以直接使用 mmap 来实现
其实Linux系统给我们提供了创建匿名映射区的方法,无需依赖一个文件即可创建映射区。
// 同样需要借助标志位参数flags来指定。
MAP_ANONYMOUS (或MAP_ANON)
如:int *p = mmap(NULL, 4, PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANONYMOUS, -1, 0);
=================================================
2: linux 和 unix 的对比
//MAP_ANONYMOUS和MAP_ANON这两个宏是Linux操作系统特有的宏
类Unix系统中如无该宏定义,可使用如下两步来完成匿名映射区的建立。
① fd = open("/dev/zero", O_RDWR);
② p = mmap(NULL, size, PROT_READ|PROT_WRITE, MMAP_SHARED, fd, 0);
=================================================
// 3: 补充:
- /dev/zero : 位桶。—— 从该文件中读数据,想读多少有多少。全部是 文件空洞 “\0”
- /dev/null : 黑洞。—— 向该文件中,写数据,想写多少,写多少。写入的数据全部消失。
举例:
// 利用 /dev/zero 文件实现非血缘关系进程间通信。
int fd = open("/dev/zero", O_RDWR);
p = mmap(NULL, 任意, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
if(p == MAP_FAILED){ //注意:不是p == NULL
perror("mmap error");
exit(1);
}
close(fd);
程序;
// 非血缘关系进程间mmap通信
写端
struct student {
int id;
char name[256];
int age;
};
void sys_err(const char *str)
{
perror(str);
exit(1);
}
int main(int argc, char *argv[])
{
struct student stu = {1, "lisi", 10};
struct student *p = NULL;
int fd = open(argv[1], O_RDWR|O_CREAT|O_TRUNC, 0644);
if (fd == -1)
sys_err("open err");
ftruncate(fd, sizeof(stu));
p = mmap(NULL, sizeof(stu), PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
if (p == MAP_FAILED) {
sys_err("mmap err");
}
while (1) {
memcpy(p, &stu, sizeof(stu));
stu.id++;
usleep(900000);
}
munmap(p, sizeof(stu));
close(fd);
return 0;
}
读端
struct student {
int id;
char name[256];
int age;
};
void sys_err(const char *str)
{
perror(str);
exit(1);
}
int main(int argc, char *argv[])
{
struct student stu;
struct student *p = NULL;
int fd = open(argv[1], O_RDONLY);
if (fd == -1)
sys_err("open err");
p = mmap(NULL, sizeof(stu), PROT_READ, MAP_SHARED, fd, 0);
if (p == MAP_FAILED) {
sys_err("mmap err");
}
while (1) {
printf("id = %d, name = %s, age = %d\n", p->id, p->name, p->age);
usleep(300000);
}
munmap(p, sizeof(stu));
close(fd);
return 0;
}
4:套接字(最稳定)
3:四种通信方式对比
1. 管道:优点:实现简单。缺点:数据必须单向流动。数据不能反复读取。
2. 信号:优点:开销小。缺点:不能携带大量数据。
3. 共享内存映射:优点:通信效率高,应用于无血缘关系进程间。 特点:数据可以反复读取。
4. 本地套接字:优点:稳定性高。缺点:实现的复杂度高。