Professional Linux kernel Architecture - 锁与进程间通信
2016-08-04 15:53
381 查看
http://note.youdao.com/yws/public/redirect/share?id=cff581642044a36bd88984432da8b6e6&type=false
参考《Professional Linux kernel Architecture》 .
1、相关概念:
IPC :(Interprocess communication) 进程间通信
竞态条件 :几个进程在访问资源时彼此干扰的情况,进程执行在不应该中断的地方被中断,从而导致进程工作不正确。
锁的作用:确保每次只有一个CPU或者进程访问被保护的范围
2、内核锁机制:
原子操作:
简单来说就是不能被中断的操作,最简单的锁操作;
自旋锁:
用于短期保护某段代码,防止其他处理器访问,内核等等自旋锁释放期间会重复检查是否获得锁,不会进入休眠状态,所谓“自旋”的形象描述;
需要注意:
1、自旋锁获得后不释放系统将变得不可用,会进入死循环,产生死锁;
2、自旋锁块不应该长期持有,影响性能;
3、需要保证自旋锁保护的代码绝对不会进入睡眠;
信号量:
1、进程执行危险代码段时前,需要对信号量做 down(减1)操作,如果此时已经为0,说明已经有进程再执行危险代码了,此时该进程进入阻塞休眠状态(和自旋锁不同),直到前一个进程退出执行Up操作,调度器无法中断执行down操作的过程,属于原子操作;
2、缺点:内核开销大,影响性能;
3、数据结构:
count:指定可同时处于信号量保护临界区进程数目;
sleepers:指定等待运行进入临界区进程数目,等待的进程会进入睡眠,直到信号量释放才被唤醒;
wait:实现一个队列,保存所有信号量上睡眠的进程的task_struct
4、用法:
内核提供了简化宏定义接口:DECLARE_MUTEX,声明一个二值信号量.
值得注意的是,最新3.18版本内核已经使用互斥锁代替.
3、RCU机制:
rcu :read-copy-update 读-复制-更新
原理:数据结构将要改变时,先创建一个新副本,在副本中修改,所有访问者结束对旧副本读取后,指针指向新副本。
使用实例:
假设ptr指针指向被rcu保护的数据结构,那么需要被以下高亮部分代码保护起来:
dev->ip6_ptr 是被rcu保护的数据结构指针.
资源释放函数:
synchronize_rcu(); 该函数后释放所有关联资源是安全的;
call_rcu(struct rcu_head *head, void (*func)(struct rcu_head *rcu)) 用于注册一个函数在所有资源访问完成后回调。
rcu机制可以保护链表,接口函数是在原有的内核标准链表函数直接加上 _rcu后缀即可,列举:
<rculist.h>
4、内存和优化屏障 barrier
rmb() 读访问内存屏障,它保证该函数前的所有读操作不可乱序执行(流水线指令重排);
wmb() 写访问内存屏障,原理类似
mb() 合并了rmb() + wmb() 功能;
这是Linux kernel层面上的屏障接口,向下对接各种体系结构CPU具体的接口,比如ARMv7以后架构:
5、读者/写者锁
任何并发处理器可以对数据结构经常读操作,但只有一个处理器可以进行写操作。
读:read_lock /read_unlock, 允许临界区内任意数目的读进程并发访问;
写:write_lock/write_unlock, 内核只允许一个写进程处于临界区访问;
6、大内核锁 BKL
可以锁住整个内核,非常影响性能,最新3.18版本内核已将它弃用,成为了历史;
7、互斥量 <mutex.h> (互斥锁)
和信号量互斥原理类似,定义方法分为静态和动态两种:
1、静态定义: 使用 DEFINE_MUTEX 产生;
2、动态定义:mutex_init() ,在运行时产生;
mutex_lock/mutex_unlock 用于上锁和解锁
实时互斥量 rt_mutex,用于解决优先级反转问题,最新内核3.18代码好像已经弃用或者新机制代替。
标准函数:
rt_mutex_init
rt_mutex_lock
rt_mutex_unlock
*一个很重要很常见的数据结构:container_of(ptr,type,member)
这个数据结构在内核代码中大量被使用,初看起来数据结构十分复杂,但是只要明白了原理就很好理解了.
ptr指针的类型和member是类似的,type是包含member成员类型的容器,
这个宏定义的作用简单理解就是:给出一个结构实例,通过计算该结构在所在容器的内存偏移,反向找到所在容器的首地址。
找出ptr指向实例本身所在的容器结构首地址,结构类型是type,比如以下代码:
找出napi数据结构所在容器结构的首地址.
进程间通信
1、System V 机制
类型 : 信号量(和上面的信号量锁没有任何关系)、消息队列、共享内存;
主要用于两个不同进程间临界区的代码保护,实现“通信”.
信号灯(信号量IPC另外叫法)与其他进程间通信方式不大相同,它主要提供对进程间共享资源访问控制机制。相当于内存中的标志,进程可以根据它判定是否能够访问某些共享资源,同时,进程也可以修改该标志。除了用于访问控制外,还可用于进程同步。信号灯有以下两种类型:
- 二值信号灯:最简单的信号灯形式,信号灯的值只能取0或1,类似于互斥锁。
注:二值信号灯能够实现互斥锁的功能,但两者的关注内容不同。信号灯强调共享资源,只要共享资源可用,其他进程同样可以修改信号灯的值;互斥锁更强调进程,占用资源的进程使用完资源后,必须由进程本身来解锁。
- 计算信号灯:信号灯的值可以取任意非负值(当然受内核本身的约束)。
(http://www.cnblogs.com/andtt/articles/2136279.html)
数据结构:
使用几个数据结构建立网状结构,负责将信号量与等待进程关联起来
struct ipc_namespace {
...
struct ipc_ids *ids[3]; //0 ->共享内存,1->信号量,2->消息队列;
...
}
ipc_namespace 默认由以下实例实现:
ipc_ids:
in_use: 当前使用中ipc对象的数目;
seq: 用于产生ipc ID,内核通过ID来管理ipc对象;
rwsem:内核信号量(锁)
ipcs_idr 用于关联kern_ipc_perm 实例指针,每个ipc对象都由kern_ipc_perm一个实例表示;
数据结构网状关系图:
从 静态ipc_namespace对象的ipc_ids找到对应的ID到映射的指针,找到kern_ipc_perm实例,通过 结构->容器方法(container_of)找到该对象
所在容器对象的sem_array结构的首地址,sem_base保存了当前信号量的值和上一次访问它的进程id,sem_pending 指向sem_queue链表,而该sem_queue对象来之各个进程. 3.18内核已经改掉了这个关系图,以上作为理解参考。
获取ipc对象的系统调用关系图:
3.18 内核代码:
2、消息队列.
进程间交换消息,使用内核的消息队列机制实现。
产生消息,并把消息写入队列的进程称为发送者(sender) ,一个或者多个其他进程则从队列中获取消息,称为接收者(recevier)
简图:进程A发生消息到msg_queue中,进程B,C从 msg_queue中获取消息.
数据结构:
q_messages 中各个消息封装在一个msg_msg实例中:
q_senders 中链表元素是msg_sender 数据结构:
q_receivers 中链表元素:
消息队列涉及数据结构关系图:(省去睡眠的发生消息进程链表)
3.18 内核源代码分析
发送消息系统调用入口:
接收消息系统调用入口:
3、共享内存.
使得多个进程可以访问同一块内存空间, 是最快的可用IPC形式。 是针对其他通信机制运行效率低而设计的。 往往与其他通信机制, 如信号量结合使用, 来达到进程间的同步及互斥。
共享内存允许两个或多个进程共享一定的内存区, 因为不需要拷贝数据, 所以这是最快的一种IPC,binder机制就是使用共享内存的方式实现。
内核的实现和前面两种对象非常类似,与信号量和消息队列相比,共享内存没有本质区别,流程很类似.相关数据结构如下图:
1、应用程序请求的IPC对象,可以通过魔数和当前的命名空间的内核ID访问;
2、对内存的访问可能受到权限系统的限制;
3、可以使用system call分配和IPC对象关联的内存,具备适当授权的所有进程都可以访问该内存.
以下是用户层接口:
#include <sys/ipc.h>
#include <sys/shm.h>
(1)创建或访问共享内存
* int shmget(key_t key,size_t size,int shmflg);
(2)附加共享内存到进程的地址空间
* void *shmat(int shmid,const void *shmaddr,int shmflg);//shmaddr通常为NULL, 由系统选择共享内存附加的地址;shmflg可以为
SHM_RDONLY
(3)从进程的地址空间分离共享内存
* int shmdt(const void *shmaddr); //shmaddr是shmat()函数的返回值
(4)控制共享内存
* int shmctl(int shmid,int cmd,struct shmid_ds *buf);
* struct shmid_ds{
struct ipc_perm shm_perm;
…
};
cmd的常用取值有:(a)IPC_STAT获取当前共享内存的shmid_ds结构并保存在buf中(2)IPC_SET使用buf中的值设置当前共享内存的
shmid_ds结构(3)IPC_RMID删除当前共享内存
4、管道、信号、套接字(socket
...
参考《Professional Linux kernel Architecture》 .
1、相关概念:
IPC :(Interprocess communication) 进程间通信
竞态条件 :几个进程在访问资源时彼此干扰的情况,进程执行在不应该中断的地方被中断,从而导致进程工作不正确。
锁的作用:确保每次只有一个CPU或者进程访问被保护的范围
2、内核锁机制:
原子操作:
简单来说就是不能被中断的操作,最简单的锁操作;
自旋锁:
用于短期保护某段代码,防止其他处理器访问,内核等等自旋锁释放期间会重复检查是否获得锁,不会进入休眠状态,所谓“自旋”的形象描述;
需要注意:
1、自旋锁获得后不释放系统将变得不可用,会进入死循环,产生死锁;
2、自旋锁块不应该长期持有,影响性能;
3、需要保证自旋锁保护的代码绝对不会进入睡眠;
信号量:
1、进程执行危险代码段时前,需要对信号量做 down(减1)操作,如果此时已经为0,说明已经有进程再执行危险代码了,此时该进程进入阻塞休眠状态(和自旋锁不同),直到前一个进程退出执行Up操作,调度器无法中断执行down操作的过程,属于原子操作;
2、缺点:内核开销大,影响性能;
3、数据结构:
count:指定可同时处于信号量保护临界区进程数目;
sleepers:指定等待运行进入临界区进程数目,等待的进程会进入睡眠,直到信号量释放才被唤醒;
wait:实现一个队列,保存所有信号量上睡眠的进程的task_struct
4、用法:
内核提供了简化宏定义接口:DECLARE_MUTEX,声明一个二值信号量.
值得注意的是,最新3.18版本内核已经使用互斥锁代替.
3、RCU机制:
rcu :read-copy-update 读-复制-更新
原理:数据结构将要改变时,先创建一个新副本,在副本中修改,所有访问者结束对旧副本读取后,指针指向新副本。
使用实例:
假设ptr指针指向被rcu保护的数据结构,那么需要被以下高亮部分代码保护起来:
dev->ip6_ptr 是被rcu保护的数据结构指针.
资源释放函数:
synchronize_rcu(); 该函数后释放所有关联资源是安全的;
call_rcu(struct rcu_head *head, void (*func)(struct rcu_head *rcu)) 用于注册一个函数在所有资源访问完成后回调。
rcu机制可以保护链表,接口函数是在原有的内核标准链表函数直接加上 _rcu后缀即可,列举:
<rculist.h>
4、内存和优化屏障 barrier
rmb() 读访问内存屏障,它保证该函数前的所有读操作不可乱序执行(流水线指令重排);
wmb() 写访问内存屏障,原理类似
mb() 合并了rmb() + wmb() 功能;
这是Linux kernel层面上的屏障接口,向下对接各种体系结构CPU具体的接口,比如ARMv7以后架构:
5、读者/写者锁
任何并发处理器可以对数据结构经常读操作,但只有一个处理器可以进行写操作。
读:read_lock /read_unlock, 允许临界区内任意数目的读进程并发访问;
写:write_lock/write_unlock, 内核只允许一个写进程处于临界区访问;
6、大内核锁 BKL
可以锁住整个内核,非常影响性能,最新3.18版本内核已将它弃用,成为了历史;
7、互斥量 <mutex.h> (互斥锁)
和信号量互斥原理类似,定义方法分为静态和动态两种:
1、静态定义: 使用 DEFINE_MUTEX 产生;
2、动态定义:mutex_init() ,在运行时产生;
mutex_lock/mutex_unlock 用于上锁和解锁
实时互斥量 rt_mutex,用于解决优先级反转问题,最新内核3.18代码好像已经弃用或者新机制代替。
标准函数:
rt_mutex_init
rt_mutex_lock
rt_mutex_unlock
*一个很重要很常见的数据结构:container_of(ptr,type,member)
这个数据结构在内核代码中大量被使用,初看起来数据结构十分复杂,但是只要明白了原理就很好理解了.
ptr指针的类型和member是类似的,type是包含member成员类型的容器,
这个宏定义的作用简单理解就是:给出一个结构实例,通过计算该结构在所在容器的内存偏移,反向找到所在容器的首地址。
找出ptr指向实例本身所在的容器结构首地址,结构类型是type,比如以下代码:
找出napi数据结构所在容器结构的首地址.
进程间通信
1、System V 机制
类型 : 信号量(和上面的信号量锁没有任何关系)、消息队列、共享内存;
主要用于两个不同进程间临界区的代码保护,实现“通信”.
信号灯(信号量IPC另外叫法)与其他进程间通信方式不大相同,它主要提供对进程间共享资源访问控制机制。相当于内存中的标志,进程可以根据它判定是否能够访问某些共享资源,同时,进程也可以修改该标志。除了用于访问控制外,还可用于进程同步。信号灯有以下两种类型:
- 二值信号灯:最简单的信号灯形式,信号灯的值只能取0或1,类似于互斥锁。
注:二值信号灯能够实现互斥锁的功能,但两者的关注内容不同。信号灯强调共享资源,只要共享资源可用,其他进程同样可以修改信号灯的值;互斥锁更强调进程,占用资源的进程使用完资源后,必须由进程本身来解锁。
- 计算信号灯:信号灯的值可以取任意非负值(当然受内核本身的约束)。
(http://www.cnblogs.com/andtt/articles/2136279.html)
数据结构:
使用几个数据结构建立网状结构,负责将信号量与等待进程关联起来
struct ipc_namespace {
...
struct ipc_ids *ids[3]; //0 ->共享内存,1->信号量,2->消息队列;
...
}
ipc_namespace 默认由以下实例实现:
ipc_ids:
in_use: 当前使用中ipc对象的数目;
seq: 用于产生ipc ID,内核通过ID来管理ipc对象;
rwsem:内核信号量(锁)
ipcs_idr 用于关联kern_ipc_perm 实例指针,每个ipc对象都由kern_ipc_perm一个实例表示;
数据结构网状关系图:
从 静态ipc_namespace对象的ipc_ids找到对应的ID到映射的指针,找到kern_ipc_perm实例,通过 结构->容器方法(container_of)找到该对象
所在容器对象的sem_array结构的首地址,sem_base保存了当前信号量的值和上一次访问它的进程id,sem_pending 指向sem_queue链表,而该sem_queue对象来之各个进程. 3.18内核已经改掉了这个关系图,以上作为理解参考。
获取ipc对象的系统调用关系图:
3.18 内核代码:
1 SYSCALL_DEFINE3(semget, key_t, key, int, nsems, int, semflg) 2 { 3 struct ipc_namespace *ns; 4 static const struct ipc_ops sem_ops = { 5 .getnew = newary, //创建新的sem ipc实例函数接口 6 .associate = sem_security, 7 .more_checks = sem_more_checks, 8 }; 9 struct ipc_params sem_params; 10 11 ns = current->nsproxy->ipc_ns; //获取当前task_struct的ipc命名空间 12 13 if (nsems < 0 || nsems > ns->sc_semmsl) 14 return -EINVAL; 15 16 sem_params.key = key; 17 sem_params.flg = semflg; 18 sem_params.u.nsems = nsems; 19 20 return ipcget(ns, &sem_ids(ns), &sem_ops, &sem_params); 21 }
int ipcget(struct ipc_namespace *ns, struct ipc_ids *ids, const struct ipc_ops *ops, struct ipc_params *params) { if (params->key == IPC_PRIVATE) return ipcget_new(ns, ids, ops, params); //如果没有就创建一个ipc实例 else return ipcget_public(ns, ids, ops, params); // 否则直接根据ipc_ids 查找到实例 }
static int newary(struct ipc_namespace *ns, struct ipc_params *params) { int id; int retval; struct sem_array *sma; ... size = sizeof(*sma) + nsems * sizeof(struct sem); sma = ipc_rcu_alloc(size); //创建新的sma实例 ... sma->sem_base = (struct sem *) &sma[1]; for (i = 0; i < nsems; i++) { INIT_LIST_HEAD(&sma->sem_base[i].pending_alter); //将信号量集合sem_base 加入到链表中 INIT_LIST_HEAD(&sma->sem_base[i].pending_const); spin_lock_init(&sma->sem_base[i].lock); } sma->complex_count = 0; INIT_LIST_HEAD(&sma->pending_alter); //将待决信号量集合sem_base 加入到链表中 INIT_LIST_HEAD(&sma->pending_const); INIT_LIST_HEAD(&sma->list_id); sma->sem_nsems = nsems; sma->sem_ctime = get_seconds(); id = ipc_addid(&sem_ids(ns), &sma->sem_perm, ns->sc_semmni); //初始化kern_ipc_perm 对象等. .... ns->used_sems += nsems; //信号量数目++ .... }
2、消息队列.
进程间交换消息,使用内核的消息队列机制实现。
产生消息,并把消息写入队列的进程称为发送者(sender) ,一个或者多个其他进程则从队列中获取消息,称为接收者(recevier)
简图:进程A发生消息到msg_queue中,进程B,C从 msg_queue中获取消息.
数据结构:
struct msg_queue { struct kern_ipc_perm q_perm; time_t q_stime; /* last msgsnd time */ time_t q_rtime; /* last msgrcv time */ time_t q_ctime; /* last change time */ unsigned long q_cbytes; /* current number of bytes on queue */ unsigned long q_qnum; /* number of messages in queue */ unsigned long q_qbytes; /* max number of bytes on queue */ pid_t q_lspid; /* pid of last msgsnd */ pid_t q_lrpid; /* last receive pid */ struct list_head q_messages; // 消息本身内容链表 struct list_head q_receivers; // 由于管理睡眠的接受者 struct list_head q_senders; // 由于管理睡眠的发送者 };
q_messages 中各个消息封装在一个msg_msg实例中:
struct msg_msg { struct list_head m_list; long m_type; size_t m_ts; /* message text size */ struct msg_msgseg *next; void *security; /* the actual message follows immediately */ };
q_senders 中链表元素是msg_sender 数据结构:
struct msg_sender { struct list_head list; struct task_struct *tsk; };
q_receivers 中链表元素:
1 struct msg_receiver { 2 struct list_head r_list; //用于关联自身链表 3 struct task_struct *r_tsk; //指向对应进程的指针 4 5 int r_mode; 6 long r_msgtype; 7 long r_maxsize; 8 9 /* 10 * Mark r_msg volatile so that the compiler 11 * does not try to get smart and optimize 12 * it. We rely on this for the lockless 13 * receive algorithm. 14 */ 15 struct msg_msg *volatile r_msg; //保持具体的消息内容. 16 };
消息队列涉及数据结构关系图:(省去睡眠的发生消息进程链表)
3.18 内核源代码分析
发送消息系统调用入口:
1 SYSCALL_DEFINE4(msgsnd, int, msqid, struct msgbuf __user *, msgp, size_t, msgsz, 2 int, msgflg) 3 { 4 long mtype; 5 6 if (get_user(mtype, &msgp->mtype)) 7 return -EFAULT; 8 return do_msgsnd(msqid, mtype, msgp->mtext, msgsz, msgflg); 9 }
接收消息系统调用入口:
1 long do_msgsnd(int msqid, long mtype, void __user *mtext, 2 size_t msgsz, int msgflg) 3 { 4 struct msg_queue *msq; 5 struct msg_msg *msg; 6 int err; 7 struct ipc_namespace *ns; 8 9 ns = current->nsproxy->ipc_ns; 10 11 ... 12 for (;;) { 13 struct msg_sender s; 14 15 ... 16 ss_add(msq, &s); //添加s到msq->q_senders 队列 17 18 ... 19 20 ss_del(&s); //删掉 mss->list 21 22 ... 23 } 24 ... 25 if (!pipelined_send(msq, msg)) { // 发送 26 /* no one is waiting for this message, enqueue it */ 27 list_add_tail(&msg->m_list, &msq->q_messages); 28 msq->q_cbytes += msgsz; 29 msq->q_qnum++; 30 atomic_add(msgsz, &ns->msg_bytes); 31 atomic_inc(&ns->msg_hdrs); 32 } 33 34 ... 35 } 36 37 SYSCALL_DEFINE5(msgrcv, int, msqid, struct msgbuf __user *, msgp, size_t, msgsz, 38 long, msgtyp, int, msgflg) 39 { 40 return do_msgrcv(msqid, msgp, msgsz, msgtyp, msgflg, do_msg_fill); 41 }
3、共享内存.
使得多个进程可以访问同一块内存空间, 是最快的可用IPC形式。 是针对其他通信机制运行效率低而设计的。 往往与其他通信机制, 如信号量结合使用, 来达到进程间的同步及互斥。
共享内存允许两个或多个进程共享一定的内存区, 因为不需要拷贝数据, 所以这是最快的一种IPC,binder机制就是使用共享内存的方式实现。
内核的实现和前面两种对象非常类似,与信号量和消息队列相比,共享内存没有本质区别,流程很类似.相关数据结构如下图:
1、应用程序请求的IPC对象,可以通过魔数和当前的命名空间的内核ID访问;
2、对内存的访问可能受到权限系统的限制;
3、可以使用system call分配和IPC对象关联的内存,具备适当授权的所有进程都可以访问该内存.
以下是用户层接口:
#include <sys/ipc.h>
#include <sys/shm.h>
(1)创建或访问共享内存
* int shmget(key_t key,size_t size,int shmflg);
(2)附加共享内存到进程的地址空间
* void *shmat(int shmid,const void *shmaddr,int shmflg);//shmaddr通常为NULL, 由系统选择共享内存附加的地址;shmflg可以为
SHM_RDONLY
(3)从进程的地址空间分离共享内存
* int shmdt(const void *shmaddr); //shmaddr是shmat()函数的返回值
(4)控制共享内存
* int shmctl(int shmid,int cmd,struct shmid_ds *buf);
* struct shmid_ds{
struct ipc_perm shm_perm;
…
};
cmd的常用取值有:(a)IPC_STAT获取当前共享内存的shmid_ds结构并保存在buf中(2)IPC_SET使用buf中的值设置当前共享内存的
shmid_ds结构(3)IPC_RMID删除当前共享内存
4、管道、信号、套接字(socket
...
相关文章推荐
- Professional Linux Kernel Architecture 笔记 —— 中断处理(Part 2)【转】
- 《Professional Linux Kernel Architecture》读书笔记 (1)
- How SMP schedule work in Linux kernel? (ARM architecture)
- 《Understanding the Linux kernel》学习笔记 Chapter 13: I/O Architecture and Device Drivers
- Conceptual Architecture of the Linux Kernel(ZT)
- Linux Kernel architecture for device drivers
- Linux核心 (The Linux Kernel)
- Linux--a free unix-386 kernel(author:Linus Torvalds)
- Linux Kernel Threads in Device Drivers
- Linux Kernel Configuration
- PHLAK - Professional Hacker Linux Assault Kit
- Linux 上实现双向进程间通信管道
- Feature: High Memory In The Linux Kernel
- LINICE2.6 - Linux Kernel Debugger
- A small trail through the Linux kernel(ZT)
- LINUX--a free unix-386 kernel
- SUSE LINUX Professional 9.2 功能和优点
- Linux Kernel Makefiles(转)
- Linux环境进程间通信(二):信号(下)
- summery of adding system calls to linux kernel