导航:首页 > 操作系统 > linuxpreempt

linuxpreempt

发布时间:2024-07-02 09:44:53

linux 中断 下半部 处理时间过长 怎么办

一、中断处理为什么要下半部?
Linux在中断处理中间中断处理分了上半部和下半部,目的就是提高系统的响应能力和并发能力。通俗一点来讲:当一个中断产生,调用该中断对应的处理程序(上半部)然后告诉系统,对应的后半部可以执行了。然后中断处理程序就返回,下半部会在合适的时机有系统调用。这样一来就大大的减少了中断处理所需要的时间。

二、那些工作应该放在上半部,那些应该放在下半部?
没有严格的规则,只有一些提示:
1、对时间非常敏感,放在上半部。
2、与硬件相关的,放在上半部。
3、不能被其他中断打断的工作,放在上半部。
以上三点之外的,考虑放在下半部。

三、下半部机制在Linux中是怎么实现的?
下半部在Linux中有以下实现机制:
1、BH(在2.5中删除)
2、任务队列(task queue,在2.5删除)
3、软中断(softirq,2.3开始。本文重点)
4、tasklet(2.3开始)
5、工作队列(work queue,2.5开始)

四、软中断是怎么实现的(以下代码出自2.6.32)?
软中断不会抢占另外一个软中断,唯一可以抢占软中断的是中断处理程序。
软中断可以在不同CPU上并发执行(哪怕是同一个软中断)

1、软中断是编译期间静态分配的,定义如下:
struct softirq_action { void (*action)(struct softirq_action *); };

/*
* PLEASE, avoid to allocate new softirqs, if you need not _really_ high
* frequency threaded job scheling. For almost all the purposes
* tasklets are more than enough. F.e. all serial device BHs et
* al. should be converted to tasklets, not to softirqs.
*/
enum {
HI_SOFTIRQ=0,
TIMER_SOFTIRQ,
NET_TX_SOFTIRQ,
NET_RX_SOFTIRQ,
BLOCK_SOFTIRQ,
BLOCK_IOPOLL_SOFTIRQ,
TASKLET_SOFTIRQ,
SCHED_SOFTIRQ,
HRTIMER_SOFTIRQ,
RCU_SOFTIRQ, /* Preferable RCU should always be the last softirq */
NR_SOFTIRQS
};

/*
* map softirq index to softirq name. update 'softirq_to_name' in * kernel/softirq.c when adding a new softirq.
*/
extern char *softirq_to_name[NR_SOFTIRQS];

static struct softirq_action softirq_vec[NR_SOFTIRQS] __cacheline_aligned_in_smp;

说明:
(1)、软中断的个数书上说是32,看来到这个版本已经发生变化了。
(2)、void (*action)(struct softirq_action *);传递整个结构体指针在于当结构体成员发生变化是,接口不变。

2、系统执行软中断一个注册的软中断必须被标记后才会执行(触发软中断),通常中断处理程序会在返回前标记它的软中断。在下列地方,待处理的软中断会被执行:
(1)、从一个硬件中断代码处返回。
(2)、在ksoftirqd内核线程。
(3)、在那些显示检查和执行待处理的软中断代码中。

ksoftirqd说明:
每个处理器都有一个这样的线程。所有线程的名字都叫做ksoftirq/n,区别在于n,它对应的是处理器的编号。在一个双CPU的机器上就有两个这样的线程,分别叫做ksoftirqd/0和ksoftirqd/1。为了保证只要有空闲的处理器,它们就会处理软中断,所以给每个处理器都分配一个这样的线程。

执行软中断的代码如下:
asmlinkage void __do_softirq(void)
{
struct softirq_action *h;
__u32 pending;
int max_restart = MAX_SOFTIRQ_RESTART;
int cpu;

pending = local_softirq_pending();
account_system_vtime(current);

__local_bh_disable((unsigned long)__builtin_return_address(0));
lockdep_softirq_enter();

cpu = smp_processor_id();
restart:
/* Reset the pending bitmask before enabling irqs */
set_softirq_pending(0);

local_irq_enable();

h = softirq_vec;

do {
if (pending & 1) {
int prev_count = preempt_count();
kstat_incr_softirqs_this_cpu(h - softirq_vec);

trace_softirq_entry(h, softirq_vec);
h->action(h);
trace_softirq_exit(h, softirq_vec);
if (unlikely(prev_count != preempt_count())) {
printk(KERN_ERR "huh, entered softirq %td %s %p"
"with preempt_count %08x,"
" exited with %08x?\n", h - softirq_vec,
softirq_to_name[h - softirq_vec],
h->action, prev_count, preempt_count());
preempt_count() = prev_count;
}

rcu_bh_qs(cpu);
}
h++;
pending >>= 1;
} while (pending);

local_irq_disable();

pending = local_softirq_pending();
if (pending && --max_restart)
goto restart;

if (pending)
wakeup_softirqd();

lockdep_softirq_exit();

account_system_vtime(current);
_local_bh_enable();
}

3、编写自己的软中断
(1)、分配索引,在HI_SOFTIRQ与NR_SOFTIRQS中间添加自己的索引号。
(2)、注册处理程序,处理程序:open_softirq(索引号,处理函数)。
(3)、触发你的软中断:raise_softirq(索引号)。

4、软中断处理程序注意
(1)、软中断处理程序执行的时候,允许响应中断,但自己不能休眠。
(2)、如果软中断在执行的时候再次触发,则别的处理器可以同时执行,所以加锁很关键。

㈡ 如何实现linux下多线程之间的互斥与同步

Linux设备驱动中必须解决的一个问题是多个进程对共享资源的并发访问,并发访问会导致竞态,linux提供了多种解决竞态问题的方式,这些方式适合不同的应用场景。

Linux内核是多进程、多线程的操作系统,它提供了相当完整的内核同步方法。内核同步方法列表如下:
中断屏蔽
原子操作
自旋锁
读写自旋锁
顺序锁
信号量
读写信号量
BKL(大内核锁)
Seq锁
一、并发与竞态:
定义:
并发(concurrency)指的是多个执行单元同时、并行被执行,而并发的执行单元对共享资源(硬件资源和软件上的全局变量、静态变量等)的访问则很容易导致竞态(race conditions)。
在linux中,主要的竞态发生在如下几种情况:
1、对称多处理器(SMP)多个CPU
特点是多个CPU使用共同的系统总线,因此可访问共同的外设和存储器。
2、单CPU内进程与抢占它的进程
3、中断(硬中断、软中断、Tasklet、底半部)与进程之间
只要并发的多个执行单元存在对共享资源的访问,竞态就有可能发生。
如果中断处理程序访问进程正在访问的资源,则竞态也会会发生。
多个中断之间本身也可能引起并发而导致竞态(中断被更高优先级的中断打断)。

解决竞态问题的途径是保证对共享资源的互斥访问,所谓互斥访问就是指一个执行单元在访问共享资源的时候,其他的执行单元都被禁止访问。

访问共享资源的代码区域被称为临界区,临界区需要以某种互斥机制加以保护,中断屏蔽,原子操作,自旋锁,和信号量都是linux设备驱动中可采用的互斥途径。

临界区和竞争条件:
所谓临界区(critical regions)就是访问和操作共享数据的代码段,为了避免在临界区中并发访问,编程者必须保证这些代码原子地执行——也就是说,代码在执行结束前不可被打断,就如同整个临界区是一个不可分割的指令一样,如果两个执行线程有可能处于同一个临界区中,那么就是程序包含一个bug,如果这种情况发生了,我们就称之为竞争条件(race conditions),避免并发和防止竞争条件被称为同步。

死锁:
死锁的产生需要一定条件:要有一个或多个执行线程和一个或多个资源,每个线程都在等待其中的一个资源,但所有的资源都已经被占用了,所有线程都在相互等待,但它们永远不会释放已经占有的资源,于是任何线程都无法继续,这便意味着死锁的发生。

二、中断屏蔽
在单CPU范围内避免竞态的一种简单方法是在进入临界区之前屏蔽系统的中断。
由于linux内核的进程调度等操作都依赖中断来实现,内核抢占进程之间的并发也就得以避免了。
中断屏蔽的使用方法:
local_irq_disable()//屏蔽中断
//临界区
local_irq_enable()//开中断
特点:
由于linux系统的异步IO,进程调度等很多重要操作都依赖于中断,在屏蔽中断期间所有的中断都无法得到处理,因此长时间的屏蔽是很危险的,有可能造成数据丢失甚至系统崩溃,这就要求在屏蔽中断之后,当前的内核执行路径应当尽快地执行完临界区的代码。
中断屏蔽只能禁止本CPU内的中断,因此,并不能解决多CPU引发的竞态,所以单独使用中断屏蔽并不是一个值得推荐的避免竞态的方法,它一般和自旋锁配合使用。

三、原子操作
定义:原子操作指的是在执行过程中不会被别的代码路径所中断的操作。
(原子原本指的是不可分割的微粒,所以原子操作也就是不能够被分割的指令)
(它保证指令以“原子”的方式执行而不能被打断)
原子操作是不可分割的,在执行完毕不会被任何其它任务或事件中断。在单处理器系统(UniProcessor)中,能够在单条指令中完成的操作都可以认为是" 原子操作",因为中断只能发生于指令之间。这也是某些CPU指令系统中引入了test_and_set、test_and_clear等指令用于临界资源互斥的原因。但是,在对称多处理器(Symmetric Multi-Processor)结构中就不同了,由于系统中有多个处理器在独立地运行,即使能在单条指令中完成的操作也有可能受到干扰。我们以decl (递减指令)为例,这是一个典型的"读-改-写"过程,涉及两次内存访问。
通俗理解:
原子操作,顾名思义,就是说像原子一样不可再细分。一个操作是原子操作,意思就是说这个操作是以原子的方式被执行,要一口气执行完,执行过程不能够被OS的其他行为打断,是一个整体的过程,在其执行过程中,OS的其它行为是插不进来的。
分类:linux内核提供了一系列函数来实现内核中的原子操作,分为整型原子操作和位原子操作,共同点是:在任何情况下操作都是原子的,内核代码可以安全的调用它们而不被打断。

原子整数操作:
针对整数的原子操作只能对atomic_t类型的数据进行处理,在这里之所以引入了一个特殊的数据类型,而没有直接使用C语言的int型,主要是出于两个原因:
第一、让原子函数只接受atomic_t类型的操作数,可以确保原子操作只与这种特殊类型数据一起使用,同时,这也确保了该类型的数据不会被传递给其它任何非原子函数;
第二、使用atomic_t类型确保编译器不对相应的值进行访问优化——这点使得原子操作最终接收到正确的内存地址,而不是一个别名,最后就是在不同体系结构上实现原子操作的时候,使用atomic_t可以屏蔽其间的差异。
原子整数操作最常见的用途就是实现计数器。
另一点需要说明原子操作只能保证操作是原子的,要么完成,要么不完成,不会有操作一半的可能,但原子操作并不能保证操作的顺序性,即它不能保证两个操作是按某个顺序完成的。如果要保证原子操作的顺序性,请使用内存屏障指令。
atomic_t和ATOMIC_INIT(i)定义
typedef struct { volatile int counter; } atomic_t;
#define ATOMIC_INIT(i) { (i) }

在你编写代码的时候,能使用原子操作的时候,就尽量不要使用复杂的加锁机制,对多数体系结构来讲,原子操作与更复杂的同步方法相比较,给系统带来的开销小,对高速缓存行的影响也小,但是,对于那些有高性能要求的代码,对多种同步方法进行测试比较,不失为一种明智的作法。

原子位操作:
针对位这一级数据进行操作的函数,是对普通的内存地址进行操作的。它的参数是一个指针和一个位号。

为方便其间,内核还提供了一组与上述操作对应的非原子位函数,非原子位函数与原子位函数的操作完全相同,但是,前者不保证原子性,且其名字前缀多两个下划线。例如,与test_bit()对应的非原子形式是_test_bit(),如果你不需要原子性操作(比如,如果你已经用锁保护了自己的数据),那么这些非原子的位函数相比原子的位函数可能会执行得更快些。

四、自旋锁
自旋锁的引入:
如 果每个临界区都能像增加变量这样简单就好了,可惜现实不是这样,而是临界区可以跨越多个函数,例如:先得从一个数据结果中移出数据,对其进行格式转换和解 析,最后再把它加入到另一个数据结构中,整个执行过程必须是原子的,在数据被更新完毕之前,不能有其他代码读取这些数据,显然,简单的原子操作是无能为力 的(在单处理器系统(UniProcessor)中,能够在单条指令中完成的操作都可以认为是" 原子操作",因为中断只能发生于指令之间),这就需要使用更为复杂的同步方法——锁来提供保护。

自旋锁的介绍:
Linux内核中最常见的锁是自旋锁(spin lock),自旋锁最多只能被一个可执行线程持有,如果一个执行线程试图获得一个被争用(已经被持有)的自旋锁,那么该线程就会一直进行忙循环—旋转—等待锁重新可用,要是锁未被争用,请求锁的执行线程便能立刻得到它,继续执行,在任意时间,自旋锁都可以防止多于一个的执行线程同时进入理解区,注意同一个锁可以用在多个位置—例如,对于给定数据的所有访问都可以得到保护和同步。
一个被争用的自旋锁使得请求它的线程在等待锁重新可用时自旋(特别浪费处理器时间),所以自旋锁不应该被长时间持有,事实上,这点正是使用自旋锁的初衷,在短期间内进行轻量级加锁,还可以采取另外的方式来处理对锁的争用:让请求线程睡眠,直到锁重新可用时再唤醒它,这样处理器就不必循环等待,可以去执行其他代码,这也会带来一定的开销——这里有两次明显的上下文切换, 被阻塞的线程要换出和换入。因此,持有自旋锁的时间最好小于完成两次上下文切换的耗时,当然我们大多数人不会无聊到去测量上下文切换的耗时,所以我们让持 有自旋锁的时间应尽可能的短就可以了,信号量可以提供上述第二种机制,它使得在发生争用时,等待的线程能投入睡眠,而不是旋转。
自旋锁可以使用在中断处理程序中(此处不能使用信号量,因为它们会导致睡眠),在中断处理程序中使用自旋锁时,一定要在获取锁之前,首先禁止本地中断(在 当前处理器上的中断请求),否则,中断处理程序就会打断正持有锁的内核代码,有可能会试图去争用这个已经持有的自旋锁,这样以来,中断处理程序就会自旋, 等待该锁重新可用,但是锁的持有者在这个中断处理程序执行完毕前不可能运行,这正是我们在前一章节中提到的双重请求死锁,注意,需要关闭的只是当前处理器上的中断,如果中断发生在不同的处理器上,即使中断处理程序在同一锁上自旋,也不会妨碍锁的持有者(在不同处理器上)最终释放锁。

自旋锁的简单理解:
理解自旋锁最简单的方法是把它作为一个变量看待,该变量把一个临界区或者标记为“我当前正在运行,请稍等一会”或者标记为“我当前不在运行,可以被使用”。如果A执行单元首先进入例程,它将持有自旋锁,当B执行单元试图进入同一个例程时,将获知自旋锁已被持有,需等到A执行单元释放后才能进入。

自旋锁的API函数:

其实介绍的几种信号量和互斥机制,其底层源码都是使用自旋锁,可以理解为自旋锁的再包装。所以从这里就可以理解为什么自旋锁通常可以提供比信号量更高的性能。
自旋锁是一个互斥设备,他只能会两个值:“锁定”和“解锁”。它通常实现为某个整数之中的单个位。
“测试并设置”的操作必须以原子方式完成。
任何时候,只要内核代码拥有自旋锁,在相关CPU上的抢占就会被禁止。
适用于自旋锁的核心规则:
(1)任何拥有自旋锁的代码都必须使原子的,除服务中断外(某些情况下也不能放弃CPU,如中断服务也要获得自旋锁。为了避免这种锁陷阱,需要在拥有自旋锁时禁止中断),不能放弃CPU(如休眠,休眠可发生在许多无法预期的地方)。否则CPU将有可能永远自旋下去(死机)。
(2)拥有自旋锁的时间越短越好。

需 要强调的是,自旋锁别设计用于多处理器的同步机制,对于单处理器(对于单处理器并且不可抢占的内核来说,自旋锁什么也不作),内核在编译时不会引入自旋锁 机制,对于可抢占的内核,它仅仅被用于设置内核的抢占机制是否开启的一个开关,也就是说加锁和解锁实际变成了禁止或开启内核抢占功能。如果内核不支持抢 占,那么自旋锁根本就不会编译到内核中。
内核中使用spinlock_t类型来表示自旋锁,它定义在:
typedef struct {
raw_spinlock_t raw_lock;
#if defined(CONFIG_PREEMPT) && defined(CONFIG_SMP)
unsigned int break_lock;
#endif
} spinlock_t;

对于不支持SMP的内核来说,struct raw_spinlock_t什么也没有,是一个空结构。对于支持多处理器的内核来说,struct raw_spinlock_t定义为
typedef struct {
unsigned int slock;
} raw_spinlock_t;

slock表示了自旋锁的状态,“1”表示自旋锁处于解锁状态(UNLOCK),“0”表示自旋锁处于上锁状态(LOCKED)。
break_lock表示当前是否由进程在等待自旋锁,显然,它只有在支持抢占的SMP内核上才起作用。
自旋锁的实现是一个复杂的过程,说它复杂不是因为需要多少代码或逻辑来实现它,其实它的实现代码很少。自旋锁的实现跟体系结构关系密切,核心代码基本也是由汇编语言写成,与体协结构相关的核心代码都放在相关的目录下,比如。对于我们驱动程序开发人员来说,我们没有必要了解这么spinlock的内部细节,如果你对它感兴趣,请参考阅读Linux内核源代码。对于我们驱动的spinlock接口,我们只需包括头文件。在我们详细的介绍spinlock的API之前,我们先来看看自旋锁的一个基本使用格式:
#include
spinlock_t lock = SPIN_LOCK_UNLOCKED;

spin_lock(&lock);
....
spin_unlock(&lock);

从使用上来说,spinlock的API还很简单的,一般我们会用的的API如下表,其实它们都是定义在中的宏接口,真正的实现在中
#include
SPIN_LOCK_UNLOCKED
DEFINE_SPINLOCK
spin_lock_init( spinlock_t *)
spin_lock(spinlock_t *)
spin_unlock(spinlock_t *)
spin_lock_irq(spinlock_t *)
spin_unlock_irq(spinlock_t *)
spin_lock_irqsace(spinlock_t *,unsigned long flags)
spin_unlock_irqsace(spinlock_t *, unsigned long flags)
spin_trylock(spinlock_t *)
spin_is_locked(spinlock_t *)

• 初始化
spinlock有两种初始化形式,一种是静态初始化,一种是动态初始化。对于静态的spinlock对象,我们用 SPIN_LOCK_UNLOCKED来初始化,它是一个宏。当然,我们也可以把声明spinlock和初始化它放在一起做,这就是 DEFINE_SPINLOCK宏的工作,因此,下面的两行代码是等价的。
DEFINE_SPINLOCK (lock);
spinlock_t lock = SPIN_LOCK_UNLOCKED;

spin_lock_init 函数一般用来初始化动态创建的spinlock_t对象,它的参数是一个指向spinlock_t对象的指针。当然,它也可以初始化一个静态的没有初始化的spinlock_t对象。
spinlock_t *lock
......
spin_lock_init(lock);

• 获取锁
内核提供了三个函数用于获取一个自旋锁。
spin_lock:获取指定的自旋锁。
spin_lock_irq:禁止本地中断并获取自旋锁。
spin_lock_irqsace:保存本地中断状态,禁止本地中断并获取自旋锁,返回本地中断状态。

自旋锁是可以使用在中断处理程序中的,这时需要使用具有关闭本地中断功能的函数,我们推荐使用 spin_lock_irqsave,因为它会保存加锁前的中断标志,这样就会正确恢复解锁时的中断标志。如果spin_lock_irq在加锁时中断是关闭的,那么在解锁时就会错误的开启中断。

另外两个同自旋锁获取相关的函数是:
spin_trylock():尝试获取自旋锁,如果获取失败则立即返回非0值,否则返回0。
spin_is_locked():判断指定的自旋锁是否已经被获取了。如果是则返回非0,否则,返回0。
• 释放锁
同获取锁相对应,内核提供了三个相对的函数来释放自旋锁。
spin_unlock:释放指定的自旋锁。
spin_unlock_irq:释放自旋锁并激活本地中断。
spin_unlock_irqsave:释放自旋锁,并恢复保存的本地中断状态。

五、读写自旋锁
如 果临界区保护的数据是可读可写的,那么只要没有写操作,对于读是可以支持并发操作的。对于这种只要求写操作是互斥的需求,如果还是使用自旋锁显然是无法满 足这个要求(对于读操作实在是太浪费了)。为此内核提供了另一种锁-读写自旋锁,读自旋锁也叫共享自旋锁,写自旋锁也叫排他自旋锁。
读写自旋锁是一种比自旋锁粒度更小的锁机制,它保留了“自旋”的概念,但是在写操作方面,只能最多有一个写进程,在读操作方面,同时可以有多个读执行单元,当然,读和写也不能同时进行。
读写自旋锁的使用也普通自旋锁的使用很类似,首先要初始化读写自旋锁对象:
// 静态初始化
rwlock_t rwlock = RW_LOCK_UNLOCKED;
//动态初始化
rwlock_t *rwlock;
...
rw_lock_init(rwlock);

在读操作代码里对共享数据获取读自旋锁:
read_lock(&rwlock);
...
read_unlock(&rwlock);

在写操作代码里为共享数据获取写自旋锁:
write_lock(&rwlock);
...
write_unlock(&rwlock);

需要注意的是,如果有大量的写操作,会使写操作自旋在写自旋锁上而处于写饥饿状态(等待读自旋锁的全部释放),因为读自旋锁会自由的获取读自旋锁。

读写自旋锁的函数类似于普通自旋锁,这里就不一一介绍了,我们把它列在下面的表中。
RW_LOCK_UNLOCKED
rw_lock_init(rwlock_t *)
read_lock(rwlock_t *)
read_unlock(rwlock_t *)
read_lock_irq(rwlock_t *)
read_unlock_irq(rwlock_t *)
read_lock_irqsave(rwlock_t *, unsigned long)
read_unlock_irqsave(rwlock_t *, unsigned long)
write_lock(rwlock_t *)
write_unlock(rwlock_t *)
write_lock_irq(rwlock_t *)
write_unlock_irq(rwlock_t *)
write_lock_irqsave(rwlock_t *, unsigned long)
write_unlock_irqsave(rwlock_t *, unsigned long)
rw_is_locked(rwlock_t *)
六、顺序琐
顺序琐(seqlock)是对读写锁的一种优化,若使用顺序琐,读执行单元绝不会被写执行单元阻塞,也就是说,读执行单元可以在写执行单元对被顺序琐保护的共享资源进行写操作时仍然可以继续读,而不必等待写执行单元完成写操作,写执行单元也不需要等待所有读执行单元完成读操作才去进行写操作。
但是,写执行单元与写执行单元之间仍然是互斥的,即如果有写执行单元在进行写操作,其它写执行单元必须自旋在哪里,直到写执行单元释放了顺序琐。
如果读执行单元在读操作期间,写执行单元已经发生了写操作,那么,读执行单元必须重新读取数据,以便确保得到的数据是完整的,这种锁在读写同时进行的概率比较小时,性能是非常好的,而且它允许读写同时进行,因而更大的提高了并发性,
注意,顺序琐由一个限制,就是它必须被保护的共享资源不含有指针,因为写执行单元可能使得指针失效,但读执行单元如果正要访问该指针,将导致Oops。
七、信号量
Linux中的信号量是一种睡眠锁,如果有一个任务试图获得一个已经被占用的信号量时,信号量会将其推进一个等待队列,然后让其睡眠,这时处理器能重获自由,从而去执行其它代码,当持有信号量的进程将信号量释放后,处于等待队列中的哪个任务被唤醒,并获得该信号量。
信号量,或旗标,就是我们在操作系统里学习的经典的P/V原语操作。
P:如果信号量值大于0,则递减信号量的值,程序继续执行,否则,睡眠等待信号量大于0。
V:递增信号量的值,如果递增的信号量的值大于0,则唤醒等待的进程。

信号量的值确定了同时可以有多少个进程可以同时进入临界区,如果信号量的初始值始1,这信号量就是互斥信号量(MUTEX)。对于大于1的非0值信号量,也可称为计数信号量(counting semaphore)。对于一般的驱动程序使用的信号量都是互斥信号量。
类似于自旋锁,信号量的实现也与体系结构密切相关,具体的实现定义在头文件中,对于x86_32系统来说,它的定义如下:
struct semaphore {
atomic_t count;
int sleepers;
wait_queue_head_t wait;
};

信号量的初始值count是atomic_t类型的,这是一个原子操作类型,它也是一个内核同步技术,可见信号量是基于原子操作的。我们会在后面原子操作部分对原子操作做详细介绍。

信号量的使用类似于自旋锁,包括创建、获取和释放。我们还是来先展示信号量的基本使用形式:
static DECLARE_MUTEX(my_sem);
......
if (down_interruptible(&my_sem))

{
return -ERESTARTSYS;
}
......
up(&my_sem)

Linux内核中的信号量函数接口如下:
static DECLARE_SEMAPHORE_GENERIC(name, count);
static DECLARE_MUTEX(name);
seam_init(struct semaphore *, int);
init_MUTEX(struct semaphore *);
init_MUTEX_LOCKED(struct semaphore *)
down_interruptible(struct semaphore *);
down(struct semaphore *)
down_trylock(struct semaphore *)
up(struct semaphore *)
• 初始化信号量
信号量的初始化包括静态初始化和动态初始化。静态初始化用于静态的声明并初始化信号量。
static DECLARE_SEMAPHORE_GENERIC(name, count);
static DECLARE_MUTEX(name);

对于动态声明或创建的信号量,可以使用如下函数进行初始化:
seam_init(sem, count);
init_MUTEX(sem);
init_MUTEX_LOCKED(struct semaphore *)

显然,带有MUTEX的函数始初始化互斥信号量。LOCKED则初始化信号量为锁状态。
• 使用信号量
信号量初始化完成后我们就可以使用它了
down_interruptible(struct semaphore *);
down(struct semaphore *)
down_trylock(struct semaphore *)
up(struct semaphore *)

down函数会尝试获取指定的信号量,如果信号量已经被使用了,则进程进入不可中断的睡眠状态。down_interruptible则会使进程进入可中断的睡眠状态。关于进程状态的详细细节,我们在内核的进程管理里在做详细介绍。

down_trylock尝试获取信号量, 如果获取成功则返回0,失败则会立即返回非0。

当退出临界区时使用up函数释放信号量,如果信号量上的睡眠队列不为空,则唤醒其中一个等待进程。

八、读写信号量
类似于自旋锁,信号量也有读写信号量。读写信号量API定义在头文件中,它的定义其实也是体系结构相关的,因此具体实现定义在头文件中,以下是x86的例子:
struct rw_semaphore {
signed long count;
spinlock_t wait_lock;
struct list_head wait_list;
};

㈢ 如何在Linux用户和内核空间中进行动态跟踪

你不记得如何在代码中插入探针点了吗? 没问题!了解如何使用uprobe和kprobe来动态插入它们吧。 基本上,程序员需要在源代码汇编指令的不同位置插入动态探针点。

探针点
探针点是一个调试语句,有助于探索软件的执行特性(即,执行流程以及当探针语句执行时软件数据结构的状态)。printk是探针语句的最简单形式,也是黑客用于内核攻击的基础工具之一。
因为它需要重新编译源代码,所以printk插入是静态的探测方法。内核代码中重要位置上还有许多其他静态跟踪点可以动态启用或禁用。 Linux内核有一些框架可以帮助程序员探测内核或用户空间应用程序,而无需重新编译源代码。Kprobe是在内核代码中插入探针点的动态方法之一,并且uprobe在用户应用程序中执行此操作。
使用uprobe跟踪用户空间
可以通过使用thesysfs接口或perf工具将uprobe跟踪点插入用户空间代码。
使用sysfs接口插入uprobe
考虑以下简单测试代码,没有打印语句,我们想在某个指令中插入探针:
[source,c\n.test.c
#include <stdio.h>\n#include <stdlib.h>\n#include <unistd.h>
编译代码并找到要探测的指令地址:
# gcc -o test test.\n# objmp -d test
假设我们在ARM64平台上有以下目标代码:
0000000000400620 <func_1>: 400620\t90000080\tadr\tx0, 410000 <__FRAME_END__+0xf6f8>
并且我们想在偏移量0x620和0x644之间插入探针。执行以下命令
# echo 'p:func_2_entry test:0x620' > /sys/kernel/debug/tracing/uprobe_event\n# echo 'p:func_1_entry test:0x644' >> /sys/kernel/debug/tracing/uprobe_event\n# echo 1 > /sys/kernel/debug/tracing/events/uprobes/enable# ./test&
在上面的第一个和第二个echo语句中,p告诉我们这是一个简单的测试。(探测器可以是简单的或返回的。)func_n_entry是我们在跟踪输出中看到的名称,名称是可选字段,如果没有提供,我们应该期待像p_test_0x644这样的名字。test 是我们要插入探针的可执行二进制文件。如果test 不在当前目录中,则需要指定path_to_test / test。
0x620或0x640是从程序启动开始的指令偏移量。请注意>>在第二个echo语句中,因为我们要再添加一个探针。所以,当我们在前两个命令中插入探针点之后,我们启用uprobe跟踪,当我们写入events/ uprobes / enable时,它将启用所有的uprobe事件。程序员还可以通过写入在该事件目录中创建的特定事件文件来启用单个事件。一旦探针点被插入和启用,每当执行探测指令时,我们可以看到一个跟踪条目。
读取跟踪文件以查看输出:
# cat /sys/kernel/debug/tracing/trac\n# tracer: no\n\n# entries-in-buffer/entries-written: 8/8\n#P:\n\n# _-----=> irqs-of\n# / _----=> need-resche\n# | / _---=> hardirq/softir\n# || / _--=> preempt-dept\n# ||| / dela\n# TASK-PID CP\n# |||| TIMESTAMP FUNCTION# | | | |||| | |
我们可以看到哪个CPU完成了什么任务,什么时候执行了探测指令。
返回探针也可以插入指令。当返回该指令的函数时,将记录一个条目:
# echo 0 > /sys/kernel/debug/tracing/events/uprobes/enabl\n# echo 'r:func_2_exit test:0x620' >> /sys/kernel/debug/tracing/uprobe_event\n# echo 'r:func_1_exit test:0x644' >> /sys/kernel/debug/tracing/uprobe_event\n# echo 1 > /sys/kernel/debug/tracing/events/uprobes/enable
这里我们使用r而不是p,所有其他参数是相同的。请注意,如果要插入新的探测点,需要禁用uprobe事件:
test-3009 [002] .... 4813.852674: func_1_entry: (0x400644)
上面的日志表明,func_1返回到地址0x4006b0,时间戳为4813.852691。
# echo 0 > /sys/kernel/debug/tracing/events/uprobes/enabl\n# echo 'p:func_2_entry test:0x630' > /sys/kernel/debug/tracing/uprobe_events count=%x\n# echo 1 > /sys/kernel/debug/tracing/events/uprobes/enabl\n# echo > /sys/kernel/debug/tracing/trace# ./test&
当执行偏移量0x630的指令时,将打印ARM64 x1寄存器的值作为count =。
输出如下所示:
test-3095 [003] .... 7918.629728: func_2_entry: (0x400630) count=0x1
使用perf插入uprobe
找到需要插入探针的指令或功能的偏移量很麻烦,而且需要知道分配给局部变量的CPU寄存器的名称更为复杂。 perf是一个有用的工具,用于帮助引导探针插入源代码中。
除了perf,还有一些其他工具,如SystemTap,DTrace和LTTng,可用于内核和用户空间跟踪;然而,perf与内核配合完美,所以它受到内核程序员的青睐。
# gcc -g -o test test.c# perf probe -x ./test func_2_entry=func_\n# perf probe -x ./test func_2_exit=func_2%retur\n# perf probe -x ./test test_15=test.c:1\n# perf probe -x ./test test_25=test.c:25 numbe\n# perf record -e probe_test:func_2_entry -e\nprobe_test:func_2_exit -e probe_test:test_15\n-e probe_test:test_25 ./test
如上所示,程序员可以将探针点直接插入函数start和return,源文件的特定行号等。可以获取打印的局部变量,并拥有许多其他选项,例如调用函数的所有实例。 perf探针用于创建探针点事件,那么在执行./testexecutable时,可以使用perf记录来探测这些事件。当创建一个perf探测点时,可以使用其他录音选项,例如perf stat,可以拥有许多后期分析选项,如perf脚本或perf报告。
使用perf脚本,上面的例子输出如下:
# perf script
使用kprobe跟踪内核空间
与uprobe一样,可以使用sysfs接口或perf工具将kprobe跟踪点插入到内核代码中。
使用sysfs接口插入kprobe
程序员可以在/proc/kallsyms中的大多数符号中插入kprobe;其他符号已被列入内核的黑名单。还有一些与kprobe插入不兼容的符号,比如kprobe_events文件中的kprobe插入将导致写入错误。 也可以在符号基础的某个偏移处插入探针,像uprobe一样,可以使用kretprobe跟踪函数的返回,局部变量的值也可以打印在跟踪输出中。
以下是如何做:
; disable all events, just to insure that we see only kprobe output in trace\n# echo 0 > /sys/kernel/debug/tracing/events/enable; disable kprobe events until probe points are inseted\n# echo 0 > /sys/kernel/debug/tracing/events/kprobes/enable; clear out all the events from kprobe_events\n to insure that we see output for; only those for which we have enabled
[root@pratyush ~\n# more /sys/kernel/debug/tracing/trace# tracer: no\n\n# entries-in-buffer/entries-written: 9037/9037\n#P:8\n# _-----=> irqs-of\n# / _----=> need-resche\n# | / _---=> hardirq/softirq#\n|| / _--=> preempt-depth#\n ||| / delay# TASK-PID CPU#\n |||| TIMESTAMP FUNCTION#\n | | | |||| | |
使用perf插入kprobe
与uprobe一样,程序员可以使用perf在内核代码中插入一个kprobe,可以直接将探针点插入到函数start和return中,源文件的特定行号等。程序员可以向-k选项提供vmlinux,也可以为-s选项提供内核源代码路径:
# perf probe -k vmlinux kfree_entry=kfre\n# perf probe -k vmlinux kfree_exit=kfree%retur\n# perf probe -s ./ kfree_mid=mm/slub.c:3408 \n# perf record -e probe:kfree_entry -e probe:kfree_exit -e probe:kfree_mid sleep 10
使用perf脚本,以上示例的输出:
关于Linux命令的介绍,看看《linux就该这么学》,具体关于这一章地址3w(dot)linuxprobe/chapter-02(dot)html

㈣ Linux环境变量顺序

内核启动的时候,各个驱动初始化的工作在文件init/main.c中的do_basic_setup()函数中做.
------------------------------------------------------------------------------------------------------
static void __init do_basic_setup(void)
{
/* drivers will send hotplug events */
init_workqueues();
usermodehelper_init();
driver_init();

#ifdef CONFIG_SYSCTL
sysctl_init();
#endif

/* Networking initialization needs a process context */
sock_init();

do_initcalls();
}
------------------------------------------------------------------------------------------------------
其中的driver_init()做一些核心的初始化,看看代码就明白了.
相应的驱动程序的初始化在do_initcalls()中做.
------------------------------------------------------------------------------------------------------
static void __init do_initcalls(void)
{
initcall_t *call;
int count = preempt_count();

for (call = __initcall_start; call < __initcall_end; call++) {
char *msg;

if (initcall_debug) {
printk(KERN_DEBUG "Calling initcall 0x%p", *call);
print_fn_descriptor_symbol(": %s()", (unsigned long) *call);
printk("\n");
}

(*call)();

msg = NULL;
if (preempt_count() != count) {
msg = "preemption imbalance";
preempt_count() = count;
}
if (irqs_disabled()) {
msg = "disabled interrupts";
local_irq_enable();
}
if (msg) {
printk(KERN_WARNING "error in initcall at 0x%p: "
"returned with %s\n", *call, msg);
}
}

/* Make sure there is no pending stuff from the initcall sequence */
flush_scheled_work();
}
------------------------------------------------------------------------------------------------------
这个__initcall_start是在文件 arch/xxx/kernel/vmlinux.lds.S (其中的xxx 是你的体系结构的名称,例如i386)
这个文件是内核ld的时候使用的.其中定义了各个sectioin,看看就明白了。
在这个文件中有个.initcall.init, 代码如下:
------------------------------------------------------------------------------------------------------
__initcall_start = .;
.initcall.init : {
*(.initcall1.init)
*(.initcall2.init)
*(.initcall3.init)
*(.initcall4.init)
*(.initcall5.init)
*(.initcall6.init)
*(.initcall7.init)
}
------------------------------------------------------------------------------------------------------

这里有7个初始化的优先级,内核会按照这个优先级的顺序依次加载.
这些优先级是在文件include/linux/init.h 中定义的. 你注意一下宏 __define_initcall的实现就明白了.
相关代码如下:

#define __define_initcall(level,fn) \
static initcall_t __initcall_##fn __attribute_used__ \
__attribute__((__section__(".initcall" level ".init"))) = fn

#define core_initcall(fn) __define_initcall("1",fn)
#define postcore_initcall(fn) __define_initcall("2",fn)
#define arch_initcall(fn) __define_initcall("3",fn)
#define subsys_initcall(fn) __define_initcall("4",fn)
#define fs_initcall(fn) __define_initcall("5",fn)
#define device_initcall(fn) __define_initcall("6",fn)
#define late_initcall(fn) __define_initcall("7",fn)

我们可以看到,我们经常写的设备驱动程序中常用的mole_init其实就是对应了优先级6:
#define __initcall(fn) device_initcall(fn)

#define mole_init(x) __initcall(x);

文章出处:http://www.diybl.com/course/6_system/linux/Linuxjs/2008628/128990.html
请采纳。

㈤ 銆怢inux鍐呮牳|杩涚▼绠$悊銆0鍙风嚎绋媠wapper绠浠

鍦↙inux鍐呮牳镄勪笘鐣岄噷锛0鍙风嚎绋嬶纸阃氩父绉颁负init_taskswapper锛夋壆婕旂潃镊冲叧閲嶈佺殑瑙掕壊锛岀壒鍒鏄鍦ㄥ唴镙稿埯濮嫔寲镄勬棭链熼桩娈点傝╂垜浠娣卞叆鎺㈣ㄨ繖涓镙稿绩缁勪欢锛屼简瑙e畠濡备綍椹卞姩鏁翠釜绯荤粺钖锷ㄦ祦绋嬨

棣栧厛锛岃╂垜浠浠嶭inux-6.1鍐呮牳鐗堟湰璇磋捣銆傚傛灉浣犲圭幆澧冩惌寤烘劅鍏磋叮锛屽彲浠ヤ粠鎴戠殑涓浜轰粨搴揿紑濮嬫帰绱锛歔鐜澧冩惌寤鸿剼链琞(https://gitee.com/kingdix10/envsetupeel)锛屼富浠揿簱[[涓讳粨搴挞摼鎺]]锛屾敼锷ㄤ粨搴揫[鏀瑰姩浠揿簱阈炬帴]]锛岄┍锷ㄧず渚媅[椹卞姩绀轰緥阈炬帴]]锛屼互鍙婅緟锷╁伐鍏穂[杈呭姪宸ュ叿阈炬帴]]銆傝繖浜涜祫婧愬皢甯锷╀綘镟村ソ鍦扮悊瑙e拰瀹炶返銆

鍦ˋRM64鏋舵瀯涓锛屼竴鍒囧嬩簬0鍙风嚎绋嬬殑鍒濆嫔寲銆傚畠棣栧厛钖锷锛屽垱寤1鍙峰拰2鍙风嚎绋嬶纴铹跺悗杩涘叆涓涓绛夊緟鐘舵侊纴杩欓氩父琚绉颁负idle鐘舵併傝繖涓杩囩▼鐢kernel_initkernel_execve寮曞硷纴钖庤呭紩瀵肩敤鎴锋佺殑init绋嫔簭锛岃繘涓姝ュ垱寤哄叾浠栬繘绋嬬嚎绋嬨

鍦ㄦ傚康鍜屽埯濮嫔寲阒舵碉纴init_task鏄涓涓闱欐佺粨鏋勪綋锛屾槸鍐呮牳钖锷ㄧ殑璧风偣銆傚畠涓task_struct绱у瘑鐩歌繛锛屽埯濮嫔寲镞惰剧疆浜嗗叧阌缁勪欢锛屽init_mm鍐呭瓨绠$悊绯荤粺銆佽皟搴﹀櫒銆佸畾镞跺櫒銆佷腑鏂澶勭悊銆丳ID绠$悊鍜屽唴镙哥紦瀛樼瓑銆

姹囩紪阒舵碉纴start_kernel钖锷ㄤ简鍐呮牳镄勬墽琛岋纴闆嗕腑绮惧姏浜庡埯濮嫔寲璋冨害鍣锛岃剧疆瀹氭椂鍣锛屼互鍙婂埯濮嫔寲cred锛堢敤鎴锋爣璇嗭级鍜宖ork绛夋牳蹇冨姛鑳姐傚湪姝よ繃绋嬩腑锛屼腑鏂鐘舵佸缑鍒扮$悊锛岀郴缁熸椂阍熷拰寤惰繜璁$畻涔熷缑浠ヨ剧疆銆

杩涘叆arch_call_rest_init()锛岀郴缁熻繘涓姝ュ埯濮嫔寲rest閮ㄥ垎锛岄槻姝㈠熬璋幂敤浼桦寲锛屽苟涓烘疮涓澶勭悊鍣ㄥ垱寤簊wapper绾跨▼銆傚湪1鍙风嚎绋嬭繍琛屽悗锛宻mp_init浼氲皟鐢╥dle_threads_init锛屼负闱瀊oot CPU鍒涘缓0鍙风嚎绋嬨

褰0鍙风嚎绋嬬粨𨱒燂纴绯荤粺寮濮嫔垱寤1鍙峰拰2鍙风嚎绋嬶纴杩涜屽叏闱㈢殑璋冨害銆傚湪rest_init涓锛宨nit_task锛圥ID=1锛夎浼桦厛鍒涘缓锛岀‘淇濆叾鍦╧threadd涔嫔墠杩愯岋纴骞惰剧疆绯荤粺鐘舵佷负SYSTEM_SCHEDULING銆傛ゆ椂锛schele()棣栨℃墽琛岋纴涓哄悗缁镄勮繘绋嬭皟搴﹀犲畾浜嗗熀纭銆

鍦╥dle绾跨▼镄勫惊鐜涓锛屾垜浠鐪嫔埌鍏抽敭镎崭綔濡备笅锛


鍦ㄥ惊鐜涓锛屼腑鏂鍜宲olling琚濡ュ杽澶勭悊锛岀‘淇濈郴缁熺殑绋冲畾杩愯屻

褰揅PU闇瑕佺荤嚎澶勭悊涓鏂锛屾垨钥卼ick骞挎挱杩囨湡锛屼细杩涘叆arch_cpu_idle_enter()锛屾镆ュ苟澶勭悊鐩稿叧闇姹伞傛帴镌锛岀郴缁熷彲鑳介吨钖痶ick锛屾垨钥呰繘琛宲olling镎崭綔锛岀劧钖庤皟鐢cpuidle_idle_call銆傞鍑烘椂锛岀郴缁熶细娓呴櫎polling鐘舵侊纴钖屾ヨ繘绋嫔苟镓ц岃皟搴︺

镐讳箣锛0鍙风嚎绋嬩綔涓哄唴镙哥殑蹇冭剰锛屽叾鍒濆嫔寲鍜岃皟搴︽満鍒跺逛簬绯荤粺镄勫惎锷ㄨ呖鍏抽吨瑕併傞氲繃鐞呜В杩欎簺缁呜妭锛屾垜浠鑳芥洿濂藉湴鎺屾彙Linux鍐呮牳镄勮繍琛屾満鍒躲

阅读全文

与linuxpreempt相关的资料

热点内容
三菱fx3u编程线缆 浏览:696
科目四用什么app最好 浏览:615
python理财 浏览:879
python读csv文件内容 浏览:306
有难度的编程项目经验 浏览:518
网购食物哪个app好 浏览:369
为什么樱校显示服务器错误 浏览:270
亚马逊app购物在哪里下载 浏览:329
网络显示连接服务器失败怎么回事 浏览:585
如何确认是256加密 浏览:39
安卓加密教育视频 浏览:265
华为会员字体在哪个文件夹 浏览:582
单片机原理及应用第二版课后习题答案 浏览:118
女程序员一天怎么度过 浏览:654
轻易做程序员好吗 浏览:56
访问网络服务器是什么意思 浏览:367
区块链属性加密 浏览:995
小米k30怎么创建文件夹 浏览:385
2355解压软件 浏览:819
php有多线程吗 浏览:448