嵌入式系统设计--课堂总结(嵌入式Linux多任务:进程、线程)

嵌入式Linux多任务:进程、线程

硬件条件:单个CPU单个核
单任务:一个任务执行完毕之后下个任务才能执行;
多任务:任务的执行可以被中断,中断之后可以执行其他任务;(并发/并行)
单核CPU:并发
多核的CPU:并发,并行

进程实现多任务;
特点:给每个进程分配独立的地址空间,4G的大小(1G内核,3G用户空间(栈、堆、数据段、代码段);互不干扰;
进程创建方式:fork>exec函数族>system>vfork;
进程的退出:exit()库函数:清理缓冲 _exit()系统调用API:不清理缓冲
进程等待:wait();解决:僵尸进程
<僵尸进程、孤儿进程、守护进程、控制台进程、后台进程>
学习目标:学会创建多任务程序:进程

进程间通信–IPC
原理:尽管进程空间是各自独立的,相互之间没有任何可以共享的空间,但是至少还有一样东西是所有进程所共享的,那就是OS,因为甭管运行有多少个进程,但是它们共用OS只有一个。
既然大家共用的是同一个OS,那么显然,所有的进程可以通过大家都共享第三方OS来实现数据的转发。
因此进程间通信的原理就是,OS作为所有进程共享的第三方,会提供相关的机制,以实现进程间数据的转发,达到数据共享的目的。

广义上的进程间通信:A进程—文件—B进程 或 A进程—数据库—B进程
通过普通文件通信
嵌入式系统设计--课堂总结(嵌入式Linux多任务:进程、线程)_第1张图片
嵌入式系统设计--课堂总结(嵌入式Linux多任务:进程、线程)_第2张图片
结果:通信成功
在这里插入图片描述

狭义上的真正的 “进程间通信”(内核提供):
1、管道
①无名管道
特点:管道只允许具有血缘关系的进程间通信,如父子进程间的通信;
管道只允许单向通信;
读管道时,如果没有数据的话,读操作会休眠(阻塞),写数据时,缓冲区写满会休眠(阻塞)
函数原型
头文件:#include
函数:int pipe(int pipefd[2]);
参数:缓存地址,缓存用于存放读写管道的文件描述符。从这个参数的样子可以看出,这个缓存就是一个拥有两个元素的int型数组。
1)元素[0]:里面放的是读管道的读文件描述符
2)元素[1]:里面放的是写管道的写文件描述符。
功能:创建一个用于亲缘进程(父子进程)之间通信的无名管道(缓存),并将管道与两个读写文件描述符关联起来。 特别需要注意的是,这里的读和写文件描述符,是两个不同的文件描述符。
注意:创建管道需要在创建子进程之前,因为如果创建在之后,那么父子进程会各自创建一个管道,因而不能实现通信。
嵌入式系统设计--课堂总结(嵌入式Linux多任务:进程、线程)_第3张图片
子程序实现写操作:注意:要关闭读操作
嵌入式系统设计--课堂总结(嵌入式Linux多任务:进程、线程)_第4张图片
父进程实现读操作:
嵌入式系统设计--课堂总结(嵌入式Linux多任务:进程、线程)_第5张图片
结果:
嵌入式系统设计--课堂总结(嵌入式Linux多任务:进程、线程)_第6张图片
②有名管道
特点:任意两个进程通信;
使用一个“有名管道”是无法实现双向通信的,因为也涉及到抢数据的问题;
函数原型
头文件:#include
函数: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函数来创建。
有名管道用完后系统自动清除
使用步骤:
进程调用mkfifo创建有名管道;
open打开有名管道;
read/write读写管道进行通信;
创建有名管道:
嵌入式系统设计--课堂总结(嵌入式Linux多任务:进程、线程)_第7张图片
子进程实现写操作:
嵌入式系统设计--课堂总结(嵌入式Linux多任务:进程、线程)_第8张图片
父进程实现读操作:
嵌入式系统设计--课堂总结(嵌入式Linux多任务:进程、线程)_第9张图片
结果:
嵌入式系统设计--课堂总结(嵌入式Linux多任务:进程、线程)_第10张图片
以上是父子进程间的有名管道通信,除此以外,任意两个进程也可以通过有名管道实现通信
write.c
嵌入式系统设计--课堂总结(嵌入式Linux多任务:进程、线程)_第11张图片
read.c
嵌入式系统设计--课堂总结(嵌入式Linux多任务:进程、线程)_第12张图片
结果:
一个进程写
在这里插入图片描述
一个进程读
在这里插入图片描述
有名管道相比较于无名管道的优点:能够实现任意两个进程间的通信
两者都存在的缺点:只允许单向通信且会阻塞
有名管道改进:设置两个管道,一个用来读,一个用来写可以解决单向通信
2、消息队列
消息队列的本质就是由内核创建的用于存放消息的链表,由于是存放消息的,所以我们就把这个链表称为消息队列。
特点:传送有格式的消息流;多进程网状交叉通信时,消息队列是上上之选;能实现大规模数据的通信;
使用步骤:
①使用msgget函数创建新的消息队列、或者获取已存在的某个消息队列,并返回唯一标识消息队列的标识符(msqID),后续收发消息就是使用这个标识符来实现的。
int msgget(key_t key, int msgflg);
头文件:
#include
#include
#include
参数:
key值用于为消息队列生成(计算出)唯一的消息队列ID。
使用ftok函数来生成key
#include
#include
key_t ftok(const char *pathname, int proj_id);

ftok通过指定路径名和一个整形数,就可以计算并返回一个唯一对应的key值,
只要路径名和整形数不变,所对应的key值就唯一不变的。
不过由于ftok只会使用整形数(proj_id)的低8位,因此我们往往会指定为一个ASCII码值,因为ASCII码值刚好是8位的整形数。
msgflg: 指定创建时的原始权限,比如0664
返回值:
成功:返回消息队列标识符(消息队列的ID)对于每一个创建好的消息队列来说,ID是固定的。
失败:失败返回-1,并设置errno。
嵌入式系统设计--课堂总结(嵌入式Linux多任务:进程、线程)_第13张图片
②收发消息
发送消息:
(a)进程先封装一个消息包
这个消息包其实就是如下类型的一个结构体变量,封包时将消息编号和消息正文写到结构体的成员中。
struct msgbuf
{
long mtype; /* 放消息编号,必须> 0 /
char mtext[msgsz]; /
消息内容(消息正文) */
};
嵌入式系统设计--课堂总结(嵌入式Linux多任务:进程、线程)_第14张图片
(b)调用相应的API发送消息
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
参数:
msqid:消息队列的标识符。
msgp:存放消息的缓存的地址,类型struct msgbuf类型,这个缓存就是一个消息包(存放消息的结构体变量)。
msgsz:消息正文大大小。
msgflg:

  • 0:阻塞发送消息
    也就是说,如果没有发送成功的话,该函数会一直阻塞等,直到发送成功为止。
    -IPC_NOWAIT:非阻塞方式发送消息,不管发送成功与否,函数都将返回
    也就是说,发送不成功的的话,函数不会阻塞。
    嵌入式系统设计--课堂总结(嵌入式Linux多任务:进程、线程)_第15张图片
    接受消息:
    ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
    参数:
    msqid:消息队列的标识符。
    msgp:缓存地址,缓存用于存放所接收的消息,类型还是struct msgbuf:
    msgsz:消息正文的大小
    msgtyp:要接收消息的编号
    int msgflg:
  • 0:阻塞接收消息
    也就是说如果没有消息时,接收回阻塞(休眠)。
    IPC_NOWAIT:非阻塞接收消息
    也就是说没有消息时,该函数不阻塞
    嵌入式系统设计--课堂总结(嵌入式Linux多任务:进程、线程)_第16张图片
    ③使用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
    在这里插入图片描述
    wait(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。
    有在创建一个新的共享内存时才会用到,否者不会用到
    嵌入式系统设计--课堂总结(嵌入式Linux多任务:进程、线程)_第17张图片
    ②进程调用shmat函数,将物理内存映射到自己的进程空间(因为进程不能直接访问物理内存)
    void *shmat(int shmid, const void *shmaddr, int shmflg);
    功能:将shmid所指向的共享内存空间映射到进程空间(虚拟内存空间),并返回影射后的起始
    地址(虚拟地址),有了这个地址后,就可以通过这个地址对共享内存进行读写操作。
    参数:
    shmid:共享内存标识符。
    shmaddr:指定映射的起始地址 NULL:表示由内核自己来选择映射的起始地址(虚拟地址)。
    这是最常见的方式,也是最合理的方式,因为只有内核自己才知道哪些虚拟地址可用,哪些不可用。
    shmflg:指定映射条件。
    0:以可读可写的方式映射共享内存
    也就是说映射后,可以读、也可以写共享内存。
    SHM_RDONLY:以只读方式映射共享内存
    也就是说映射后,只能读共享内存,不能写。
    嵌入式系统设计--课堂总结(嵌入式Linux多任务:进程、线程)_第18张图片
    ③shmdt函数,取消映射
    int shmdt(const void *shmaddr);
    ④调用shmctl函数释放开辟的那片物理内存空间
    int shmctl(int shmid, int cmd, struct shmid_ds *buf);
    结果:
    嵌入式系统设计--课堂总结(嵌入式Linux多任务:进程、线程)_第19张图片
    4、信号量
    当多个进程/线程进行共享操作时,用于资源保护,以防止出现相互干扰的情况
    资源保护的操作:
    ①互斥:对于互斥操作来说,多进程共享操作时,多个进程间不关心谁先操作、谁后操作的先后顺序问题,它们只关心一件事,那就是我在操作时别人不能操作。
    ②同步:所以所谓同步就是,多个共享操作时,进程必须要有统一操作的步调,按照一定的顺序来操作
    实现互斥和同步的方法:加锁
    信号量其实是OS创建的一个共享变量,进程在进行操作之前,会先检查这个变量的值,这变量的值就是一个标记,通过这个标记就可以知道可不可以操作,以实现互斥。
    使用步骤:(互斥)
    ①进程调用semget函数创建新的信号量集合,或者获取已有的信号量集合。
    int semget(key_t key, int nsems, int semflg);
    参数:
    key:设置同消息队列和共享内存。一般都使用ftok获取key值。
    nsems:指定集合中信号量的个数。用于互斥时,数量都指定为1,因为只需要一个信号量。
    semflg:设置同消息队列和共享内存。一般都设置为0664|IPC_CREAT。
    返回值:调用成功则返回信号量集合的标识符,失败则返回-1,并且errno被设置。
    嵌入式系统设计--课堂总结(嵌入式Linux多任务:进程、线程)_第20张图片
    ②调用semctl函数给集合中的每个信号量设置初始值
    int semctl(int semid, int semnum, int cmd, …);
    参数:
    semid:信号量标识符。通过标识符就能找到信号量集合。
    semnum:集合中某个信号量的编号。信号量的编号为非负整数,而且是自动从0开始编号的。
    cmd:控制选项。
    • SETVAL:通过第四个参数,给集合中semnu编号的信号量设置一个int初始值。
      如果是二值信号量的话,设置初始值要么是0,要么是1,如果信号量的目的是互斥的话,基本都是设置为1。
      嵌入式系统设计--课堂总结(嵌入式Linux多任务:进程、线程)_第21张图片
      ③调用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操作成功,会使得进程永远休眠下去,这造成就是死锁。
      嵌入式系统设计--课堂总结(嵌入式Linux多任务:进程、线程)_第22张图片
      ④调用semctl删除信号量集合
      结果:没有乱码
      嵌入式系统设计--课堂总结(嵌入式Linux多任务:进程、线程)_第23张图片

你可能感兴趣的:(嵌入式系统设计--课堂总结(嵌入式Linux多任务:进程、线程))