转自:http://www.cnblogs.com/plinx/archive/2013/01/28/2873121.html

在linux内核中,主要的静态发生于以下几种情况:

  1、对称多处理器(SMP)的多个CPU:

  多个CPU共同使用系统总线,可访问共同点的外设和存储器。

  2、单CPU内核进程与抢占它的进程:

  一个进程的执行可被另一高优先级进程打断。

  3、中断(硬中断、软中断、Tasklet,底半部)与进程之间:

  中断可以打断正在执行的进程,若访问该进程正在访问的空间,将引发竞态。

  上述并发的发生出了SMP是真正的并行以外,其他的都是“宏观并行,微观串行”的,但其引发的实质问题与SMP相似。

  访问共享字段的代码区域成为临界区(critical sections)

  中断屏蔽:

  使用方法

local_irq_disable()  //屏蔽中断
...
critical section //临界区
...
local_irq_enable() //开中断

  local_irq_disable/enable只能禁止/使能本CPU内的中断,不能解决SMP多CPU引发的竞态,故不推荐使用,其适宜于自旋锁联合使用。

  原子操作:

  使用方法

//设置原子变量的值
static __inline__ void atomic_set(atomic_t *v, int i); //输入原子指针,将原子变量置为i
atomic_t v = ATOMIC_INIT(i);                //直接将v的原子变量初始化为i //原子变量的基本操作
atomic_read(atmic_t *v);          //读值
void atomic_add/sub(int i, atomic_t *v); //加/减i操作
void atomic_inc/dec(atomic_t *v);     //自加/自减操作
int atomic_inc/dec_test(atomic_t *v);  //自加/自减后测试,为0返回ture,否则返回false
int atomic_sub_and_test(int i, atomic_t *v); //减i后测试,为0返回ture,否则返回false
int atomic_add/sub_return(int i, atomic_t *v); //加/减i后return
int atomic_inc/dec_return(atomic_t *v); //自加/自减后return 

  一个操作例程

static atomic_t xxx_atomic = ATOMIC_INIT(1); //初始化

static int xxx_open(struct inode *inode, struct file *filp)
{
...
if(!atomic_dec_and_test(&xxx_atomic)){
//首次调用xxx_atomic时,其为1,则test后返回ture
atomic_inc(&xxx_availavle);
//再次调用是执行if(!false){}的内容
return - EBUSY;
}
...
return 0;
} static int xxx_release(struct inode *inode, struct file *filp)
{
atomic_inc(&xxx_atomic);
//清楚调用,使其变回初值
return 0;
}
//这里只是举例,并非一定要先dec_test然后inc, 只要前后的操作,不互相冲突, 实现再次调用时返回忙,而释放时使原子变量回到调用前的值即可

  自旋锁(spin lock):

  自旋锁是一种典型的对临界资源进行互斥访问的手段。

  为了获得一个自旋锁,在某CPU上运行的代码需先执行一个原子操作,该操作测试并设置(test-and-set)某个内存变量,由于它是原子操作,所以在该操作完成之前,其他单元无法访问这个内存变量。

  自旋锁有四种操作:

//定义自旋锁
spinlock_t lock; //初始化自旋锁
spin_lock_init(lock); //获得自旋锁
spin_lock(lock); //若获得则返回,否则自旋 tryspin_lock(lock); //若获得返回真,否则返回假 //释放自旋锁
spin_unlock(lock);
//自旋锁主要针对SMP或单CPU但内核可以抢占的情况,其他系统或不可抢占的CPU中,自旋锁为空操作

  驱动程序中应该谨慎使用自旋锁,原因如下:

  1、自旋锁是忙等待锁,当等待时间较长的时候将降低系统系能;

  2、自旋锁可能导致系统死锁(锁陷阱);

  3、自旋锁锁定器件不能调用可能引起进程调度的函数。如果进程获得自旋锁之后再阻塞赛,如调用copy_from_user()、copy_to_user()、kmalloc()和msleep()等函数,则可能导致内核崩溃。

  接下来深入研究一下自旋锁的工作过程

//此处的spin_lock是针对配置了SMP的内核
static inline void spin_lock(spinlock_t *lock)
{
raw_spin_lock(&lock->rlock);
}
#define raw_spin_lock(lock) _raw_spin_lock(lock)
void __lockfunc _raw_spin_lock(raw_spinlock_t *lock)
{
__raw_spin_lock(lock);
}
EXPORT_SYMBOL(_raw_spin_lock);
static inline void __raw_spin_lock(raw_spinlock_t *lock)  //这里开始,便是自旋锁实际的执行过程了
{
preempt_disable();                   
      //禁止抢占
spin_acquire(&lock->dep_map, 0, 0, _RET_IP_);    
      //判断锁是否为0,lock为0则可以抢占,lock为1则不可抢占
LOCK_CONTENDED(lock, do_raw_spin_trylock, do_raw_spin_lock);
      //使lock为1,即加锁
}
//接下来细看一下每个函数的内存实现 //首先是禁止抢占函数
#define preempt_disable() \
do { \
inc_preempt_count(); \
      //禁止抢占函数的本质就是将preempt_count+1,具体如下(1)
barrier(); \
      //barrier函数是一个内存屏障函数,具体如下(2)
} while (0) //(1)
#define preempt_count() (current_thread_info()->preempt_count) #define inc_preempt_count() add_preempt_count(1)
#define add_preempt_count(val) do { preempt_count() += (val); } while (0)
//即最后返回为 current_thread_info()->preempt_count+1
//(2)
//在linux/arch/cris/include/asm/system.h文件中,可以看到mb函数族的本质就是barrier
#define barrier() __asm__ __volatile__("": : :"memory") //其实是一个空操作
#define mb() barrier() //读写屏障
#define rmb() mb() //读屏障
#define wmb() mb() //写屏障
//引用大师的讲解,CPU越过内存屏障后,将刷新自己对存储器的缓冲状态。这条语句实际上不生成任何代码,但可使gcc在barrier()之后刷新寄存器对变量的分配。
//屏障之所以起到作用,是因为在执行空操作的时候是不允许其他进程对寄存器调用的,而这样的做法,保证了在屏障前执行的操作,和在屏障后执行的操作不互相影响,具体也可参阅《LDK》,上面有个简洁易明的图例 //接着来看一下spin_acquire()与LOCK_CONTENTDED()
#ifdef CONFIG_DEBUG_LOCK_ALLOC
# ifdef CONFIG_PROVE_LOCKING
# define spin_acquire(l, s, t, i) lock_acquire(l, s, t, 0, 2, NULL, i)
# define spin_acquire_nest(l, s, t, n, i) lock_acquire(l, s, t, 0, 2, n, i)
# else //CONFIG未设置CONFIG_PROVE_LOCKING情况下
# define spin_acquire(l, s, t, i) lock_acquire(l, s, t, 0, 1, NULL, i)
# define spin_acquire_nest(l, s, t, n, i) lock_acquire(l, s, t, 0, 1, NULL, i)
# endif
# define spin_release(l, n, i) lock_release(l, n, i)
#else //CONFIG未设置CONFIG_DEBUG_LOCK_ALLOC情况下
# define spin_acquire(l, s, t, i) do { } while (0)
# define spin_release(l, n, i) do { } while (0)
#endif
  首先来仔细看一下spin_acquire()
void lock_acquire(struct lockdep_map *lock, unsigned int subclass,
int trylock, int read, int check,
struct lockdep_map *nest_lock, unsigned long ip)
//入口参数对应为*lock = &lock->dep_map, subclass = 0, trylock = 0,
//read = 0, check = 1/2, *nest_lock = NULL, ip = _REP_IP_
{
unsigned long flags; if (unlikely(current->lockdep_recursion))
//current->lock_recursion不为0则返回
return; raw_local_irq_save(flags); //save flags
check_flags(flags);     //暂时保留、看不是太懂 current->lockdep_recursion = 1; //这里将其置1了,可想而知,在另外一个进程调用lock_acquire来获取该dep_map的时候,将直接返回
trace_lock_acquire(lock, subclass, trylock, read, check, nest_lock, ip);
__lock_acquire(lock, subclass, trylock, read, check,
irqs_disabled_flags(flags), nest_lock, ip, 0);
//接下来在详解该函数,(2) current->lockdep_recursion = 0;
//再将其置0,使其他进程可以正常调用lock_acquire raw_local_irq_restore(flags);
//接下来再详解该函数,(1)
}
EXPORT_SYMBOL_GPL(lock_acquire); //(1)
#define raw_local_irq_restore(flags) \
do { \
typecheck(unsigned long, flags); \
arch_local_irq_restore(flags); \
//这个暂时不讨论了
} while (0) #define typecheck(type,x) \
//一个检验类型的函数,type 与 x 类型相同则返回1
({ type __dummy; \
typeof(x) __dummy2; \
(void)(&__dummy == &__dummy2); \
1; \
}) //(2)
static int __lock_acquire(struct lockdep_map *lock, unsigned int subclass,
int trylock, int read, int check, int hardirqs_off,
struct lockdep_map *nest_lock, unsigned long ip,
int references)
//传入参数 *lock = &lock->dep_map, subclass = 0, trylock = 0,
//read = 0, check = 1/2, hardirqs_off = irqs_disabled_flags(flags),
//*nest_lock = NULL, ip = _RET_IP_
//这个函数确实难懂了些= =、姑且略过
{
struct task_struct *curr = current;
struct lock_class *class = NULL;
struct held_lock *hlock;
unsigned int depth, id;
int chain_head = 0;
int class_idx;
u64 chain_key; if (!prove_locking)
check = 1; if (unlikely(!debug_locks))
return 0;
/*
* Lockdep should run with IRQs disabled, otherwise we could
* get an interrupt which would want to take locks, which would
* end up in lockdep and have you got a head-ache already?
*/
if (DEBUG_LOCKS_WARN_ON(!irqs_disabled()))
return 0; if (lock->key == &__lockdep_no_validate__)
check = 1; if (subclass < NR_LOCKDEP_CACHING_CLASSES)
class = lock->class_cache[subclass];
/*
* Not cached?
*/
if (unlikely(!class)) {
class = register_lock_class(lock, subclass, 0);
if (!class)
return 0;
}
atomic_inc((atomic_t *)&class->ops);
if (very_verbose(class)) {
printk("\nacquire class [%p] %s", class->key, class->name);
if (class->name_version > 1)
printk("#%d", class->name_version);
printk("\n");
dump_stack();
} /*
* Add the lock to the list of currently held locks.
* (we dont increase the depth just yet, up until the
* dependency checks are done)
*/
depth = curr->lockdep_depth;
/*
* Ran out of static storage for our per-task lock stack again have we?
*/
if (DEBUG_LOCKS_WARN_ON(depth >= MAX_LOCK_DEPTH))
return 0; class_idx = class - lock_classes + 1; if (depth) {
hlock = curr->held_locks + depth - 1;
if (hlock->class_idx == class_idx && nest_lock) {
if (hlock->references)
hlock->references++;
else
hlock->references = 2; return 1;
}
} hlock = curr->held_locks + depth;
/*
* Plain impossible, we just registered it and checked it weren't no
* NULL like.. I bet this mushroom I ate was good!
*/
if (DEBUG_LOCKS_WARN_ON(!class))
return 0;
hlock->class_idx = class_idx;
hlock->acquire_ip = ip;
hlock->instance = lock;
hlock->nest_lock = nest_lock;
hlock->trylock = trylock;
hlock->read = read;
hlock->check = check;
hlock->hardirqs_off = !!hardirqs_off;
hlock->references = references;
#ifdef CONFIG_LOCK_STAT
hlock->waittime_stamp = 0;
hlock->holdtime_stamp = lockstat_clock();
#endif if (check == 2 && !mark_irqflags(curr, hlock))
return 0; /* mark it as used: */
if (!mark_lock(curr, hlock, LOCK_USED))
return 0; /*
* Calculate the chain hash: it's the combined hash of all the
* lock keys along the dependency chain. We save the hash value
* at every step so that we can get the current hash easily
* after unlock. The chain hash is then used to cache dependency
* results.
*
* The 'key ID' is what is the most compact key value to drive
* the hash, not class->key.
*/
id = class - lock_classes;
/*
* Whoops, we did it again.. ran straight out of our static allocation.
*/
if (DEBUG_LOCKS_WARN_ON(id >= MAX_LOCKDEP_KEYS))
return 0; chain_key = curr->curr_chain_key;
if (!depth) {
/*
* How can we have a chain hash when we ain't got no keys?!
*/
if (DEBUG_LOCKS_WARN_ON(chain_key != 0))
return 0;
chain_head = 1;
} hlock->prev_chain_key = chain_key;
if (separate_irq_context(curr, hlock)) {
chain_key = 0;
chain_head = 1;
}
chain_key = iterate_chain_key(chain_key, id); if (!validate_chain(curr, lock, hlock, chain_head, chain_key))
return 0; curr->curr_chain_key = chain_key;
curr->lockdep_depth++;
check_chain_key(curr);
#ifdef CONFIG_DEBUG_LOCKDEP
if (unlikely(!debug_locks))
return 0;
#endif
if (unlikely(curr->lockdep_depth >= MAX_LOCK_DEPTH)) {
debug_locks_off();
printk("BUG: MAX_LOCK_DEPTH too low!\n");
printk("turning off the locking correctness validator.\n");
dump_stack();
return 0;
} if (unlikely(curr->lockdep_depth > max_lockdep_depth))
max_lockdep_depth = curr->lockdep_depth; return 1;
}

  继续往下看一下LOCK_CONTENDED()

//入口参数为(lock, do_raw_spin_trylock, do_raw_spin_lock)
#define LOCK_CONTENDED(_lock, try, lock) \
do { \
if (!try(_lock)) { \
//这里实际为 do_raw_spin_trylock(lock), 具体如下(1)
lock_contended(&(_lock)->dep_map, _RET_IP_); \
//详解如下(2)
lock(_lock); \
//这里实际为 do_raw_spin_lock(lock), 具体如下(3)
} \
lock_acquired(&(_lock)->dep_map, _RET_IP_); \
//详解如下(4)
} while (0) //(1)
static inline int do_raw_spin_trylock(raw_spinlock_t *lock)
{
return arch_spin_trylock(&(lock)->raw_lock);
}
# define arch_spin_trylock(lock) ({ (void)(lock); 1; })
//实际返回值都是为1,写个小程序验证下就明白了
//在锁机制中, 若上锁了则lock 为1, 否则 为0
//(2)
//好吧、到此为止先了……这里的函数真的比较深、能力所限看不太懂
void lock_contended(struct lockdep_map *lock, unsigned long ip)
{
unsigned long flags; if (unlikely(!lock_stat))
return; if (unlikely(current->lockdep_recursion))
return; raw_local_irq_save(flags);
check_flags(flags);
current->lockdep_recursion = 1;
trace_lock_contended(lock, ip);
__lock_contended(lock, ip);
current->lockdep_recursion = 0;
raw_local_irq_restore(flags);
}
EXPORT_SYMBOL_GPL(lock_contended);

  一个自旋锁机制的内核实现确实是博大精深。

  若只挑简单的地方来理解,可以只理解总结为如下步奏来实现一个自旋锁

  1、声明锁变量

  2、上锁

  3、临界区

  4、解锁

  顺序锁(seqlock):

  顺序锁是对读写锁的一种优化,若使用顺序锁,读与写操作不阻塞,只阻塞同种操作,即读与读/写与写操作。

  写执行单元的操作顺序如下:

//获得顺序锁
void write_seqlock(seqlock_t *s1);
int write_tryseqlock(seqlock_t *s1);
write_seqlock_irqsave(lock, flags)
write_seqlock_irq(lock)
write_seqlock_bh(lock) //释放顺序锁
void write_sequnlock(seqlock_t *s1);
write_sequnlock_irqrestore(lock, flags)
write_sequnlock_irq(lock)
write_sequnlock_bh(lock)

  读执行单元的操作顺序

//读开始
unsinged read_seqbegin(const seqlock_t *s1);
read_seqbegin_irqsave(lock, flags) //重读,读执行单元在访问完被顺序锁s1保护的共享资源后需要调用该函数来检查在读操作器件是否有写操作,如果有,读执行单元需要从新读一次。
int reead_seqretry(const seqlock_t *s1, unsigned iv);
read_seqretry_irqrestore(lock, iv, flags)

  读-拷贝-更新(RCU,Read-Copy Update):

  对于被RCU保护的功效数据结构,读执行单元不需要获得任何锁就可以访问它,不使用原子指令,而且在除alpha的所有架构上也不需要内存屏障(Memory Barrier),因此不会导致锁竞争、内存延迟以及流水线停滞。使用RCU的写执行单元在访问它前需要首先拷贝一个副本,然后对副本进行修改,最后使用一个回调机制在适当的实际把指向原来数据的指针重新指向新的被修改的数据,这个时机就是所有引用该数据的CPU都退出对共享数据的操作的时候。

  RCU可以看作读写锁的高性能版本,相比读写锁,RCU的优点在于既允许多个读执行单元同时访问被保护的数据,又允许多个读执行单元和多个写执行单元同时访问被保护的数据。但是RCU不能替代读写锁,因为如果写比较多时,对读执行单元的性能提高不能弥补写执行单元导致的损失。

  具体操作:略

  信号量(semaphore):

  信号量是用于保护临界区的一种常用方法,它的使用方式和自旋锁类似。

  相同点:只有得到信号量的进程才能执行临界区的代码。

  (linux自旋锁和信号量锁采用的都是“获得锁-访问临界区-释放锁”,可以称为“互斥三部曲”,实际存在于几乎所有多任务操作系统中)

  不同点:当获取不到信号量时,进程不会原地打转而是进入休眠等待状态。

//信号量的结构
struct semaphore sem; //初始化信号量
void sema_init(struct semaphore *sem, int val)
//常用下面两种形式
#define init_MUTEX(sem) sema_init(sem, 1)
#define init_MUTEX_LOCKED(sem) sema_init(sem, 0)
//以下是初始化信号量的快捷方式,最常用的
DECLARE_MUTEX(name) //初始化name的信号量为1
DECLARE_MUTEX_LOCKED(name) //初始化信号量为0 //常用操作
DECLARE_MUTEX(mount_sem);
down(&mount_sem); //获取信号量
...
critical section //临界区
...
up(&mount_sem); //释放信号量

  信号量用于同步时只能唤醒一个执行单元,而完成量(completion)用于同步时可以唤醒所有等待的执行单元。

  自旋锁与互斥锁的选择

  1、当锁 不能被获取到时,使用信号量的开销是进程上下文切换时间Tsw,使用自旋锁的开始是等待获取自旋锁的时间Tcs,若Tcs比较小,则应使用自旋锁,否则应使用信号量

  2、信号量锁保护的临界区可以包含引起阻塞的代码,而自旋锁则却对要避免使用包含阻塞的临界区代码,否则很可能引发锁陷阱

  3、信号量存在于进程上下文,因此,如果被保护的共享资源需要在中断或软中断情况下使用,则在信号量和自旋锁之间只能选择自旋锁。当然,如果一定要使用信号量,则只能通过down_trylock()方式进行,不能获取就立即返回以避免阻塞。

  读写信号量:与读写信号锁相似,是一种放宽粒度的实现机制。

  

  小结一下并发控制这一部分:

   现在的处理器基本上都是SMP类型的,而且在新的内核版本中,基本上都支持抢占式的操作,在linux中很多程序都是可重入的,要保护这些数据,就得使用不同的锁机制。

  而锁机制的基本操作过程其实大同小异的,声明变量,上锁,执行临界区代码,然后再解锁。

  不同点在于,可以重入的限制不同,有的可以无限制重入,有的只允许异种操作重入,而有的是不允许重入操作的。

  而在考虑不同的锁机制的使用时,也要考虑CPU处理的效率问题,对于不同的代码长度,不同的代码执行时间,选择一个好的锁对CPU的良好使用有很大的影响,否则将造成浪费。

linux设备驱动中的并发控制【转】的更多相关文章

  1. 蜕变成蝶~Linux设备驱动中的并发控制

    并发和竞争发生在两类体系中: 对称多处理器(SMP)的多个CPU 内核可抢占的单CPU系统 访问共享资源的代码区域称为临界区(critical sections),临界区需要以某种互斥机制加以保护.在 ...

  2. Linux设备驱动中的并发控制

    1.并发是指多个执行单元同时.并行的执行.并发的执行单元对共享资源的访问很容易导致竞态. 在 Linux 内核中,主要的竞态发生于如下几种情况: ①对称多处理器(SMP)的多个 CPU ②单CPU内进 ...

  3. Linux设备驱动中的阻塞和非阻塞I/O

    [基本概念] 1.阻塞 阻塞操作是指在执行设备操作时,托不能获得资源,则挂起进程直到满足操作所需的条件后再进行操作.被挂起的进程进入休眠状态(不占用cpu资源),从调度器的运行队列转移到等待队列,直到 ...

  4. 蜕变成蝶~Linux设备驱动中的阻塞和非阻塞I/O

    今天意外收到一个消息,真是惊呆我了,博客轩给我发了信息,说是俺的博客文章有特色可以出本书,,这简直让我受宠若惊,俺只是个大三的技术宅,写的博客也是自己所学的一些见解和在网上看到我一些博文以及帖子里综合 ...

  5. Linux设备驱动中的阻塞和非阻塞I/O <转载>

    Green 博客园 首页 新随笔 联系 订阅 管理 Linux设备驱动中的阻塞和非阻塞I/O   [基本概念] 1.阻塞 阻塞操作是指在执行设备操作时,托不能获得资源,则挂起进程直到满足操作所需的条件 ...

  6. Linux设备驱动中的IO模型---阻塞和非阻塞IO【转】

    在前面学习网络编程时,曾经学过I/O模型 Linux 系统应用编程——网络编程(I/O模型),下面学习一下I/O模型在设备驱动中的应用. 回顾一下在Unix/Linux下共有五种I/O模型,分别是: ...

  7. Linux设备驱动中的软件架构思想

    目录 更新记录 一.Linux驱动的软件架构 1.1 出发点 1.2 分离思想 1.3 分层思想 二.platform设备驱动 2.1 platform设备 2.2 platform驱动 2.3 pl ...

  8. Linux设备驱动中的异步通知与异步I/O

    异步通知概念: 异步通知的意识是,一旦设备就绪,则主动通知应用程序,这样应用程序根本就不需要查询设备状态,这一点非常类似于硬件上的“中断”概念,比较准确的称谓是“信号驱动的异步IO”,信号是在软件层次 ...

  9. Linux设备驱动中的ioctl

    memdev.h #ifndef _MEMDEV_H #define _MEMDEV_H #define MEM_MAGIC 'm' #define MEM_RESTART _IO(MEM_MAGIC ...

随机推荐

  1. LeetCode - 70. Climbing Stairs(0ms)

    You are climbing a stair case. It takes n steps to reach to the top. Each time you can either climb ...

  2. erc20代币合约

    看这篇文章需要对以太坊,智能合约,代币等概念有基本的了解. 什么是ERC20 可以把ERC20简单理解成以太坊上的一个代币协议,所有基于以太坊开发的代币合约都遵守这个协议.遵守这些协议的代币我们可以认 ...

  3. Pro Git - 笔记1

    Getting Started About Version Control Local Version Control Systems Centralized Version Control Syst ...

  4. Unity UGUI 图片 轴对称效果 减少资源

    制作UI的过程中,为了节省资源,对称的图一般美术切一半给我们 手动拼图 有时会出现拼接处出现裂缝或重叠 调整大小时也不方便 得一块一块调整 所以就用BaseMeshEffect 的ModifyMesh ...

  5. 【log4net】- 日志使用教程

    一.log4net简介: 1. Log4net的优点: 几乎所有的大型应用都会有自己的用于跟踪调试的API.因为一旦程序被部署以后,就不太可能再利用专门的调试工具了.然而一个管理员可能需要有一套强大的 ...

  6. hbase表的写入

    hbase列式存储给我们画了一个很美好的大饼,好像有了它,很多问题都可以轻易解决.但在实际的使用过程当中,你会发现没有那么简单,至少一些通用的准则要遵守,还需要根据业务的实际特点进行集群的参数调整,不 ...

  7. C语言100例01 PHP版(练习)

    题目:有1.2.3.4个数字,能组成多少个互不相同且无重复数字的三位数?都是多少? 程序分析:可填在百位.十位.个位的数字都是1.2.3.4.组成所有的排列后再去 掉不满足条件的排列. 代码: for ...

  8. 【bzoj4976】宝石镶嵌 乱搞+dp

    题目描述 从$n$个数中选出$n-k$个,使得它们的二进制或(or)最大.输出这个值. 输入 第一行包含两个正整数$n,k(2\le n\le 100000,1\le k\le 100,k<n) ...

  9. 【bzoj3362/3363/3364/3365】[Usaco2004 Feb]树上问题杂烩 并查集/树的直径/LCA/树的点分治

    题目描述 农夫约翰有N(2≤N≤40000)个农场,标号1到N,M(2≤M≤40000)条的不同的垂直或水平的道路连结着农场,道路的长度不超过1000.这些农场的分布就像下面的地图一样, 图中农场用F ...

  10. sql in()批量操作

    //批量修改 update 表A   set A.name='n'  where   A.id    in(字符串); //批量删除 delete  from    表名称 where  列名称   ...