linux系统知识 - 进程&线程
作者:Vamei 出处:http://www.cnblogs.com/vamei 欢迎转载,也请保留这段声明。谢谢!
参考链接
http://www.cnblogs.com/vamei/archive/2012/09/20/2694466.html
http://www.cnblogs.com/vamei/archive/2012/10/09/2715393.html
背景知识
指令:计算机能做的事情其实非常简单,比如计算两个数之和、寻找到内存中的某个地址。这些最基础的计算机动作称为指令。
程序:一系列指令所构成的集合。通过程序,我们可以让计算机完成复杂的动作。程序大部分时候被存储为可执行文件。
进程:进程是程序的一个具体实现,是正在执行的程序。
进程创建
计算机开机时,内核只创建了一个init进程。剩下的所有进程都是init进程通过fork机制(新进程通过老进程复制自身得到)建立的。
进程存活于内存中,每个进程在内存中都有自己的一片空间
fork
fork是一个系统调用。
当进程fork时,linux在内存中开辟出新的一片内存空间给新的进程,并将老的进程空间的内容复制到新的进程空间中,此后两个进程同时运行。
fork通常作为一个函数被调用。这个函数会有两次返回,将子进程的PID返回给父进程,0返回给子进程。
通常在调用fork函数之后,程序会设计一个if选择结构。当PID等于0时,说明该进程为子进程,那么让它执行某些指令,比如说使用exec库函数(library function)读取另一个程序文件,并在当前的进程空间执行 (这实际上是我们使用fork的一大目的:为某一程序创建进程);而当PID为一个正整数时,说明为父进程,则执行另外一些指令。由此,就可以在子进程建立之后,让它执行与父进程不同的功能。
进程运行
进程内存使用(内存地址由高到低)
Stack(堆栈)
以帧(stack frame)为单位。
帧中存储当前激活函数的参数和局部变量,以及该函数的返回地址。
当程序调用函数时,stack向下增长一帧。
当激活函数返回时,会从栈中弹出(pop,读取并从栈中删除)该帧,并根据帧中记录的返回地址,经控制权交给返回地址所指向的指令
Unused Area
Heap(堆)
Global Data
从低到高存放常量、已初始化的全局/静态变量、未初始化的全局/静态变量
Text(instruction codes)
存放指令(也就是代码段)
图示
注意点:
1.最下方的帧和Global Data一起,构成了当前的环境。当前激活的函数可以从环境中调取需要的变量
2.Text和Global data在进程一开始的时候就确定了,并在整个进程中保持固定大小。
进程附加信息
除了进程自身内存中的内容,每个进程还要包括一些附加信息,包括PID、PPID、PGID等,用来说明进程身份、进程关系以及其他统计信息。
这些信息保存在内核的内存空间中,内核为每个进程在内核的内存空间中保存一个变量(task_struct结构体)以保存上述信息。
内核可以通过查看自己空间中各个进程的附加信息就能知道进程的情况,而不用进入到进程自身的空间。
每个进程的附加信息中有位置专门用于保存接收到的信号。
栈
进程运行的过程中,通过调用和返回函数,控制权不断在函数间转移。
进程在调用函数的时候,原函数的帧保留有离开原函数时的状态,并为新的函数开辟所需的帧空间。
在调用函数返回时,该函数的帧所占据的空间随着帧pop而清空。进程再次回到原函数的帧中所保留的状态,并根据返回地址所指向的指令继续执行。
上面的过程不断继续,栈不断增长或减小直到main()函数返回,栈完全清空,进程结束。
堆
当程序使用malloc时,堆会向上增长,增长的部分就是malloc从内存中申请的空间。
malloc申请的空间会一直存在,直到使用free系统调用来释放,或者进程结束。
内存泄漏 - 没有释放不再使用的堆空间,导致堆不断增大,可用内存不断减少。
栈溢出 Stack overflow
栈和堆的大小会随着进程的运行增大或者变小,当栈顶和堆相遇时,该进程再无可用内存。则进程栈溢出,进程中止。
如果清理及时,依然栈溢出,则需要增加物理内存。
进程组
每个进程属于一个进程组,每个进程组可以包含多个进程。
进程组会有一个进程组领导进程(process group id),领导进程的PID成为进程组的ID,即PGID。
领导进程可以先终结,此时进程组依然存在,并持有相同的PGID,知道进程组中最后一个进程终结
进程组的重要作用是可以发信号给进程组。进程组的所有进程都会收到该信号
会话session
多个进程组构成一个会话。
会话是由其中的进程建立的,该进程叫做会话的领导进程(session leader),领导进程的PID成为识别会话的SID(session id)
会话中的每一个进程组称为一个工作(job)。
会话可以有一个进程组成为会话的前台工作,其他进程组是后台工作。
每个会话可以连接一个控制终端。
当控制终端有输入输出时,或者由终端产生的信号(ctrl+z/ctrl+c),都会传递给会话的前台工作。
一个命令可以末尾加上&让它后台运行。
可以通过fg从后台拿出工作,使其变为前台工作
bash支持工作控制,sh不支持。
前台工作
独占stdin、(独占命令行窗口,只有运行完了或者手动终止,才能执行其他命令)
可以stdout、stderr
后台工作
不继承stdin(无法输入,如果需要读取输入,会halt暂停)
继承stdout、stderr(后台任务的输出会同步在命令行下显示)
SIGHUP信号
用户退出session时,系统向该session发送SIGHUP信号
session将SIGHUP信号发送给所有子进程
子进程收到SIGHUP信号后,自动退出
前台工作肯定会收到SIGHUP信号
后台工作是否会收到SIGHUP信号,决定于huponexit参数($shopt | grep hupon),这个参数决定session退出时SIGHUP信号是否会发送给后台工作
disown
disown可以将工作移出后台工作列表,从而即使huponexit参数打开,在session结束时,系统也不会向不在后台任务列表中的任务发送SIGHUP信号
标准I/O
标准I/O继承于session,如果session结束,被移出的后台任务有需要使用I/O,就会报错终止执行
因此需要将目标任务的标准I/O进行重定向。
nohup
no hang up - 不挂起
nohup的进程不再接受SIGHUP信号
nohup的进程关闭了stdin,即使在前台。
nohup的进程将stdout、stderr重定向到nohup.out
screen和tmux
作用:终端复用器,可以在同一个终端里面,管理多个session
不做深入。
多线程原理
程序运行过程中只有一个控制权存在,称为单线程
程序运行过程中有多个控制权存在,称为多线程
单CPU的计算机,可以通过不停在不同线程的指令间切换,造成类似多线程的效果
由于一个栈只有栈顶帧可被读写,相应的,只有栈顶帧对应的函数处于运行中(激活状态)
多进程的程序通过在一个进程内存空间中创建多个栈(每个栈之间以一定空白区域隔开,以备栈的增长),从而绕开栈的限制
多个栈共享进程内存中的text、heap、global data区域。
由于同一个进程空间存在多个栈,任何一个空白区域被填满,都会导致stack overflow的问题
多线程并发
多线程相当于一个并发系统,并发系统一般同时执行多个任务
如果多个任务对共享资源同时有操作,则会导致并发问题
并发问题解决是将原先的两个指令构成一个不可分隔的原子操作
多线程同步
同步是指一定的时间内只允许某一个线程访问某个资源。
可以通过互斥锁、条件变量和读写锁来实现同步资源
互斥锁(mutex):把某一段程序代码加锁,即表示某一时间段内只允许一个线程去访问这一段代码,其他的线程只能等待该线程释放互斥锁,才可以访问该代码段
条件变量:一般与互斥锁相结合,适用于多个线程等待某个条件的发生,而在条件发生时,这些等待的线程会同时被通知该条件已经发生,同时进行下一步工作。解决每个线程需要不断尝试获得互斥锁并检查条件是否发生时出现的资源浪费情况。
读写锁:
一个unlock的RW lock可以被某个线程获取R锁或者W锁;
如果被一个线程获得R锁,RW lock可以被其它线程继续获得R锁,而不必等待该线程释放R锁。但是,如果此时有其它线程想要获得W锁,它必须等到所有持有共享读取锁的线程释放掉各自的R锁。
如果一个锁被一个线程获得W锁,那么其它线程,无论是想要获取R锁还是W锁,都必须等待该线程释放W锁。
进程间通信
IPC( interprocess communication )
方式:文本、信号、管道、传统IPC
进程通信 - 文本
一个进程将信息写入到文本中,另一个进程去读取
由于是位于磁盘,所以效率非常低
进程通信 - 信号
可以以整数的形式传递非常少的信息
进程通信 - 管道
可以在两个进程之间建立通信渠道,分为匿名管道PIPE和命名管道FIFO
进程通信 - 传统IPC
主要指消息队列(message queue)、信号量(semaphore)、共享内存(shared memory)。
这些IPC的特点是允许多个进程之间共享资源。但是由于多进程任务具有并发性,所以也需要解决同步的问题。
传统IPC - 消息队列
与PIPE相似,也是建立一个队列,先放入队列的消息最先被取出。
不同点
消息队列允许多个进程放入/取出消息。
每个消息可以携带一个整数识别符(message_type),可以通过识别符对消息分类
进程从消息队列中取出消息时,按照先进先出的顺序取出,也可以只取出某种类型的消息(也是先进先出的顺序)。
消息队列不使用文件API(即调用文件+参数)
消息队列不会自动消失,他会一直存在于内核中,直到某个进程删除该队列
传统IPC - semaphore
与mutex类似,是一个进程/线程计数锁,允许被N个进程/线程取到,有更多的进程/线程申请时,会等待
一个semaphore会一直在内核中,直到某个进程/线程删除它
传统IPC - 共享内存
一个进程可以将自己内存空间中的一部分拿出来,允许其他进程读写。
在使用共享内存的时候,同样要注意同步的问题。
我们可以使用semaphore同步,也可以在共享内存中建立mutex或者其他的线程同步变量来同步
进程终结
当子进程终结时候,它会通知父进程,并清空自己所占据的内存,并在内核中留下自己的退出信息(exit code,0 - 正常退出,>0 - 异常退出),在这个信息里,会解释该进程为什么退出。
父进程得知子进程终结时,对该子进程使用wait系统调用,wait系统调用会取出子进程的退出信息,并清空该信息在内核中所占据的空间。
但是,如果父进程早于子进程终结时,子进程就成了一个孤儿(orphand)进程。孤儿进程会被过继给init进程。init进程会在该子进程终结时调用wait系统调用。
一个糟糕的程序也可能造成子进程的退出信息滞留在内核中的情况(父进程不对子进程调用wait函数,内核中滞留task_struct结构体),这样的情况下,子进程成为僵尸进程。当大量僵尸(Zombie)进程积累时,内存空间会被挤占。