您的位置:首页 > 运维架构 > Linux

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 内核代码:

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

...
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: