Linux内核---锁与进程间通信

概述
Linux作为多任务操作系统,能够同时运行多个进程。通常,各个进程必须尽可能保持独立,避免彼此干扰。这对于保护数据和系统稳定性都很有必要。但有时候,应用程序必须彼此通信。
举例来说:
一个进程的数据传输到另一个进程时
数据由多个进程共享时
进程必须彼此等待时
需要协调资源的使用时
如果几个进程共享一个资源,则很容易彼此干扰,必须防止这种情况。因此内核不仅提供了共享数据的机制,同时提供了协调对数据访问的机制。
用户空间应用程序和内核自身都需要保护资源,特别是后者。在SMP系统上,各个CPU可能同时处于核心态,在理论上可以操作所有现存数据结构。为阻止CPU之间的相互干扰,需要通过锁保护一定的范围。锁可以确保每次只能有一个CPU访问被保护的范围。

解决思路
几个进程在访问资源时彼此干扰的情况通常称之为竞态条件(race condition),当出行竞态条件后,其问题的本质是,进程的执行在不应该的地方被中断,从而导致进程工作的不正确。显然,一种可能的解决方案是标记出相关的代码段,使之无法被调度器中断。尽管这种方法原则上是可行的,但是有几个内在问题。在某种情况下,有问题的程序可能迷失在标记代码中无法退出,因此无法放弃CPU,进而导致计算机不可用。因此我们必须立即放弃这种解决方案。
问题的解决方案不一定必须要求临界区不可中断的。只要没有其他的进程进入临界区,那么在临界区中执行的进程完全可以中断的。这种严格的禁止条件,可以确保几个进程不能同时改变共享的值,我们称之为互斥。也就是在给定时刻,只有一个进程可以进入临界区。
下面讨论大多数系统采用的方案:
信号量
信号量(semaphore)是由E. W. Dijkstra在1965年设计。初看起来,它们对各种进程间通信问题提供了一种简单得令人吃惊的解答,但对信号量的使用仍需要经验、直觉和谨慎。
实质上,信号量只是受保护的特别变量,能够表示为正负整数。其初始值为1。
为操作信号量定义了两个标准操作:up和down。这两个操作分别用于控制关键代码范围的进入和退出,且假定相互竞争的进程访问信号量机会均等。
在一个进程想要进入关键代码时,它调用down函数。这会将信号量的值减1,即将其设置为0,然后执行危险代码段。在执行完操作之后,调用up函数将信号量的值加1,即重置为初始值。信号量有下面两种特性。
(1) 又一个进程试图进入关键代码段时,首先也必须对信号量执行down操作。因为第1个进程已经进入该代码段,信号量的值此时为0。这导致第2个进程在该信号量上“睡眠”。换句话说,它会一直等待,直至第1个进程退出相关的代码。
在执行down操作时,有一点特别重要。即从应用程序的角度来看,该操作应视为一个原子操作。它不能被调度器调用中断,这意味着竞态条件是无法发生的。
从内核视角来看,查询变量的值和修改变量的值是两个不同的操作,但用户将二者视为一个原子操作。当进程在信号量上睡眠时,内核将其置于阻塞状态,且与其他在该信号量上等待的进程一同放到一个等待列表中。
(2) 在进程退出关键代码段时,执行up操作。这不仅会将信号量的值加1(恢复为1),而且还会选择一个在该信号量上睡眠的进程。该进程在恢复执行后,完成down操作将信号量减1(变为0),此后即可安全地开始执行关键代码。
如果没有内核的支持,这个过程是不可能的,因为用户空间库无法保证down操作不被中断。在讲解对应函数的实现之前,首先必须讨论内核自身用于保护关键代码段的机制。这些机制是用户程序使用保护措施的基础。
信号量在用户层可以正常工作,原则上也可以用于解决内核内部的各种锁问题。但事实上不是这样:性能是内核最首先的一个目标,虽然信号量初看起来容易实现,但其开销对内核来说过大。这也是内核中提供了许多不同的锁和同步机制的原因,这些我将在下文讨论。
内核锁机制
内核可以不受限制地访问整个地址空间。在多处理器系统上(或类似地,在启用了内核抢占的单处理器系统上,可参见第2章),这会引起一些问题。如果几个处理器同时处于核心态,则理论上它们可以同时访问同一个数据结构,这刚好造成了前一节讲述的问题。
在第一个提供了SMP功能的内核版本中,该问题的解决方案非常简单,即每次只允许一个处理器处于核心态。因此,对数据未经协调的并行访问被自动排除了。令人遗憾的是,该方法因为效率不高,很快被废弃了。
现在,内核使用了由锁组成的细粒度网络,来明确地保护各个数据结构。如果处理器A在操作数据结构X,则处理器B可以执行任何其他的内核操作,但不能操作X。内核为此提供了各种锁选项,分别优化不同的内核数据使用模式。
**原子操作:**这些是最简单的锁操作。它们保证简单的操作,诸如计数器加1之类的操作,可以不中断的原子执行。即使操作由几个简单的汇编语句组成,也可以保证。
**自旋锁:**最常用的锁操作。它们用于短期的保护某段代码,以防止其他处理器的访问。在内核等待自旋锁释放时,会反复检查能否获取锁,而不会进入睡眠状态(忙等待)。当然如果等待时间过长,则效率不高。
**信号量:**这些是用最经典的方法实现的。在等待信号量释放时,内核进入睡眠状态,直至被唤醒。唤醒后,内核才尝试获取信号量。互斥量是信号量的特里,互斥量保护的临界区,每次只能有一个用户进入。
**读者/写者锁:**这些锁会区分用户对数据结构两种不同类型的访问,任何数据的处理器都能并发的对数据进行读访问,但只有一个处理器只能进行写访问。事实上,在进行写访问时,读访问是无法进行的。

	**自旋锁**
	自旋锁用于处理器之间的互斥,适合保护很短的临界区,并且不允许在临界区睡眠。申请自旋锁的时候,如果自旋锁被其他处理器占有,该 处理器自旋等待(也称为忙等待)。若进程、软中断和硬件中断都可以使用自旋锁。目前内核的自旋锁是排队自旋锁(queued spinlock,也称为"FIFO ticket spinlock"),核心算法类似银行柜台排队叫号。
	**Linux内核自旋锁源码定义如下:**
typedef struct spinlock {
	union {
		struct raw_spinlock rlock;

#ifdef CONFIG_DEBUG_LOCK_ALLOC
# define LOCK_PADSIZE (offsetof(struct raw_spinlock, dep_map))
		struct {
			u8 __padding[LOCK_PADSIZE];
			struct lockdep_map dep_map;
		};
#endif
	};
} spinlock_t;
typedef struct raw_spinlock {
	arch_spinlock_t raw_lock;
#ifdef CONFIG_GENERIC_LOCKBREAK
	unsigned int break_lock;
#endif
#ifdef CONFIG_DEBUG_SPINLOCK
	unsigned int magic, owner_cpu;
	void *owner;
#endif
#ifdef CONFIG_DEBUG_LOCK_ALLOC
	struct lockdep_map dep_map;
#endif
} raw_spinlock_t;

b、处理器架构都要定义自己的数据类型arch_spinlock_t,如下ARM64架构定义如下:

typedef struct arch_spinlock {
	/* Next ticket number to hand out. */
	int next_ticket;
	/* The ticket number that currently owns this lock. */
	int current_ticket;
} arch_spinlock_t;

c、Linux内核自旋锁申请/释放常用函数如下:

spin_lock/spin_lock_bh/spin_trylock/spin_lock_irq
spin_unlock/spin_unlock_bh/spin_unlock_irq/spin_unlock_irqrestore

d、在多处理器系统当中,函数spin_lock负责申请自旋锁,其内核源码如下(arm64架构):

static inline void arch_spin_lock(arch_spinlock_t *lock)
{
	unsigned int tmp;
	arch_spinlock_t lockval, newval;

	asm volatile(
	/* Atomically increment the next ticket. */
	ARM64_LSE_ATOMIC_INSN(
	/* LL/SC */
"	prfm	pstl1strm, %3\n"
"1:	ldaxr	%w0, %3\n"
"	add	%w1, %w0, %w5\n"
"	stxr	%w2, %w1, %3\n"
"	cbnz	%w2, 1b\n",
	/* LSE atomics */
"	mov	%w2, %w5\n"
"	ldadda	%w2, %w0, %3\n"
	__nops(3)
	)

	/* Did we get the lock? */
"	eor	%w1, %w0, %w0, ror #16\n"
"	cbz	%w1, 3f\n"
	/*
	 * No: spin on the owner. Send a local event to avoid missing an
	 * unlock before the exclusive load.
	 */
"	sevl\n"
"2:	wfe\n"
"	ldaxrh	%w2, %4\n"
"	eor	%w1, %w2, %w0, lsr #16\n"
"	cbnz	%w1, 2b\n"
	/* We got the lock. Critical section starts here. */
"3:"
	: "=&r" (lockval), "=&r" (newval), "=&r" (tmp), "+Q" (*lock)
	: "Q" (lock->owner), "I" (1 << TICKET_SHIFT)
	: "memory");
}

函数spin_unlock负责释放自旋锁,其内核源码如下:


static inline void arch_spin_unlock(arch_spinlock_t *lock)
{
	unsigned long tmp;

	asm volatile(ARM64_LSE_ATOMIC_INSN(
	/* LL/SC */
	"	ldrh	%w1, %0\n"
	"	add	%w1, %w1, #1\n"
	"	stlrh	%w1, %0",
	/* LSE atomics */
	"	mov	%w1, #1\n"
	"	staddlh	%w1, %0\n"
	__nops(1))
	: "=Q" (lock->owner), "=&r" (tmp)
	:
	: "memory");
}

e、读写自旋锁
读写自旋锁(又称为读写锁)是对自旋锁的改进,区分读者和写者,允许多个读者同时进入临界区,读者和写者互斥,写者和写者互斥。如果读者占用读锁,写者申请写锁的时候自旋等待。如果写者占有写锁,读者申请读锁的时候自旋等待。
读写自旋锁内核源码定义定义如下:

/*
 * include/linux/rwlock_types.h - generic rwlock type definitions
 *				  and initializers
 *
 * portions Copyright 2005, Red Hat, Inc., Ingo Molnar
 * Released under the General Public License (GPL).
 */
typedef struct {
	arch_rwlock_t raw_lock;
#ifdef CONFIG_GENERIC_LOCKBREAK
	unsigned int break_lock;
#endif
#ifdef CONFIG_DEBUG_SPINLOCK
	unsigned int magic, owner_cpu;
	void *owner;
#endif
#ifdef CONFIG_DEBUG_LOCK_ALLOC
	struct lockdep_map dep_map;
#endif
} rwlock_t;

不同处理器架构都要自己定义数据类型arch_rwlock_t,ARM64架构定义如下:

typedef struct {
	volatile unsigned int lock;
} arch_rwlock_t;

2、互斥锁
实时互斥量是内核支持的另一种形式的互斥量。它们需要在编译时通过配置选项CONFIG_RT_MUTEX显式启用。与普通的互斥量相比,它们实现了优先级继承(priority inheritance),该特性可用于解决(或在最低限度上缓解)优先级反转的影响。二者在大多数操作系统教科书中一般都有讨论。
考虑一种情况,系统上有两个进程运行:进程A优先级高,进程C优先级低。假定进程C已经获取了一个互斥量,正在所保护的临界区中运行,且在短时间内不打算退出。但在进程C进入临界区之后不久,进程A也试图获取保护临界区的互斥量。由于进程C已经获取该互斥量,因而进程A必须等待。这导致高优先级的进程A等待低优先级的进程C。
如果有第3个进程B,优先级介于进程A和进程C之间,情况会更加糟糕。假定进程C仍然持有锁,进程A在等待。 现在进程B开始运行。由于它的优先级高于进程C,因此可以抢占进程C。但它实际上也抢占了进程A,尽管进程A的优先级高于进程B。 如果进程B继续运行,那么它可以让进程A等待更长时间,因为进程C被进程B抢占,所以它只能更慢地完成其操作。因此看起来仿佛进程B的优先级高于进程A一样。这种糟糕的情况称为无限制优先级反转(unbounded priority inversion)。
该问题可以通过优先级继承解决。如果高优先级进程阻塞在互斥量上,该互斥量当前由低优先级进程持有,那么进程C的优先级(在我们的例子中)临时提高到进程A的优先级。 如果进程B现在开始运行,只能得到与进程A竞争情况下的CPU时间,从而理顺了优先级的问题。
互斥锁只允许一个进程进入临界区,适合保护比较长的临界区,因为竞争互斥锁时进程可能睡眠和再次唤醒,
代价很高。尽管可以把二值信号当作互斥锁使用,但是内核单独实现互斥锁,内核源码的互斥锁定义如下:

/*
 * Simple, straightforward mutexes with strict semantics:
 *
 * - only one task can hold the mutex at a time
 * - only the owner can unlock the mutex
 * - multiple unlocks are not permitted
 * - recursive locking is not permitted
 * - a mutex object must be initialized via the API
 * - a mutex object must not be initialized via memset or copying
 * - task may not exit with mutex held
 * - memory areas where held locks reside must not be freed
 * - held mutexes must not be reinitialized
 * - mutexes may not be used in hardware or software interrupt
 *   contexts such as tasklets and timers
 *
 * These semantics are fully enforced when DEBUG_MUTEXES is
 * enabled. Furthermore, besides enforcing the above rules, the mutex
 * debugging code also implements a number of additional features
 * that make lock debugging easier and faster:
 *
 * - uses symbolic names of mutexes, whenever they are printed in debug output
 * - point-of-acquire tracking, symbolic lookup of function names
 * - list of all locks held in the system, printout of them
 * - owner tracking
 * - detects self-recursing locks and prints out all relevant info
 * - detects multi-task circular deadlocks and prints out all affected
 *   locks and tasks (and only those tasks)
 */
struct mutex {
	atomic_long_t		owner;
	spinlock_t		wait_lock;
#ifdef CONFIG_MUTEX_SPIN_ON_OWNER
	struct optimistic_spin_queue osq; /* Spinner MCS lock */
#endif
	struct list_head	wait_list;
#ifdef CONFIG_DEBUG_MUTEXES
	void			*magic;
#endif
#ifdef CONFIG_DEBUG_LOCK_ALLOC
	struct lockdep_map	dep_map;
#endif
};
	互斥量的所有者通过owner指定,wait_lock提供实际的保护。所有等待的进程都在wait_list中排队。与普通互斥量相比,决定性的改变是等待列表中的进程按优先级排序。在等待列表改变时内核可相应地校正锁持有者的优先级。这需要到调度器的一个接口,可由函数rt_mutex_setprio提

供。

四、消息队列
消息队列是消息的链接表,包括 Posix 消息队列和 System V 消息队列。消息队列克服了信号承载信息量少、管道只能承载无格式字节流以及缓冲区大小受限等缺点,克服了早期 Lunix 通信机制的一些缺点。消息队列将消息看作一个记录,具有特定的格式以及特定的优先级,对消息队列有写权限的进程可以向中按照一定的规则添加新消息;对消息队列有读权限的进程则可以从消息队列中读取消息,消息队列是随内核持续的。
消息队列msg_queue数据结构:

/* one msq_queue structure for each present queue on the system */
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;
};

2、系统调用定义
在程序上层可以直接调用msgsnd(msqid,&msgs,sizeof(struct msgstru),IPC_NOWAIT) 这样的形式来发送消
息,但是在底层是用以下的形式来调用 :

SYSCALL_DEFINE4(msgsnd, int, msqid, struct msgbuf __user *, msgp, size_t, msgsz,
		int, msgflg)
{
	long mtype;

	if (get_user(mtype, &msgp->mtype))
		return -EFAULT;
	return do_msgsnd(msqid, mtype, msgp->mtext, msgsz, msgflg);
}

	对于 SYSCALL_DEFINE4,首个变量用于函数名,剩下的偶数对参数,依次代表参数类型与参数变量。SYSCALL_DEFINEx,随后的 x 就是对于不同的参数的个数。
	消息队列系统调用:
asmlinkage long sys_msgget(key_t key, int msgflg);
asmlinkage long sys_msgsnd(int msqid, struct msgbuf __user *msgp,
				size_t msgsz, int msgflg);
asmlinkage long sys_msgrcv(int msqid, struct msgbuf __user *msgp,
				size_t msgsz, long msgtyp, int msgflg);
asmlinkage long sys_msgctl(int msqid, int cmd, struct msqid_ds __user *buf);

a.msgget 函数
得到消息队列标识符或创建一个消息队列对象并返回消息队列标识符。
b.msgsnd 函数
将消息写入到消息队列。
c.msgrcv 函数
从消息队列读取消息。
d.msgctl函数
获取和设置消息队列的属性。

五、共享内存(原理机制)
共享内存就是允许两个或多个进程共享一定的存储区。就如同 malloc() 函数向不同进程返回了指向同一个物理内存区域的指针。当一个进程改变这块地址中内容的时候,其它进程都会察觉到这个更改。因为数据不需要在客户机和服务器端之间复制,数据直接写到内存,不用若干次数据拷贝,所以这是最快的一种IPC。备注:共享内存没有任何的同步与互斥机制,所以要使用信号量来实现对共享内存的存取的同步。共享内存是IPC通信中传输速度最快的通信方式没有之一。
共享内存原理结构图如下:
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值