为什么需要内核锁?
多核处理器下,会存在多个进程处于内核态的情况,而在内核态下,进程是可以访问所有内核数据的,因此要对共享数据进行保护,即互斥处理
有哪些内核锁机制?
(1)原子操作
atomic_t数据类型,atomic_inc(atomic_t *v)将v加1
原子操作比普通操作效率要低,因此必要时才使用,且不能与普通操作混合使用
如果是单核处理器,则原子操作与普通操作相同
(2)自旋锁
spinlock_t数据类型,spin_lock(&lock)和spin_unlock(&lock)是加锁和解锁
等待解锁的进程将反复检查锁是否释放,而不会进入睡眠状态(忙等待),所以常用于短期保护某段代码
同时,持有自旋锁的进程也不允许睡眠,不然会造成死锁——因为睡眠可能造成持有锁的进程被重新调度,而再次申请自己已持有的锁
如果是单核处理器,则自旋锁定义为空操作,因为简单的关闭中断即可实现互斥
///////////
互斥量与信号量的区别?(转载但找不到原文出处)
(1)互斥量用于线程的互斥,信号线用于线程的同步
这是互斥量和信号量的根本区别,也就是互斥和同步之间的区别
互斥:是指某一资源同时只允许一个访问者对其进行访问,具有唯一性和排它性。但互斥无法限制访问者对资源的访问顺序,即访问是无序的
同步:是指在互斥的基础上(大多数情况),通过其它机制实现访问者对资源的有序访问。在大多数情况下,同步已经实现了互斥,特别是所有写入资源的情况必定是互斥的。少数情况是指可以允许多个访问者同时访问资源
(2)互斥量值只能为0/1,信号量值可以为非负整数
也就是说,一个互斥量只能用于一个资源的互斥访问,它不能实现多个资源的多线程互斥问题。信号量可以实现多个同类资源的多线程互斥和同步。当信号量为单值信号量是,也可以完成一个资源的互斥访问
(3)互斥量的加锁和解锁必须由同一线程分别对应使用,信号量可以由一个线程释放,另一个线程得到
/////
mutex和spin lock的区别和应用(sleep-waiting和busy-waiting的区别)
信号量mutex是sleep-waiting。 就是说当没有获得mutex时,会有上下文切换,将自己、加到忙等待队列中,直到另外一个线程释放mutex并唤醒它,而这时CPU是空闲的,可以调度别的任务处理。
而自旋锁spin lock是busy-waiting。就是说当没有可用的锁时,就一直忙等待并不停的进行锁请求,直到得到这个锁为止。这个过程中cpu始终处于忙状态,不能做别的任务。
/////////////////////
例如在一个双核的机器上有两个线程(线程A和线程B),它们分别运行在Core0 和Core1上。 用spin-lock,coer0上的线程就会始终占用CPU。
另外一个值得注意的细节是spin lock耗费了更多的user time。这就是因为两个线程分别运行在两个核上,大部分时间只有一个线程能拿到锁,所以另一个线程就一直在它运行的core上进行忙等待,CPU占用率一直是100%;而mutex则不同,当对锁的请求失败后上下文切换就会发生,这样就能空出一个核来进行别的运算任务了。(其实这种上下文切换对已经拿着锁的那个线程性能也是有影响的,因为当该线程释放该锁时它需要通知操作系统去唤醒那些被阻塞的线程,这也是额外的开销)
///
总结
(1)Mutex适合对锁操作非常频繁的场景,并且具有更好的适应性。尽管相比spin lock它会花费更多的开销(主要是上下文切换),但是它能适合实际开发中复杂的应用场景,在保证一定性能的前提下提供更大的灵活度。
(2)spin lock的lock/unlock性能更好(花费更少的cpu指令),但是它只适应用于临界区运行时间很短的场景。而在实际软件开发中,除非程序员对自己的程序的锁操作行为非常的了解,否则使用spin lock不是一个好主意(通常一个多线程程序中对锁的操作有数以万次,如果失败的锁操作(contended lock requests)过多的话就会浪费很多的时间进行空等待)。
(3)更保险的方法或许是先(保守的)使用 Mutex,然后如果对性能还有进一步的需求,可以尝试使用spin lock进行调优。毕竟我们的程序不像Linux kernel那样对性能需求那么高(Linux Kernel最常用的锁操作是spin lock和rw lock)。
task_state=R
任务的状态,R:runnign, S:sleeping (TASK_INTERRUPTIBLE), D:disk sleep (TASK_UNINTERRUPTIBLE), T: stopped, T:tracing stop,Z:zombie, X:dead
jiffies
全局变量jiffies取值为自操作系统启动以来的时钟滴答的数目,在头文件中定义,数据类型为unsigned long volatile (32位无符号长整型)。
系统启动时,内核将该变量初始化为0,此后,每次时钟中断处理程序就会增加该变量的值。因为一秒内时钟中断的次数等于HZ, 所以jiffies一秒内增加的值也就为HZ。
jiffies转换为秒可采用公式:(jiffies/HZ)计算,系统运行时间以秒为单位,等于jiffies/Hz。
将秒转换为jiffies可采用公式:(seconds*HZ)计算。
// 输出了线程名,优先级,线程号,线程状态,带有『deamon』字样的线程表示守护线程,即DDMS中『*』线程
"main" prio=5 tid=1 Native
// 输出了线程组名,sCount被挂起次数,dsCount被调试器挂起次数,obj表示线程对象的地址,self表示线程本身的地址
| group="main" sCount=1 dsCount=0 obj=0x7541b3c0 self=0xb4cf6500
// sysTid是Linux下的内核线程id,nice是线程的调度优先级,cgrp是调度属组,sched分别标志了线程的调度策略和优先级,handle是线程的处理函数地址。
//获取唯一的系统内核线程ID (唯一),在linux下每一个进程都一个进程id,类型pid_t,可以由 getpid()获取。
//POSIX线程也有线程id,类型pthread_t,可以由 pthread_self()获取,线程id由线程库维护。
// 但是各个进程独立,所以会有不同进程中线程号相同节的情况。那么这样就会存在一个问题,我的进程p1中的线程pt1要与进程p2中的线程pt2通信怎么办,进程id不可以,线程id又可能重复,所以这里会有一个真实的线程id唯一标识,tid。glibc没有实现gettid的函数,所以我们可以通过linux下的系统调用 syscall(SYS_gettid) 来获得。
//syscall(SYS_gettid)获得的是本线程的ID 在本线程的任意位置,使用系统调用即可获取本线程的id,这样不需要使用全局数据存放线程ID了
| sysTid=4280 nice=-1 cgrp=default sched=0/0 handle=0xb6f5cb34
// state是调度状态;schedstat从 /proc/[pid]/task/[tid]/schedstat读出,三个值分别表示线程在cpu上执行的时间、线程的等待时间和线程执行的时间片长度,有的android内核版本不支持这项信息,得到的三个值都是0;utm是线程用户态下使用的时间值(单位是jiffies);stm是内核态下的调度时间值;core是最后执行这个线程的cpu核的序号。
| state=S schedstat=( 52155108 81807757 159 ) utm=2 stm=3 core=0 HZ=100
| stack=0xbe121000-0xbe123000 stackSize=8MB
| held mutexes=
//调用栈信息
native: #00 pc 00040984 /system/lib/libc.so (__epoll_pwait+20)
native: #01 pc 00019f5b /system/lib/libc.so (epoll_pwait+26)
native: #02 pc 00019f69 /system/lib/libc.so (epoll_wait+6)
native: #03 pc 00012c57 /system/lib/libutils.so (android::Looper::pollInner(int)+102)
native: #04 pc 00012ed3 /system/lib/libutils.so (android::Looper::pollOnce(int, int*, int*, void**)+130)
native: #05 pc 00082bed /system/lib/libandroid_runtime.so (android::NativeMessageQueue::pollOnce(_JNIEnv*, _jobject*, int)+22)
native: #06 pc 0000055d /data/dalvik-cache/arm/system@[email protected] (Java_android_os_MessageQueue_nativePollOnce__JI+96)
at android.os.MessageQueue.nativePollOnce(Native method)
at android.os.MessageQueue.next(MessageQueue.java:323)
at android.os.Looper.loop(Looper.java:135)
at android.app.ActivityThread.main(ActivityThread.java:5435)
at java.lang.reflect.Method.invoke!(Native method)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:735)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:625)
cmdline:这个主要是当前这个进程被运行时的command line,里面包括了运行时指定的一些参数,比如如果是mysqld的话就包括basedir==,datadir==,port=,socket=等等信息,你可以自己尝试一下。
cwd:current working directory,当前的工作目录
environ:这是个比较有用的文件,里面记录了当前进程的一些环境变量,比如一台机器上对同一个系统起多个实例(当然是不同端口、不同数据目录),而你想知道哪个进程对应的是在哪个数据目录起来的(可能是因为你想kill-9其中一个实例,因为你怕弄错,所以的确定哪个进程是对应哪个数据目录),那么此时你该怎么办呢?两者方法:1.strings /proc/pid/environ | grep PWD 2.tr \\0 \\n < /proc/pid/envrion | grep PWD 即可。当然里面还有很多的信息,你可以自己尝试。
exe:这个就是气这个进程的执行文件
fd:进程打开的文件描述符,我记得以前有人使用mysql遇到过two many openfiles的错误,这个就是打开太多的文件导致的,当然你如果只修改mysql里面的参数可能不会起作用,因为可能os上设置了一个比较小的数,所以要两者都调大。
fdinfo:跟上面一个一样,只不过只有文件描述符的值,没有表示这个文件描述符是对应打开的哪个文件。在/proc很多时候都是这样维护的:可能两个文件里面要表示的信息是一样的,但是有一个一般是以人能容易读懂的格式给出。
limits:这个跟fd有一点关联,因为这个里面限制了进程对系统资源的使用额度,比如前面说的你可以打开多少文件,具体的设定你可以修改这个文件/etc/securiry/limits.conf,里面也对每一项说得很清楚,只要会点E文,理解应该没问题
oom_adj/oom_score:这两个与linux的OOM机制有关的文件(关于OOM请看这里),oom_adj相当于一个因子,它值越大,在OOM时更容易被系统kill掉,但最终决定是否被kill的是oom_score,其实计算这个oom_score时,就是根据oom_adj来的,oom_adj更大,计算出来的oom_score就更大,也能容易在OOM时被系统kill掉。当然如果是很重要的服务现场,为了避免出现这种情况设置oom_adj=0就行,表示永远不会因为OOM被kill。
stat/status:这两者要表示的信息都是一样的,进程的基本状态,但是后一个是以人容易读懂的格式给出。