Linux内核设计与实现---内核同步方法

内核同步方法

  • 1 原子操作
    • 原子整数操作
      • 原子性与顺序性的比较
    • 原子位操作
  • 2 自旋锁
        • 自旋锁是不可递归的
    • 其他针对自旋锁的操作
    • 自旋锁和下半部
  • 3 读-写自旋锁
  • 4 信号量
    • 创建和初始化信号量
    • 使用信号量
  • 5 读-写信号量
  • 6 自旋锁和信号量
  • 7 完成变量
  • 8 互斥锁
    • 互斥锁API
  • 9 禁止抢占
  • 10 顺序和屏障

1 原子操作

原子操作是保证指令以原子的方式执行,执行过程不被打断。内核提供了两组原子接口,一组针对整数进行操作,另一组针对单独的位进行操作。在Linux支持的所有体系结构上都实现了这两组接口。

原子整数操作

针对整数的原子操作只能对atomic_t类型的数据进行处理。Linux支持的所有机器上的整数数据都是32位的,但是使用atomic_t的代码只能将该类型的数据当做24位来用。这个限制完全是因为在SPARC体系结构上,原子操作的实现不同于其他体系结构:32位int类型的低8位嵌入了一个锁,
在这里插入图片描述
因为SPARC体系结构对原子操作缺乏指令级的支持,所有只能利用该锁来避免对原子类型数据的并发访问。

原子操作的声明在<asm/atomic.h>文件中。所有的体系结构内核会提供一些相同的方法,有些体系结构会提供一些在该体系结构上使用的额外原子操作方法。

定义一个atomic_t类型的数据,还可以在定义时给它设定初值:

atomic_t v;	/* 定义v */
atomic_t u = ATOMIC_INIT(0);	/* 定义u并把u初始化为0 */

原子整数操作列表如下:
在这里插入图片描述

原子性与顺序性的比较

原子性确保指令执行期间不被打断,要么全部执行完,要么根本不执行。而顺序性确保即使两条或多条指令出现在独立的执行线程中,它们要执行顺序要按规定的执行。例如,给一个整数初始化为10,要么初始化成功,要么初始化失败,这就是原子性。接着又有一个操作给整数初始化为20,原子性不管是先初始化为10还是先初始化为20,这是顺序性的责任。

原子位操作

原子性操作是与体系结构相关的操作,定义在文件<asm/bitops.h>中。位操作函数是对普通的内存地址进行操作的,它的参数是一个指针和一个尾号。原子位操作的列表如下:
在这里插入图片描述
内核还提供了两个例程用来从指定的地址开始搜素第一个被设置(未被设置)的位

int find_first_bit(unsigned long *addr,unsigned int size);
int find_first_zero_bit(unsigned long *addr,unsigned int size);

2 自旋锁

Linux内核最常见的锁是自旋锁(spin lock)。自旋锁最多只能被一个可执行线程持有。如果一个执行线程试图获得一个被争用(已经被使用)的自旋锁,那么该线程就会一直进行忙循环,等待锁重新可用。在任何时刻,自旋锁都可以防止多余一个的执行线程同时进入临界区。

如果自旋锁已经被争用了,那么请求它的线程在等待锁重新可用时将一直自旋,所以特别浪费处理器时间,因此自旋锁不应该被长时间持有。

自旋锁的实现和体系结构密切相关,代码往往通过汇编实现。这些与体系结构相关的代码定义在文件<asm/spinlock.h>中,实际需要用到的接口定义在文件<linux/spinlock.h>中。自旋锁的基本使用形式如下:

spinlock_t mr_lock = SPIN_LOCK_UNLOCKED;spin_lock(&mr_lock);
/* 临界区 */
spin_unlock(&mr_lock);

因为自旋锁在同一时刻至多被一个执行线程持有,所以一个时刻只能有一个线程位于临界区内,这就为多处理器机器提供了防止并发访问所需的保护机制。注意在单处理器机器上,编译的时候并不会加入自旋锁,它仅仅被当做一个设置内核抢占机制是否被启用的开关。如果禁止内核抢占,那么在编译时自旋锁会被完全剔除内核。

自旋锁是不可递归的

Linux内核实现的自旋锁是不可递归的,如果你请求一个你已经持有的自旋锁,那么你将会自旋,等待释放这个锁,由于自旋,释放这个锁的操作不会执行,所有会一直处于自旋忙等待中,于是你被自己锁死了。

自旋锁可以使用在中断处理程序中。在中断处理程序中使用自旋锁时,一定要在获取锁之前,首先禁止本地中断(当前处理器上的中断请求),否则,中断处理程序就会打断正持有锁的内核代码,有可能试图去争用这个已经被持有的自旋锁。这样一来,中断处理程序就会自旋,但是锁的持有者在这个中断处理程序执行完毕前不可能运行,会造成死锁。注意,需要关闭的只是当前处理器上的中断,如果中断发生在不同的处理器上,即使中断处理程序在同一锁上自旋,也不会妨碍锁的持有者最终释放锁。

内核提供的禁止中断同时请求锁的接口:

spinlock_t mr_lock = SPIN_LOCK_UNLOCKED;
unsigned long flags;spin_lock_irqsave(&mr_lock,flags);
/* 临界区 */
spin_lock_irqrestore(&mr_lock,flags);

函数spin_lock_irqsave保存中断的当前状态,并禁止中断,然后再去获取指定的锁。反过来spin_lock_irqrestore对指定的锁解锁,然后让中断恢复到加锁前的状态。

配置选项CONFIG_DEBUG_SPINLOCK为使用自旋锁的代码加入了许多调试检测手段。

其他针对自旋锁的操作

spin_lock_init()用来初始化动态创建的自旋锁。spin_try_lock试图获得某个特定的自旋锁,其他自旋锁操作如下:
在这里插入图片描述

自旋锁和下半部

由于下半部可以抢占进程上下文中的代码,所以当下半部和进程上下文共享数据时,必须对进程上下文中的共享数据进行保护,所以需要加锁的同时还要禁止下半部执行。同样,由于中断程序程序可以抢占下半部,所以如果中断处理程序和下半部共享数据,那么就必须在获取恰当的锁的同时还要禁止中断。

3 读-写自旋锁

Linux提供了专门的读写自旋锁,这种自旋锁为读和写分别提供了不同的锁,一个或多个读任务可以并发的持有读者锁;相反,用于写的锁最多只能被一个写任务持有,而且此时不能有并发的读操作。有时把读写锁叫做共享排斥锁,或者并发排斥锁,因为这种锁以共享(对读者而言)和排斥(对写着而言)的形式获得使用。

加锁逻辑:

  1. 假设临界区内没有任何的thread,这时候任何read thread或者write thread可以进入
  2. 假设临界区内有一个read thread,这时候新来的read thread可以任意进入,但是write thread不可以进入
  3. 假设临界区有一个write thread,这时候任何的read thread或者write thread 都不可以进入
  4. 假设临界区内有一个或者多个read thread,write thread当然不可以进入临界区,但是该write thread也无法阻止后续read thread的进入,他要一直等到临界区一个read thread也没有的时候,才可以进入。可见,rw spinlock给reader赋予了更高的权限。

读写自旋锁的使用方式类似于普通自旋锁:

rwlock_t my_rwlock = RW_LOCK_UNLOCKED;	/* 初始化 */
read_lock(&my_rwlock);
/* 临界区 只读*/
read_unlock(&my_lock);

在可以写的临界区加上如下代码:

write_lock(&my_rwlock);
/* 临界区(写) */
write_unlock(&my_rwlock);

不能同时请求读锁和写锁:

read_lock(&my_rwlock);
write_lock(&my_rwlock);

这样将会带来死锁,因为写锁会不断自旋,而读锁得不到释放。
针对读写自旋锁的操作如下:
在这里插入图片描述
在使用Linux读-写自旋锁时,最后要考虑的一点是这种锁照顾读比照顾写要多一点,当读锁被持有时,写操作为了互斥访问只能等待,但是,读者却可以继续成功地占用锁。而自旋等待的写者在所有读者释放锁之前是无法获得锁的。

自旋锁提供了一种快速简单的锁实现方式,如果加锁时间不长并且代码不会休眠,利用自旋锁时最佳选择。

4 信号量

Linux中的信号量是一种睡眠锁。如果有一个任务试图获得一个已经被占用的信号量时,信号量会将其放到一个等待队列,然后让其睡眠。这时处理器能去执行其他代码。当持有信号量的进程将信号量释放后,处于等待队列中的那个任务将被唤醒,并获得该信号量。

信号量可以同时允许任意数量的锁持有者,而自旋锁在一个时刻最多允许一个任务持有它。信号量同时允许的持有者数量可以在声明信号量时指定,这个值称为使用者数量。通常情况下,信号量和自旋锁一样,在一个时刻仅允许有一个锁持有者。当数量等于1,这样的信号量被称为二值信号量或者被称为互斥信号量;初始化时也可以把数量设置为大于1的非0值,这种情况,信号量被称为计数信号量,它允许在一个时刻至多有count个锁持有者。

信号量支持两个原子操作P()和V()。前者叫做测试操作,后者叫做增加操作,后来系统把这两种操作分别叫做down()和up(),Linux也遵从这种叫法。down()通过对信号量减1来请求一个信号量,如果减1结果是0或者大于0,那么就获得信号量锁,任务就可以进入临界区,如果结果是负的,那么任务会被放入等待队列。相反,当临界区的操作完成后,up()操作用来释放信号量,如果在该信号量上的等待队列不为空,那么处于队列中等待的任务被唤醒。

创建和初始化信号量

信号量的实现是与体系结构有关的,具体实现定义在文件<asm/semaphore.h>中。struct semaphore类型表示信号量。可以通过以下方式静态声明信号量:

static DECLARE_SEMAPHORE_GENERIC(name,count);

其中name是信号量变量名,count是信号量的使用者数量。创建更为普通的互斥信号量可以使用以下方式:

static DECLARE_MUTEX(name);

我们可以使用sema_init对信号量进行动态初始化:

sema_init(sem,count);

sem是指针,count是信号量的使用者数量。初始化一个动态创建的互斥信号量时使用以下函数:

sema_MUTEX(sem)

使用信号量

函数down_interruptible()试图获取指定的信号量,如果获取失败,它将以TASK_INTERRUPTIBLE状态进入睡眠。如果进程在等待获取信号量的时候接受到了信号,那么该进程就会被唤醒,而函数down_interruptible()会返回EINTR。另外一个函数down()获取信号量失败会让进程在TASK_UNINTERRUPTIBLE状态下睡眠,我们应该避免这种情况,因为进程等待信号量的时候就不再响应信号了。

使用down_trylock()函数,可以尝试获取指定的信号量,在信号量被占用时,它立刻返回非0值,否则,返回0,并且成功获取信号量锁。

要释放指定的信号,需要调用up()函数。
在这里插入图片描述
针对信号量的操作如下表:
在这里插入图片描述

5 读-写信号量

与自旋锁一样,信号量也有区分读写访问的可能,。读写信号量在内核中是由rw_semaphore结构表示的,定义在文件<linux/rwsem.h>中。通过以下语句可以创建静态声明的读写信号量:

static DECLARE_RWSEM(name);

动态创建读写信号量可以通过下面的函数:

init_rwsem(struct rw_semaphore *sem)

所有的读写信号量都是互斥信号量(它们的引用计数等于1)。只要没有写着,并发持有读锁的读者数不限。相反,只有唯一的写者(没有读者时)可以获得写锁。所有的读写锁的睡眠都不会被信号打断,它只有一个down()操作:
在这里插入图片描述

6 自旋锁和信号量

在中断上下文中只能使用自旋锁,在任务睡眠时只能使用信号量。
在这里插入图片描述

7 完成变量

如果在内核中一个任务需要发出信号通知另一个任务发生了某个特定事件,利用完成变量是使两个任务以同步的简单方法。如果一个任务要执行一些工作时,另一任务就会在完成变量上等待,当这个任务完成后,会使用完成变量去唤醒在等待的任务。

完成变量由结构体completion表示,定义在<linux/cmpletion.h>中。可以通过以下方式创建:

DECLARE_COMPLETION(mr_comp)	/* 静态创建 */
init_completion()	/* 动态创建 */

在一个指定的完成变量上,需要等待的任务调用wait_for_completion()来等待特定事件。当特定事件发生后,产生事件的任务调用complete()来发送信号唤醒正在等待的任务。

8 互斥锁

Mutex(互斥锁)是较常用的锁机制,为了理解它是怎么工作的,来看一看它在include/linux/mutex.h中的结构定义:

/** 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;raw_spinlock_t		wait_lock;
#ifdef CONFIG_MUTEX_SPIN_ON_OWNERstruct optimistic_spin_queue osq; /* Spinner MCS lock */
#endifstruct list_head	wait_list;
#ifdef CONFIG_DEBUG_MUTEXESvoid			*magic;
#endif
#ifdef CONFIG_DEBUG_LOCK_ALLOCstruct lockdep_map	dep_map;
#endif
};

其结构中有一个链表类型字段:wait_list,当竞争者无法获取资源时,就会加入到该链表中,竞争者从调度器的运行列表中删除,放入处于睡眠状态的等待链表(wait_list)中。然后内核调度并执行其他任务,当锁被释放时,等待队列中的等待者被唤醒,从wait_list移除,然后重新被调度。

互斥锁API

  1. 声明
    静态声明:
#define DEFINE_MUTEX(mutexname) \struct mutex mutexname = __MUTEX_INITIALIZER(mutexname)

动态声明:

struct mutex my_mutex;
mutex_init(&my_mutex);
  1. 获取互斥锁
void mutex_lock(struct mutex *lock);
int mutex_lock_interruptible(struct mutex *lock);
int  mutex_lock_killable(struct mutex *lock);
int mutex_trylock(struct mutex *lock);

mutex_lock在不能获得互斥锁时,会把任务加入到睡眠队列中,必须要保证互斥锁能够释放。mutex_lock_interruptible在不能获得互斥锁时,会把任务加入到睡眠队列中,但是任务在睡眠队列时有信号到达,mutex_lock_interruptible会返回 -EINTR,程序会自动往下运行。
mutex_lock_killable在不能获得互斥锁时,会把任务加入到睡眠队列中,只有杀死任务的信号,才能中断驱动程序,程序会继续往下运行。
mutex_trylock如果不能获得互斥锁,不会把任务加入到睡眠列表中,直接返回。
3. 释放互斥锁

void mutex_unlock(struct mutex *lock);

有时需要检查互斥锁是否锁定。为此使用mutex_is_locked函数:

/*** mutex_is_locked - is the mutex locked* @lock: the mutex to be queried** Returns true if the mutex is locked, false if unlocked.*/
extern bool mutex_is_locked(struct mutex *lock);

已经被其他任务获取了,返回true,没有被使用返回false

互斥锁没有轮询机制,每次在互斥锁上调用mutex_unlock时,内核都会检查wait_list中的等待任务,如果有等待者,则其中的一个将被唤醒,它们唤醒的顺序域它们入睡的顺序相同。

9 禁止抢占

由于内核时抢占性的,内核中的进程在任何时候都可能停下来以便另一个更高优先级的进程运行。这意味着一个任务与被抢占的任务可能会在同一个临界区内运行,为了避免这种情况,内核抢占代码使用自旋锁作为非抢占区域的标记。如果一个自旋锁被持有,内核便不能进行抢占。因为内核抢占和SMP面对相同的并发问题,并且内核已经是SMP安全的,因此,这种简单的变化使得内核也是抢占安全的。

实际中,某些情况下并不需要自旋锁,但是仍然需要关闭内核抢占,出现最频繁的情况就是每个处理器上的数据。如果数据对每个处理器是唯一的,那么这样的数据可能就不需要使用锁来保护,因为数据只能被一个处理器访问,如果自旋锁没有被持有,内核又是抢占的,那么一个新调度的任务就可能访问同一个变量,如下所示
在这里插入图片描述

这样,即使这是一个单处理器,变量foo也会被多个进程以伪并发的方式访问。通常,这个变量会请求得到一个自旋锁(防止多处理器上的真并发)。但是如果这是每个处理器上独立的变量,可能就不需要锁。

为了解决这个问题,可以通过preempt_disable()禁止内核抢占。这是一个可以嵌套调用的函数,可以调用任意次。每次调用都必须有一个相应的preempt_enable()调用。当最后一次preempt_enable()被调用后,内核抢占才重新启用。

抢占计数存放着持有锁的数量和preempt_disable()的调用次数,如果计数是0,那么内核可以进行抢占,如果为1或更大的值,那么内核就不会进行抢占。函数preempt_count()返回这个值。
在这里插入图片描述

为了更简洁的方法解决每个处理器上的数据访问问题,可以通过get_cpu获得处理器编号。这个函数在返回当前处理器号前会首先关闭内核抢占。put_cpu()会恢复内核抢占:
在这里插入图片描述

10 顺序和屏障

屏障是告诉编译器不要对给定点周围的指令序列进行重新排序。

rmb()方法提供了一个读内存屏障,它确保在rmb()之前的载入操作不会被重新排在该调用之后,在rmb()之后的载入操作不会被重新排在该调用之前。
wmb()提供了一个写内存屏障,这个函数的功能和rmb()类似,区别仅仅是它是针对存储而非载入。
mb()方法即提供了读屏障也提供了写屏障。
内核和编译器屏障方法如下:
在这里插入图片描述
注意,对于不同体系结构,屏障的实际效果差别很大。例如,如果一个体系结构不执行打乱存储(比如intel x86芯片),那么wmb()就什么也不做。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/379642.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

UNIX环境高级编程---进程间通信总结

进程间通信1 管道匿名管道命名管道2 消息队列3 信号量POSIX信号量有名信号量无名信号量有名信号量和无名信号量的公共操作4 共享内存5 信号相关函数6 套接字针对 TCP 协议通信的 socket 编程模型针对 UDP 协议通信的 socket 编程模型针对本地进程间通信的 socket 编程模型总结L…

搜索---广度优先遍历、深度优先遍历、回溯法

参考文章&#xff1a;https://github.com/CyC2018/CS-Notes/blob/master/notes/Leetcode%20%E9%A2%98%E8%A7%A3%20-%20%E6%90%9C%E7%B4%A2.md 广度优先搜索&#xff08;BFS&#xff09; 广度优先搜索是按层来处理顶点的&#xff0c;距离开始点最近的那些顶点首先被访问&#…

如何更改Visual Studio 2008中类文件引用的默认名称空间?

在编写程序的时候&#xff0c;如果某些名称空间经常用到&#xff0c;每次创建一个文件的时候&#xff0c;都需要手工添加名称空间&#xff0c;是不是很烦人呢&#xff1f;多说人会回答&#xff1a;是的。如果新建文件的时候就自动加上自己需要的名称空间该多好啊。&#xff1a;…

Linux内核设计与实现---内存管理

内存管理1 页2 区3 获得页获得填充为0的页释放页4 kmalloc()gfp_mask标志kfree()5 vmalloc()6 slab层slab层的设计7 slab分配器的接口8 在栈上的静态分配9 高端内核的映射永久映射临时映射10 每个CPU的分配11 新的每个CPU的接口编译时的每个CPU数据运行时每个CPU数据12 使用每个…

多语言开发 之 通过基页类及Session 动态响应用户对语言的选择

在用户通过UserLogin.aspx登录系统时 提供其对语言的选择选择后 将所选存入Session 以便登录系统后的其他页面进行按语言显示当然相关页面需要支持多语言具体信息可参看使用 根据语言环境不同 而显示不同的 资源本地化 ASP.NET 网页 App_Code下定义基页类 BasePage.cs Codeusin…

Linux内核设计与实现---虚拟文件系统

虚拟文件系统1 通用文件系统2 文件系统抽象层3 Unix文件系统4 VFS对象及其数据结构其他VFS对象5 超级快对象超级块操作6 索引节点对象索引节点操作7 目录项对象目录项状态目录项缓存目录项操作8 文件对象9 和文件系统相关的数据结构10 和进程相关的数据结构11 Linux中的文件系统…

Java里面的几种路径的区别

1&#xff0c;相对路径 相对路径就是指由这个文件所在的路径引起的跟其它文件&#xff08;或文件夹&#xff09;的路径关系。 也就是说&#xff1a; 对于如图所示&#xff1a;一news.html为例 在WEB15工程下的WebContent下的WEB-INF下的news.html 当我访问的news.html的时候…

Linux内核设计与实现---块I/O层

块I/O层1 解刨一个块设备2 缓冲区和缓冲区头3 bio结构体新老方法对比4 请求队列5 I/O调度程序I/O调度程序的工作Linus电梯最终期限I/O调度程序预测I/O调度程序完全公正的排队I/O调度程序空操作的I/O调度程序I/O调度程序的选择系统中能够 随机访问 固定大小数据片的设备被称为块…

算法---数

数1 最大公约数2 最小公约数3 进制转换4 阶乘统计阶乘尾部0的个数5 字符串加法减法二进制加法6 多数投票问题数组中出现次数多于n/2的元素7 相遇问题改变数组元素使所有元素都相同1 最大公约数 欧几里得算法&#xff1a;两个整数的最大公约数等于其中较小的那个数和两数相除余…

Linux内核设计与实现---进程地址空间

进程地址空间1 内存描述符分配内存描述符销毁内存描述符mm_struct与内核线程2 内存区域VMA标志VMA操作内存区域的树形结构和内存区域的链表结构3 操作内存区域find_vma()find_vma_prev()find_vma_intersection()4 mmap()和do_mmap()&#xff1a;创建地址空间mmap&#xff08;&a…

JavaScript中带有示例的Math.log10()方法

JavaScript | Math.log10()方法 (JavaScript | Math.log10() Method) Math operations in JavaScript are handled using functions of math library in JavaScript. In this tutorial on Math.log10() method, we will learn about the log10() method and its working with e…

JSP技术

一、jsp脚本和注释 jsp脚本&#xff1a; 1&#xff09;<%java代码%> ----- 内部的java代码翻译到service方法的内部 2&#xff09;<%java变量或表达式> ----- 会被翻译成service方法内部out.print() 3&#xff09;<%!java代码%> ---- 会被翻译成servlet的成…

EL技术

1&#xff0e;EL 表达式概述 EL&#xff08;Express Lanuage&#xff09;表达式可以嵌入在jsp页面内部&#xff0c;减少jsp脚本的编写&#xff0c;EL 出现的目的是要替代jsp页面中脚本的编写。 2&#xff0e;EL从域中取出数据(EL最重要的作用) jsp脚本&#xff1a;<%requ…

SVN+AnkhSVN端配置

对于ankhSVN我想很多人不陌生&#xff0c;因为经常使用&#xff0c;但是我还是发现很多人并不怎么会配置&#xff0c;或者完全不知道其需要配置&#xff0c;如果不配置的话&#xff0c;当两个人同时需要修改某个文件的时候就容易中弹了。SVN默认是不支持“锁定-编辑-解锁”的&a…

Linux内核设计与实现---模块

模块1 构建模块放在内核源代码树中放在内核代码外2 安装模块3 产生模块依赖性4 载入模块5 管理配置选项6 模块参数7 导出符号表Linux内核是模块化组成的&#xff0c;它允许内核在运行时动态地向其中插入或从中删除代码。 与开发的内核核心子系统不同&#xff0c;模块开发更接近…

Linux内核设计与实现---kobject sysfs

kobject sysfs1 kobject2 ktype3 kset4 subsystem5 别混淆了这些结构体6 管理和操作kobject7 引用计数kref8 sysfssysfs中添加和删除kobject向sysfs添加文件9 内核事件层2.6内核增加了一个引人注目的新特性—同一设备模型。设备模型提供了独立的机制专门表示设备&#xff0c;并…

开发Windows Mobile今日插件 -- 内存电量,桌面便笺,桌面记单词

本篇文章讲解的是开发 Windows Mobile 上的今日插件。关于是今日插件&#xff0c;在 PPC 或者 SP SDK 的帮助文档中有相关的章节介绍&#xff0c;在网络上也有一些帖子和资源讲解。在这里简要回顾一下。今日插件就是在windows mobile的桌面上显示的条目&#xff0c;例如系统提供…

算法---递归

递归结题三部曲 何为递归&#xff1f;程序反复调用自身即是递归。 我自己在刚开始解决递归问题的时候&#xff0c;总是会去纠结这一层函数做了什么&#xff0c;它调用自身后的下一层函数又做了什么…然后就会觉得实现一个递归解法十分复杂&#xff0c;根本就无从下手。 相信…

给定条件找最小值c语言程序_根据给定条件最小化n的最小步骤

给定条件找最小值c语言程序Problem statement: 问题陈述&#xff1a; Given a number n, count minimum steps to minimize it to 1 performing the following operations: 给定数字n &#xff0c;执行以下操作&#xff0c;计算最少的步骤以将其最小化为1&#xff1a; Operat…

那个年代的苏联歌曲

小时候&#xff0c;不时听父亲提起电影《这里的黎明静悄悄》&#xff0c;怎么也想不到如此美丽的名字为什么要和战争联系起来。后来在大学看了这部电影之后&#xff0c;开始认为这名字是合适的&#xff0c;因为电影讲的是女性——战场中的女性&#xff0c;各自都怀揣着爱情去保…