嵌入式课堂整理8
嵌入式Linux多任务:进程、线程
(1)硬件条件:单个CPU单个核
(2)单任务:一个任务执行完毕之后下个任务才能执行;
(3)多任务:任务的执行可以被中断,中断之后可以执行其他任务; (并发/并行)
(4)单核CPU:并发
(5)多核的CPU:并发,并行
一、进程实现多任务
1.特点:给每个进程分配独立的地址空间, 4G的大小!(1G内核, 3G用户空间:栈、堆、数据段、代码段) ;互不干扰;
2.进程创建方式: fork>exec函数族>system>vfork;
3.进程的退出: exit ()库函数:清理缓冲
_exit()系统调用API:不清理缓冲
4.进程等待: wait () ; 解决:僵尸进程
<僵尸进程、孤儿进程、守护进程、控制台进程、后台进程>
5.学习目标:学会创建多任务程序:进程;
二、进程间通信-IPC
1.原理:尽管进程空间是各自独立的,相互之间没有任何可以共享的空间,但是至少还有一样东西是所有进程所共享的,那就是OS,因为不管运行有多少个进程,但它们共用OS只有一个。
既然大家共用的是同一个OS,那么显然,所有的进程可以通过大家都共享第三方0S来实现数据的转发。
2.因此进程间通信的原理就是, OS作为所有进程共享的第三方,会提供相关的机制,以实现进程间数据的转发,达到数据共享的目的。
广义上的进程间通信: A进程-文件-B进程或A进程-数据库-B进程
3.通过普通文件通信结果:通信成功
三、侠义上的真正的“进程间通信" (内核提供) :
1、管道
①无名管道
特点:管道只允许具有血缘关系的进程间通信,如父子进程间的通信;
管道只允许单向通信;
读管道时,如果没有数据的话,读操作会休眠(阻塞) ,写数据时,缓冲区写满会休眠(阻塞)。
函数原型
头文件: #include <unistd.h>
函数: int pipe(int pipefd[2);
参数:缓存地址,缓存用于存放读写管道的文件描述符。从这个参数的样子可以看出,这个缓存就是一个拥有两个元素的int型数组。
(1)元素[0]:里面放的是读管道的读文件描述符
(2)元素[1]:里面放的是写管道的写文件描述符。
功能:创建一个用于亲缘进程(父子进程)之间通信的无名管道(缓存) ,并将管道与两个读写文件描述符关联起来。特别需要注意的是,这两个读和写文件描述符,是两个不同的文件描述符。
注意:创建管道需要在创建子进程之前,因为如果创建在其之后,那么父子进程会各自创建一个管道,此时就有两个管道,因而不能实现通信。
②有名管道
特点:任意两个进程通信;
使用一个"有名管道"是无法实现双向通信的,因为也涉及到抢数据的问题;
函数原型
头文件: #include <unistd.h>
函数: int mkfifo(const char *pathname,
mode_t mode);
参数:
(1)pathname:被创建管道文件的文件路径名。
(2)mode:指定被创建时原始权限,一般为0664 (110110100) ,必须包含读写权限。
返回值:成功返回0,失败则返回-1,并且errno被设置。
功能:创建有名管道文件,创建好后便可使用open打开。
如果是创建普通文件的话,我们可以使用open的O_CREAT选项来创建,比如: open("./file",O_RDWR|O_CREAT, 0664);是对于"有名管道"这种特殊文件,这里只能使用mkfifo函数来创建。
有名管道用完后系统自动清除
使用步骤:(1)进程调用mkfifo创建有名管道
(2)open打开有名管道
(3)read/write读写管道进行通信
有名管道相比较于无名管道的优点:能够实现任意两个进程间的通信
两者都存在的缺点:只允许单向通信且会阻塞有名管道。改进:设置两个管道,一个用来读,一个用来写可以解决单向通信
创建有名管道
子进程实现写操作
父进程实现读操作
结果:
以上是父子进程间的有名管道通信,除此以外,任意两个进程也可以实现有名管道实现通信。
write.c
2、消息队列
消息队列的本质就是由内核创建的用于存放消息的链表,由于是存放消息的,所以我们就把这个链表称为消息队列。
特点:传送有格式的消息流;多进程网状交叉通信时,消息队列是上上之选;能实现大规模数据的通信;
使用步骤:
①使用msgget函数创建新的消息队列、或者获取已存在的某个消息队列,并返回唯一标识消息队列的标识符(msqID) ,后续收发消息就是使用这个标识符来实现的。
int msgget(key_t key,int msgflg);
头文件:
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/msg.h>
参数:
key值用于为消息队列生成(计算出)唯一的消息队列ID。
使用ftok函数来生成key
#include<sys/types.h>
#include<sys/ipc.h>
key_t ftok(const char *pathname,int proj_id);
ftok通过指定路径名和一个整型数,就可以计算并返回一个唯一对应的key值,
只要路径名和整型数不变,所对应的key值就唯一不变的。
不过由于ftok只会使用整型数(proj_id)的低8位,因此我们往往会指定为一个ASCII码值,因为ASCII码值刚好是8位的整形数。
msgfig:指定创建时的原始权限,比如0664
返回值:
成功:返回消息队列标识符(消息队列的ID)对于每一个创建好的消息队列来说, ID是固定的。
失败:失败返回-1,并设置errno。
②收发消息
发送消息:
1)进程先封装一个消息包
这个消息包其实就是如下类型的一个结构体变量,封包时将消息编号和消息正文写到结构体的成员中。
struct msgbuf
{long mtype;/放消息编号,必须>0/
char mtext[msgsz)]/*消息内容(消息正文) *
};
2)调用相应的API发送消息
msgsnd(int msqid, const void *msgp,
size_t msgsz,int msgflg);
参数:
msqid:消息队列的标识符。
msgp:存放消息的缓存的地址,类型struct msgbuf类型,这个缓存就是一个消息包(存放消息的结构体变量) 。
msgsz:消息正文大小。
msgflg:
a) 0:阻塞发送消息
也就是说,如果没有发送成功的话,该函数会一直阻塞等,直到发送成功为止。
-IPC_NOWAIT:非阻塞方式发送消息,不管发送成功与否,函数都将返回。也就是说,发送不成功的话,函数不会阻塞。
3)接受消息:
ssize_t mgrcv (int msqid, void *msgp,size_t msgsz,long msgtyp, int msgflg);
参数:
msqid:消息队列的标识符。
msgp:缓存地址,缓存用于存放所接收的消息,类型还是struct msgbuf:
msgsz:消息正文的大小
msgtyp:要接收消息的编号
int msglg:
a) 0:阻塞接收消息
也就是说如果没有消息时,接收回阻塞(休眠)。
IPC _NOWAIT:非阻塞接收消息,即没有消息时,该函数不阻塞。
③使用msgctl函数,利用消息队列标识符删除消息队列。
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
参数:
msqid:消息队列标识符
cmd:控制选项,其实cmd有很多选项,这里只简单介绍三个
IPC_STAT:将msqid消息队列的属性信息,读到第三个参数所指定的缓存。
IPC_SET:使用第三个参数中的新设置去修改消息队列的属性
- 定一个struct msqid_ds buf。
- 将新的属性信息设置到buf中
- cmd指定为IPC_SET后,msgctl函数就会使用buf中的新属性去修改消息队列原有的属性。
IPC_RMID:删除消息队列
删除消息队列时,用不到第三个参数,用不到时设置为NULL。
buf:存放属性信息,如果是删除队列则为NULL
3、共享内存
让同一块物理内存被映射到进程A、B各自的进程地址空间。进程A可以即时看到进程B对共享内存中数据的更新。
特点:减少进入内核空间的次数;直接使用地址来读写缓存时,效率会更高,适用于大数据量的通信;
使用步骤
①进程调用shmget函数创建新的或获取已有共享内存
int shmget(key_t key, size_t size, int shmflg);
参数:
key:用于生成共享内存的标识符,同消息队列key值。
size:指定共享内存的大小,我们一般要求size是虚拟页大小的整数倍。一般来说虚拟页大小是4k(4096字节),如果你指定的大小不是虚拟页的整数倍,也会自动补成整数倍。
semflg:与消息队列一样指定原始权限和。IPC_CREAT,比如0664|IPC_CREAT。
当在创建一个新的共享内存时才会用到,否者不会用到。
②进程调用shmat函数,将物理内存映射到自己的进程空间(因为进程不能直接访问物理内存)
void *shmat(int shmid, const void *shmaddr, int shmflg);
功能:将shmid所指向的共享内存空间映射到进程空间(虚拟内存空间),并返回映射后的起始地址(虚拟地址),有了这个地址后,就可以通过这个地址对共享内存进行读写操作。
参数:
shmid:共享内存标识符。
shmaddr:指定映射的起始地址 NULL:表示由内核自己来选择映射的起始地址(虚拟地址)。
这是最常见的方式,也是最合理的方式,因为只有内核自己才知道哪些虚拟地址可用,哪些不可用。
shmflg:指定映射条件。
0:以可读可写的方式映射共享内存
也就是说映射后,可以读、也可以写共享内存。
SHM_RDONLY:以只读方式映射共享内存
也就是说映射后,只能读共享内存,不能写。
③shmdt函数,取消映射
int shmdt(const void *shmaddr);
④调用shmctl函数释放开辟的那片物理内存空间
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
结果:
4、信号量
当多个进程/线程进行共享操作时,用于资源保护,以防止出现相互干扰的情况
资源保护的操作:
①互斥:对于互斥间不关心操作来说,多进程共享操作时,多个进程谁先操作、谁后操作的先后顺序问题,它们只关心一件事,那就是我在操作时别人不能操作。
②同步:所以所谓同步就是,多个共享操作时,进程必须要有统一操作的步调,按照一定的顺序来操作
实现互斥和同步的方法:加锁
信号量其实是OS创建的一个共享变量,进程在进行操作之前,会先检查这个变量的值,这变量的值就是一个标记,通过这个标记就可以知道可不可以操作,以实现互斥。
使用步骤:(互斥)
①进程调用semget函数创建新的信号量集合,或者获取已有的信号量集合。
int semget(key_t key, int nsems, int semflg);
参数:
key:设置同消息队列和共享内存。一般都使用ftok获取key值。
nsems:指定集合中信号量的个数。用于互斥时,数量都指定为1,因为只需要一个信号量。
semflg:设置同消息队列和共享内存。一般都设置为0664|IPC_CREAT。
返回值:调用成功则返回信号量集合的标识符,失败则返回-1,并且errno被设置。
在这里插入图片描述
②调用semctl函数给集合中的每个信号量设置初始值
int semctl(int semid, int semnum, int cmd, …);
参数:
semid:信号量标识符。通过标识符就能找到信号量集合。
semnum:集合中某个信号量的编号。信号量的编号为非负整数,而且是自动从0开始编号的。
cmd:控制选项。
SETVAL:通过第四个参数,给集合中semnu编号的信号量设置一个int初始值。
如果是二值信号量的话,设置初始值要么是0,要么是1,如果信号量的目的是互斥的话,基本都是设置为1。
③调用semop函数,对集合中的信号量进行pv操作(加锁解锁)
P操作(加锁):对信号量的值进行-1,如果信号量的值为0,p操作就会阻塞
V操作(解锁):对信号量的值进行+1,V操作不存在阻塞的问题
int semop(int semid, struct sembuf *sops, unsigned nsops);
参数:
semid:信号量集合的标识符。
sops:这个参数更好理解的写法是struct sembuf sops[],
nsops:用于指定数组元素个数的。
结构体成员
struct sembuf
{
unsigned short sem_num;
short sem_op;
short sem_flg;
}
这个结构体不需要我们自己定义,因为在semop的头文件中已经定义了。
sem_num:信号量编号,决定对集合中哪一个信号量进行pv操作
sem_op:设置为-1,表示想-1进行p操作,设置1表示想+1进行v操作
sem_flg:
IPC_NOWAIT: 一般情况下,当信号量的值为0时进行p操作的话,semop的p操作会阻塞。如果不想阻塞的话,可以指定这个选项,NOWAIT就是不阻塞的意思。
-SEM_UNDO:防止死锁,还是以二值信号量为例,当进程在v操作之前就结束时,信号量的值就会一直保持为0,那么其它进程将永远无法p操作成功,会使得进程永远休眠下去,这造成就是死锁。
④调用semctl删除信号量集合
结果:没有乱码