面试必问:七大进程间通信和线程同步

网络编程是Linux C/C++的面试重点,这次我就来聊聊进程间通信和线程同步的问题,可以参看《unix高级环境编程》,希望帮助到大家。

值得一读:
1、https://www.nowcoder.com/discuss/193598?source_id=profile_create&channel=2002(Linux C/C++ 学习路线,已拿 BAT offer!)
2、https://www.nowcoder.com/discuss/225624?source_id=profile_create&channel=2002(我凭着这份简历,拿下了 BAT offer)

关于如何写简历,以及我的简历模板(好好看看上面推荐的文章),或者实习、秋招有什么问题,都可以私信找我,很愿意帮助到大家。

一、进程间的七大通信方式

signal、file、pipe、shm、sem、msg、socket

1、signal

信号通信的目的:某某事件发生!此时需要处理什么,进程间(可以是不相关的进程)传递信号

场景:信号又被称之为中断,需要处理什么对应的是中断处理函数,此时设置断点,形参入栈,保存现场信息,然后去执行中断处理函数,当处理完成之后,恢复现场信息,程序继续往下执行

Linux下可以通过kill -l查看其所有信号(其一共64种信号)

发送信号:kill(pid, 信号)    //对指定的进程发送什么信号

raise(信号) <==> kill(getpid(), 信号)  //就是给自己发送指定的信号

alarm(秒数) :定时产生一个SIGALRM信号,调用alarm方法之后,只会产生一次该信号

接收信号:signal(信号,函数指针)   //对该信号接收,并调用自己的函数指针进行处理

信号通信方式的局限性:不能够传递复杂的、有效的、具体的数据。

2、file

每打开一个文件,就会产生一个文件控制块,而文件控制块与文件描述符是一一对应的,通过对文件描述符的操作进而对文件进行操作

文件描述符的分配原则:编号的连续性(节省编号的资源)

通过文件系统对文件描述符的读/写控制,进程间一方对文件写,一方对文件读,达到文件之间的通信;可以是不相关进程间的通信

使用的API:write()和read()

为了能够实现两个进程通过文件进行有序的数据交流,还得借助于信号的处理机制

(1)、通过pause()等待对方发起一个信号,已确认可以开始执行下一次读/写操作;

pause():只要接受到任何的信号,立马就可以往下执行

(2)、通过kill(,SIGUSR1)方法向对方发出明确的信号:可以开始下一步执行(读、写)

缺点:i、文件通信没有访问规则,ii、(因为CPU > 内存 > 文件)是低速的

3、pipe

在通信的进程间构建一个单向的数据流动的通道,数据通过管道从一个进程流向另一个进程是具有时间先后顺序的,所以是半双工通信;管道文件是一种临时文件,不是磁盘上真真正正的文件,是一块内存区域

分为:

fd[0]:读出数据

fd[1]:写入数据

无名管道:只能用于亲缘关系的父子进程,fd = pipe(),得到的是管道文件描述符,通过fd,用的是write()和read()读写数据;

达到双方通信:得用2个管道,达到可以发多句话,的fork()子进程处理

有名管道:非父子进程间通信mkfifo()

mkfifo会在文件系统中创建一个管道文件,然后使其映射内存的一个特殊区域,凡是能够打开mkfifo创建的管道文件进程(通过这个文件描述符),都可以使用该文件实现FIFO的数据流动

mkfifo(文件名, O_CREAT | O_EXCL | 0755);创建了2个管道文件,在客户端创建一个读的管道文件,在服务器创建一个写的管道文件,然后当做文件操作即可

socketpair可以创建双向管道,fd[0]、fd[1]都是同时具有读和写的属性;

优点:(1)、有强制的访问规则FIFO,(2)、用内存模仿文件,也就是用文件的方式操作内存

管道通信的特点:

(1)、如果管道为空,从管道读取数据的一方会阻塞。直到管道中有新的数据为止

(2)、管道的数据通信具有FIFO特性,这样可以避免数据的混乱

(3)、管道数据的读取与发送并没有次数限制,而是管道是否为空时最重要的指标

(4)、这种管道的使用具有一个最大的局限性:只适用于父子进程之间。从程序的设计中可以看到,管道的创建是父进程完成的,而且是在创建子进程之前,从而才使得子进程拥有了管道文件描述符,才能够使得父子进程约定持有管道的入口或出口

(5)、一个管道只能实现单向的数据流

使用ipcs可以查看当前系统中IPC资源的情况。ipcrm -m shmid   ipcrm -s semid

ipcrm -q msgid

4、shm

各个进程都能够共同访问的共享的内存区域;是独立于所有的进程空间之外的地址区域;  (不相关)进程之间的通信

进程对于共享内存的操作与管理主要是:

(1)、申请创建一个共享内存区域(操作系统内核是不可能主动为进程创建共享内存的!),操作系统内核得到申请然后创建

(2)、申请使用一个已存在的共享内存区域

(3)、申请释放共享内存区域(操作系统内核也是不可能主动释放共享内存区域的!),操作系统内核得到申请然后释放

说明key_t key

i>、key_t是一个long类型,是IPC资源外部约定的key(关键)值,通过key值映射对应的唯一存在的某一个IPC资源

ii>、通过key_t的值就能够判断某一个对应的共享内存区域在哪,是否已经创建等等。

iii>、一个key值只能映射一个共享内存区域,但同时还可以映射一个信号量,而且还能同时映射一个消息队列资源,于是就可以使用一个key值管理三种不同的资源
int shmget(key_t key, size_t size, int shmflg); //返回值是共享内存的标号shmid
int shmid = shmget(key, 256, IPC_CREAT | IPC_EXCL | 0755);
key_t值的产生,有两种方式:

i>、把key值写死; //自己直接写一个数字即可

ii>、根据文件的inode编号生成。需要调用的API:ftok("./tmp/a.c", 3)方法,该方法是获取指定文件的inode编号在根据第二个参数计算得到最终的一个整型量。

shm的使用:

i>、建立进程与共享内存的映射关系

ii>、读/写(直接使用指针即可

iii>、如果对于共享内存的使用结束,此时就要断开与共享内存的映射

对于第一步来说,需要使用的API:shmat()方法。

对于第三步来说,需要使用的API:shmdt()方法。

被映射正在使用共享内存是否此时可以执行删除操作呢

是,虽然可以执行删除操作,却不能将其直接删除掉。而是做了2个操作

i>、将其状态置为dest(可回收状态)

ii>、将其key值置为0x00000000,IPC_PRIVATE值

当共享内存处于dest(待回收状态),则将其资源设为"私有"(只能将该共享资源分享给其子进程,其它进程无法创建于该资源的使用),当所有的使用该共享内存的进程都退出,此时操作系统才回收共享内存

共享内存的控制

共享内存的控制信息可以通过shmctl()方法获取,会保存在struct_shmid_ds结构体中

共享内存的控制主要是shmid_ds,即就是共享内存的控制信息

cmd:看执行什么操作(1、获取共享内存信息;2、设置共享内存信息;3、删除共享内存)

API:int shmctl(int shmid, int cmd, struct shmid_ds *buf)

5、sem

原因:进程在访问共享资源是存在冲突的,必须的有一种强制手段说明这些共享资源的访问规则------>信号量

sem:表示的是一种共享资源(空闲)的个数,对共享资源的访问规则

访问规则:

i>、用一种数量去标识某一种共享资源的个数(空闲)

ii>、当有进程需要访问对应的共享资源的时候,则需要先查看(申请),根据资源对应的当前可用数量进行申请。(申请所需要使用的资源个数)

iii>、资源的管理者(操作系统内核),就使用当前的资源个数减去要申请的资源个数,结果 >=0,表示有可用资源,允许该进程继续访问;否则表示资源不可用,则告诉进程(暂停或者立即返回)

iv>、资源数量的变化就表示资源的占用和释放。占用:使得可用资源减少;释放:使得可用资源增加

创建信号量集:int semid = semget(key_t key, int nsems, int semflg)

初始化信号量:

信号量ID事实上是信号量集合的ID,一个ID对应的是一组信号量。此时就使用信号量ID设置整个信号量集合,这种操作分为2种大的可能性

i>、针对信号量集合中的一个信号量进行设置;信号量集合中的信号量是按照数组的方式被管理起来的,从而可以直接使用信号的数组下标来进行访问

ii>、针对整个信号量集和进行统一的设置。

需要使用的API:semctl()方法。
int semctl(int semid, int semnum, int cmd, ...);
cmd参数

GETALL 获取信号量集合中所有信号量的资源个数
SETALL 设置所有
GETVAL 获取其中一个
SETVAL 设置其中一个
第四个参数 可变参

如果cmd是GETALL、SETALL、GETVAL、SETVAL...的话,则需要提供第四个参数。第四个参数是一个共用体,这个共用体在程序中必须的自己定义(作用:初始化资源个数),定义格式如下:
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) */
};
 
信号量的操作:

API:semop()方法。  (op:operator操作)
int semop(int semid, struct sembuf *sops, unsigned nsops);
第二个参数需要借助结构体struct sembuf:
struct sembuf{
unsigned short sem_num;  /* semaphore number  数组下标 */
short sem_op;   /* semaphore operation */
short sem_flg;  /* operation flags 默认0*/
};
通过下标直接对其信号量sem_op进行加减即可

信号量的特征:

如果有进程通过信号量申请共享资源,而且此时资源个数已经小于0,则此时对于该进程,有两种可能性:等待资源,不等待。

如果此时进程选择等待资源,则操作系统内核会针对该信号量构建进程等待队列,将等待的进程加入到该队列之中。

如果此时有进程释放资源,则会:(1)、先将资源个数增加;(2)、从等待队列中抽取第一个进程;(3)、根据此时资源个数和第一个进程需要申请的资源个数进行比较,结果大于0,则唤醒该进程;结果小于0,则让该进程继续等待。

所以信号量的操作和共享内存一般联合使用来达到进程间的通信

6、msg

就是在进程间架起通道,从宏观上看是一样的,但是管道在字节流上是连续的,消息队列在发送数据时,分为一个一个独立的数据单元,也就是消息体,每个消息体都是固定大小的存储块,在字节流上不连续;

消息队列与管道不同的地方在于:管道中的数据并没有分割为一个一个的数据独立单位,在字节流上是连续的。然而,消息队列却将数据分成了一个一个独立的数据单位,每一个数据单位被称为消息体。每一个消息体都是固定大小的存储块儿,在字节流上是不连续的。

创建消息队列
int msgget(key_t key, int msgflg);  //创建0个消息队列
消息的发送和消息的接收

在发送消息的时候动态的创建消息队列;
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp,int msgflg);
(1)、msgsnd()方法在发送消息的时候,是在消息体结构体中指定,当前的消息发送到消息队列集合中的哪一个消息队列上。

(2)、消息体结构体中就必须包含一个type值,type值是long类型,而且还必须是结构体的第一个成员。而结构体中的其他成员都被认为是要发送的消息体数据。

(3)、无论是msgsnd()发送还是msgrcv()接收时,只要操作系统内核发现新提供的type值对应的消息队列集合中的消息队列不存在,则立即为其创建该消息队列



总结:为了能够顺利的发送与接收,发送方与接收方需要约定:i>、同样的消息体结构体;(2)、发送方与接收方在发送和接收的数据块儿大小上要与消息结构体的具体数据部分保持一致! 否则:将不会读出正确的数据

重点注意:

消息结构体被发送的时候,只是发送了消息结构体中成员的值,如果结构体成员是指针,并不会将指针所指向的空间的值发送,而只是发送了指针变量所保存的地址值。数组作为消息体结构体成员是可以的。因为整个数组空间都在消息体结构体中
struct msgbuf{
long mtype;   //自己制定要传输的消息队列的编号(由自己任意指定);    
char mtext[1]; //只能是数组,不能是指针,发送的数据块;
};
long mtype制定消息队列编号,下面的数组才是要发送的数据,计算大小,也是这个数组所申请的空间大小。接收方倒数第二个参数为:mtype的值(制定的消息队列编号)。

均是指针连接;

在接收的时候的指明是哪个消息队列进行接收(比发送多了一个参数);

7、socket
网络之间不同进程间通信,相当于网络编程部分了

二、线程

1、线程的API函数

函数名称 说明
pthread_create() 创建线程
pthread_equal() 比较线程
pthread_self() 得到当前线程ID
pthread_exit() 线程内部退出
pthread_join() 等一个线程的结束(让一个线程加入到另一个线程的执行队列之后)
pthread_cancel() 终止某线程的执行
pthread_t 线程ID(线程唯一标识)

(2)、线程间的协作

在一个进程中会出现多个线程会访问同一个内存区域,因此就需要使用一种线程间的协作手段来处理

线程的同步机制主要有:互斥量,信号量,条件变量

互斥量 :出现了mutex,就为互斥量,为锁机制

函数名称 说明
pthread_mutex_init() 动态初始化临界(互斥)资源标识
pthread_mutex_destroy() 销毁临界资源标识
pthread_mutex_lock() 上锁 (阻塞)
pthread_mutex_trylock() 尝试上锁 (非阻塞)
pthread_mutex_unlock() 解锁
pthread_mutex_t 类型 互斥量
(1)、在一个lock(加锁)和unlock(解锁)之间,形成的叫做:临界区域

线程同步:阻塞别人而完成自己。利用互斥量达到同步,使封锁区域最小化

(2)、加锁后,没有解锁————>将发生阻塞(不能再进行加锁)

(3)、利用互斥量,将程序执行的不确定顺序变为了确定性的顺序

2、条件变量

i>静态初始化:
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
ii>、动态初始化

分别调用pthread_mutex/cond_init,pthread_mutex/cond_destroy()初始化;

条件变量针对死锁情况(就是没有出现unlock),此时调用pthread_cond_wait()方法也可以进行解锁;也就是说wait()函数会在阻塞之时进行解锁。

pthread_cond_wait()方法是:在阻塞之时,自动解锁。

该方法在遇到pthread_cond_signal()时,唤醒等待的wait()方法,但是不直接执行wait()其后的语句,而是接着原先pthread_cond_signal()其后的方法继续执行,直到遇到pthread_mutex_lock()锁时,此时,转到wait()其后的方法执行。

在这里利用的是pthread_cond_wait()和pthread_cond_signal()方法

3、线程间同步

锁机制:互斥锁、条件变量、信号量、读写锁

互斥锁:提供了以排他方式数据结构被并发修改的方法

读写锁:写锁优先抢占资源,读锁允许多个线程共同读共享数据,而写锁操作是互斥的

条件变量:以原子方式阻塞进程,直到某个特定条件为真为止

一般情况下:互斥锁起保护作用,条件变量和互斥锁一起使用

总结:线程间通信的目的主要用于线程同步,所以线程没有像进程通信中的用于数据交换的通信机制




#2021校招研发提前批开始啦##学习路径#
全部评论
前排支持!!!!
点赞 回复 分享
发布于 2020-06-24 22:38
前排
点赞 回复 分享
发布于 2020-06-24 23:10
支持
点赞 回复 分享
发布于 2020-06-24 23:23
支持
点赞 回复 分享
发布于 2020-06-25 07:37

相关推荐

点赞 评论 收藏
分享
昨天 00:11
已编辑
广东工业大学 算法工程师
避雷深圳&nbsp;&nbsp;yidao,试用期&nbsp;6&nbsp;个月。好嘛,试用期还没结束,就直接告诉你尽快找下一家吧,我谢谢您嘞
牛客75408465号:笑死,直属领导和 hr 口径都没统一,各自说了一些离谱的被裁理由,你们能不能认真一点呀,哈哈哈哈哈😅😅😅
点赞 评论 收藏
分享
17 151 评论
分享
牛客网
牛客企业服务