快速记住Linux内核的基本概念
打算给我们部门弄个内部分享。发现大家对一些底层知识的认知停留在一句一句的,比如听说JVM使用-XX:-UseBiasedLocking取消偏向锁可以提高性能,因为它只适用于非多线程高并发应用。使用数字对象的缓存-XX:AutoBoxCacheMax=20000比默认缓存-128~127要提高性能。对于JVM和Linux内核,操作系统没有系统的概念,遇到实际问题往往没有思路。所以我的内部分享,主要分为Linux部分,jvm部分和redis部分。这篇是Linux篇。学习思路为主,知识为辅。我也是菜鸟一枚~~不过是个钻石心的菜鸟,不怕别人知道我有多菜。
先说为什么我要去学习Linux内核。我在上家公司负责整个公司的搜索引擎。有一次很熟练的在一台虚拟机上新搭建了一套,压测到8000,额,报了一个NIO异常,说是:too many open files。当时查了一下,那台机器太破,和很多服务公用,内存快满了。所以换了台好点的机器就没有这个问题了。但是句柄超限到底是个什么东西呢?先来看看Linux内核的一些基本概念。
大局观嘛,先来看看unix的体系结构。
简单解释一下:任何计算机系统都包含一个基本的程序集合,它控制计算机硬件资源,提供程序运行环境。称为操作系统。在这个集合里,最重要的程序被称为内核,在系统启动时被装载。因为它相对较小,而且位于环境的核心。内核的接口被称为系统调用(system call)。公用函数库构建在系统调用接口之上,也可使用系统调用。shell是一个特殊的应用程序,为运行其他应用程序提供一个接口。
一些操作系统允许所有的用户程序直接与硬件部分进行交互,如MS-DOS。但是类Unix操作系统在胡勇应用程序前把与计算机物理组织相关的所有底层细节隐藏了。当程序想使用硬件资源时,必须向操作系统发出一个请求,内核对这个请求进行评估,如果允许使用这个资源,内核代表应用程序与相关的硬件部分进行交互。为了实施这种机制,现代操作系统依靠特殊的硬件特性来禁止用户程序直接与底层硬件部分打交道,或者直接访问任意的物理地址。硬件为CPU引入了至少两种不同的执行模式:用户程序的非特权模式和内核的特权模式。Unix把他们分别称为用户态(User Mode)和内核态(Kernel Model)。
我们平时敲的一些Linux命令,实际上都是对应的内核的C语言函数。比如cat xxx | grep 'x'。这里面两个命令用|连接起来,这个叫做“管道”。先用男孩纸惯用的职业一点的语言介绍一下:管道是一个广泛应用的进程间通信手段。其作用是在具有亲缘关系的进程之间传递消息,所谓有亲缘关系,是指有同一个祖先。可以是父子,兄弟或者祖孙等等。反正只要共同的祖先调用了pipe函数,打开的管道文件会在fork之后,被各个后代所共享。其本质是内核维护了一块缓冲区与管道文件相关联,对管道文件的操作,被内核转换成对这块缓冲区内存的操作。分为匿名管道和命名管道。
这里面包含了一些概念。进程的概念大家都应该很清楚:程序的执行实例被称为进程。UNIX系统确保每个进程都有一个唯一的数字表示符,称为进程ID(process ID),它是一个非负数。Linux很多命令都会将其显示出来。有3个用于进程控制的主要函数:fork,exec和waitpid。其中fork函数用来创建一个新进程,此进程是调用进程的一个副本,称为子进程。fork对父进程返回新的子进程的进程ID(一个非负整数),对子进程则返回0。因为fork创建一个新进程,所以说它被调用一次,但返回两次。
一个进程内的所有线程共享同一地址空间,文件描述符,栈以及进程相关的属性。因为它们能访问同一存储区,所以各线程在访问共享数据时需要采取同步措施以避免不一致性。说到这里大家都应该多少有些概念了:为什么进程开销大,线程涉及锁。
匿名管道是一个未命名的,单向管道,通过父进程和一个子进程之间传输数据。只能实现本地机器上两个进程之间的通信,而不能实现跨网络的通信。常用的比如Linux命令。
命名管道是进程间单向或双向管道,建立时指定一个名字,任何进程都可以通过该名字打开管道的另一端,可跨网络通信。
这是一个jvisualvm调试的截图,蓝框部分就相当于一个命名管道。
好,现在来回答一个问题:用户进程间通信主要哪几种方式?
刚才说的匿名管道和命名管道都算一种。除此之外,还有:信号,消息队列,共享内存,信号量和套接字。不用头疼,看到最后你很可能会有豁然开朗的感觉,学的东西终于可以串在一起了。
信号(signal):其实是软中断信号的简称。用来通知进程发生了异步事件。在软件层次上是对中断机制的一种模拟,在原理上,一个进程收到一个信号与处理器收到一个中断请求是一样的。信号是进程间通信机制中唯一的异步通信机制,一个进程不必通过任何操作来等待信号的到达。
收到信号的进程对各种信号有不同的处理方法,主要是三类:
1>类似中断的处理程序,对于需要处理的信号,进程可以指定处理函数,由该函数来处理。
2>忽略某个信号,对该信号不做任何处理。
3>对该信号的处理保留系统的默认值,这种缺省操作,对大部分的信号的缺省操作是让进程终止。进程通过系统调用signal来指定进程对某个信号的处理行为。
下面是window的信号列表
Linux也是用kill -l命令:
1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL
5) SIGTRAP 6) SIGABRT 7) SIGBUS 8) SIGFPE
9) SIGKILL 10) SIGUSR1 11) SIGSEGV 12) SIGUSR2
13) SIGPIPE 14) SIGALRM 15) SIGTERM 17) SIGCHLD
18) SIGCONT 19) SIGSTOP 20) SIGTSTP 21) SIGTTIN
22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ
26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO
30) SIGPWR 31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1
36) SIGRTMIN+2 37) SIGRTMIN+3 38) SIGRTMIN+4 39) SIGRTMIN+5
40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8 43) SIGRTMIN+9
44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13
52) SIGRTMAX-12 53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9
56) SIGRTMAX-8 57) SIGRTMAX-7 58) SIGRTMAX-6 59) SIGRTMAX-5
60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2 63) SIGRTMAX-1
64) SIGRTMAX
我在用gdb命令运行调试C语言程序的时候经常可以看到这些信号量。
再来看消息队列。消息队列提供了一种从一个进程向另一个进程发送一个数据块的方法。每个数据块都被认为含有一个类型,接收进程可以独立的接收含有不同类型的数据结构。可以通过发送消息来避免命名管道的同步和阻塞问题。但是消息队列和命名管道一样,每个数据块都有一个最大长度的限制。
共享内存就是允许两个不相关的进程访问同一个逻辑内存。共享内存是在两个正在运行的进程之间共享和传递数据的一种非常有效的方式。不同进程之间共享的内存通常安排为同一段物理内存。进程可以将同一段共享内存连接到他们自己的地址空间中,所有进程都可以访问共享内存中的地址。
信号量:为了防止出现因多个程序同时访问一个共享资源而引发的一系列问题,我们需要一种方法,它可以通过生成并使用令牌来授权,在任一时刻只能有一个执行线程访问代码的临界区域。临界区域是指执行数据更新的代码需要独占式的执行。而信号量就可以提供这样的一种访问机制。让一个临界区同一时间只有一个线程在访问它,也就是说信号量是用来协调对共享资源访问的。
套接字:这种通信机制使得客户端/服务器的开发工作既可以在本地单机上进行,也可以跨网络进行。它的特性有三个属性确定:域(domain),类型(type)和协议(protocol)。简单的说:源IP地址和目的IP地址以及源端口号和目的端口号的组合成为套接字。
下面介绍一下通信过程,里面涉及一些C语言的函数,不用怕,眼熟即可。如果你学习过nio,你会发现这些是很常接触的。
要想使不同主机的进程通信,就必须使用套接字,套接字是用socket()函数创建,如果需要C/S模式,则需要把server的套接字与地址和端口绑定起来,使用bind(),当上述操作完成后,便可使用listen()来监听这个端口,如果有其他程序来connect,那么server将会调用accept()来接受这个申请并为其服务。client是调用connect()来建立与server之间的连接,这时会使用三次握手来建立一条数据链接。当连接被建立后,server与client便可以通信了,通信可以使用read()/write(),send()/recv(),sendto()/recvfrom()等函数来实现,但是不同的函数作用和使用位置是不同的。当数据传送完后,可以调用close()来关闭server与client之间的链接。
到此,本篇文章的主要内容就没有了,基本就在介绍一个东西:linux内核的进程通信。这是学习任何高级编程语言nio部分的基础。下面引入一些辅助理解的概念。
文件句柄:在文件I/O中,要从一个文件读取数据,应用程序首先要调用操作系统函数并传送文件名,并选一个到该文件的路径来打开文件。该函数取回一个顺序号,即文件句柄(file handle),该文件句柄对于打开的文件是唯一的识别依据。一个句柄就是你给一个文件,设备,套接字(socket)或者管道的一个名字,以便帮助你记住你证处理的名字,并隐藏某些缓存等的复杂性。说白了就是文件指针啦。
文件描述符:内核利用文件描述符来访问文件。打开现存文件或新建文件时,内核会返回一个文件描述符。读写文件也需要使用文件描���符来指定待读写的文件。文件描述符形式上是非负整数,实际上它是一个索引值,指向内核为每一个进程所维护的该进程打开文件的记录表。当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符。在程序设计中,一些涉及底层的程序编写往往会围绕着文件描述符展开。但是文件描述符往往值适用于unix,linux这样的操作系统。习惯上,标准输入的文件描述符是0,标准输出是1,标准错误是2.
`/letv/apps/jdk/bin/java -DappPort=4 $JAVA_OPTS -cp $PHOME/conf:$PHOME/lib/* com.letv.mms.transmission.http.VideoFullServerBootstrap $1 $3 > /dev/null 2>&1 &`
自己部署过java后台程序的话,对上面的shell命令应该都能理解。 /dev/null 2>&1 这里面的2就是文件描述符,这个是将错误输出到文件。
这两个概念比较绕,不用过多区分,可以当成一回事来理解。打开文件(open files)包括文件句柄但不仅限于文件句柄,由于lnux所有的事务都以文件的形式存在,要使用诸如共享内存,信号量,消息队列,内存映射等都会打开文件,但这些不会占用文件句柄。查看进程允许打开的最大文件句柄数的linux命令:ulimit -n
好了,今天的概念都介绍完了,回到最初的问题:too many open files。 当时的机器破,内存快满了。所以搜索引擎走的是索引文件,有很多的IO操作,共享内存和内存映射这块的文件肯定是供不上的,报错了。萦绕在心头两年的问题稍微有点认知了。