1.main函数
C函数总是从执行一个名为main的函数开始。main函数的原型为
int main(int argc, char *argv[]);
其中 argc是命令行参数的数量而,argv是参数指针的数组。
2.进程终止
有8种方法终止一个进程。普通终止有5种:
1).从main函数中返回;
2).调用exit;
3).调用_exit或_Exit;
4).最后线程从启动例程(eg:start函数)返回;
5).从最后线程里调用pthread_exit
异常终止有3种:
6).调用abort
7).收到一个信号
8).最后线程回应一个取消请求
Exit 函数
三个普通终止程序的函数:_exit和_Exit从内核立即返回;eixt则先执行特定清理处理然后从内核返回
#includevoid exit(int status); void _Exit(int status); #include void _exit(int status);
atexit函数
根据ISO C,一个进程可以最多注册32个函数,这些函数由exit函数调用的函数。这些被称为exit处理器,并通过调用atexit函数来登记这些函数。
#includeint atexit(void (*func)(void)); //成功返回0,错误返回非0值。
声明说我们传递一个函数地址作为atexit的参数。当这个函数被调用时,不传入任何参数也不返回任何值。exit函数以它们注册的顺序的相反顺序调用这些函数。每个函数都被调用和它被注册的一样多的次数。
3.命令行参数
当一个程序被执行时,使用exec的进程可以传递命令行参数给这个新的程序。这是UNIX系统shell的普通操作的一部分
4.环境表
每个程序还被传入一个环境列表。就像参数列表那样,环境列表是一个字符指针的数组,每个指针包含一个以null终止的C字符串的地址。这个指针数组的地址包含在全局变量environ里:
extern char **environ;
例如,如果环境由5个字符串组成,用environ指向一个长度为5的指针数组。数组里的每个地址都指向一个如“HOME=/home/tommy\0”形式的字符串。每个字符串的结尾处显示的有null字符。我们称environ为环境指针,称指针数组为环境列表 ,称它们指向的字符串为环境字符串
5.C程序的存储空间布局
1).代码段(text segment 又称正文段),CPU执行的机器指令。通过,代码段是可共享的,以便经常执行的程序只需在内存里单个拷贝,比如文本编辑器,C编译器,外壳,等等。还有代码段通常是只读的,为了阻止一个程序偶然修改了它的指令。
2).初始化的数据段(Initialized data segment),通常简称为数据段,包括在程序里特别初始化的变量。例如,C出现在任何函数外的声明int maxcount = 99;会导致这个变量以其初始值存储在初始数据段里。
3).未初始化的数据段(Uninitialized data segment),经常被称为“bss”段,在代表“block started by symbol”的古老的汇编操作之后命令。在这个段的数据被内核在程序开始执行前初始化为数字0或null指针。出现在任何函数外的C声明long sum[1000];导致这个变量被存储在未初始化的数据段里。
4).栈,存储自动变量和每次一个函数调用时保存信息的地方。每次一个函数被调用时,它要返回到的地址和关于调用者环境的特定信息,比如一些机器寄存器,被保存在栈里。新调用的函数然后在栈上为自动和临时变量开辟空间。这是在C里的递归函数如何工作的。每次一个递归函数调用它自身时,一个新的栈框架被使用,所以一堆变量不会和这个函数的其它实例的变量冲突。
5).堆,动态内存分配通常发生的地方。历史上,堆一直放在未初始化数据和栈之间。
这些段的典型布局是:最低地址是代码段,其上是初始化数据,再上是未初始化数据,最高地址是命令行参数和环境变量,其下是栈,在栈和bss段之间是堆。
6.存储器分配
ISO C为内存分配规定了三个函数:
1).malloc:分配指定字节数量的内存。内存的初始值是不确定的。
2).calloc:为指定数量的指定尺寸的对象开辟空间。这个空间被初始化为0。
3).realloc:增加或减少之前开辟的区域。当长度增加时,它可能会导致把之前开辟的空间移到其它地方,来在尾部提供额外的空间。还有,当长度增加时,在旧对空和新区域尾部之间的空间的初始值是不确定的。
#includevoid *malloc(size_t size); void *calloc(size_t nobj, size_t size); void *realloc(void *ptr, size_t newsize); //三者成功都返回非空指针,错误返回NULL。 void free(void *ptr);
7.环境变量
如同前述,环境字符串的形式通常是这样的格式:
name=value
1).ISO C定义了一个函数getenv,可以用其环境变量值,但是该标准又称环境的内容是由实现定义的
#includechar *getenv(const char *name); //返回和name相关的值的指针,没有找到则返回NULL。
注意这个函数返回一个name=value的字符串的指针。我们应该使用getenv来从环境得到指定的值,而不是直接访问environ。
2).
includeint putenv(char *str); int setenv(const char *name, const char *value, int rewrite); int unsetenv(const char *name); //三者成功返回0,错误返回非0.
a.putenv函数取形式为name=value的字符串,并把它放在环境列表中。如果name已经存在,它的旧的定义会首先被移除。
b.setenv将name设置为value,如果name存在于环境中,那么1、如果rewrite为非0,则存在的name的定义首先被移除;2、如果rewrite为0,name的已存在的定义不被删除,name不会被设置为新的value,也没有错误发生。
c.unsetenv函数删除任何name的定义。
注意putenv和setenv的区别。setenv必须开辟内存来创建它参数的name=value的字符串,putenv可以把字符串直接传给环境。
8.setjmp和longjmp函数
setjmp 与 longjmp的结合使用,却可以实现在不同程序之间的跳转
#includeint setjmp(jmp_buf env); //如果直接调用返回0,如果从longjmp调用返回则返回非0。 void longjmp(jmp_buf env, int val);
我们从我们想回到的地点里调用setjmp,在下面例子里是main函数。这种情况下, setjmp返回0因为我们直接调用它。在这个setjmp的调用里,env参数是一个特殊的类型jmp_buf。这个数据类型是某种格式的数组,能够存储所有所需的信息,当我们调用longjmp时用来恢复栈的状态。通常,env变量是一个全局变量,因为我们将需要从另一个函数里引用它。
#includejmp_buf jmpbuffer; int main(void) { char line[MAXLINE]; if (setjmp(jmpbuffer) != 0) printf("error"); while (fgets(line, MAXLINE, stdin) != NULL) do_line(line); exit(0); } void cmd_add(void) { int token; token = get_token(); if (token < 0) longjmp(jmpbuffer, 1); /* rest of processing for this command */ } /* 当main被执行时,我们调用setjmp,它在变量jmpbuffer里记录任何它需要信息并返回0。我们然后调用do_line,它会用 cmd_add,并假定察觉到某种形式的一个错误。
在cmd_add里调用longjmp之前,栈里有main、do_line和cmd_add函数的框架。但是longjmp导致栈直接回到main函数,把cmd_add和do_line的栈框架给丢弃了。调用
longjmp导致main里的 setjmp返回,但这次它返回的值为1(longjmp的第二个参数。) */
9.getrlimit和setrlimit函数
每个进程都有一堆资源限制,其中一些可以用getrlimit和setrlimit函数查询和更改。
#includeint getrlimit(int resource, struct rlimit *rlptr); int setrlimit(int resource, const struct rlimit *rlptr); //两者成功都返回0,错误都返回非0。
对于两个函数的每次调用都指单个资源和一个指向以下结构体的指针:
struct rlimit { rlim_t rlim_cur; /* soft limit: current limit */ rlim_t rlim_max; /* hard limit: maximum value for rlim_cur */ };
1)进程的状态的概述:
1.1)Running(R),运行或将要运行
1.2)Interruptible(S),被阻断而等待一个事件,可能会被一个信号激活
1.3)Uninterruptible(D),被阻断而等待一个事件,不会被信号激活
1.4)Stopped(T),由于任务的控制或者外部的追踪而被终止,比如:strace
1.5)Zombie(Z),僵死,但是它的父进程尚未调用wait函数.
1.6)Deal(X),这个永远看不见
在内核源代码中的定义如下:
=====================================================
/usr/src/linux/fs/proc/array.c
static const char *task_state_array[] = {
"R (running)", /* 0 */
"S (sleeping)", /* 1 */
"D (disk sleep)", /* 2 */
"T (stopped)", /* 4 */
"T (tracing stop)", /* 8 */
"Z (zombie)", /* 16 */
"X (dead)" /* 32 */
};
=====================================================
在ps命令的帮助中定义如下:
PROCESS STATE CODES
Here are the different values that the s, stat and state output specifiers (header "STAT" or "S") will display to
describe the state of a process.
D Uninterruptible sleep (usually IO)
R Running or runnable (on run queue)
S Interruptible sleep (waiting for an event to complete)
T Stopped, either by a job control signal or because it is being traced.
W paging (not valid since the 2.6.xx kernel)
X dead (should never be seen)
Z Defunct ("zombie") process, terminated but not reaped by its parent.
For BSD formats and when the stat keyword is used, additional characters may be displayed:
< high-priority (not nice to other users)
N low-priority (nice to other users)
L has pages locked into memory (for real-time and custom IO)
s is a session leader
l is multi-threaded (using CLONE_THREAD, like NPTL pthreads do)
+ is in the foreground process group
======================================================
关于D和Z一段有趣的解释:
有一类垃圾却并非这么容易打扫,那就是我们常见的状态为 D (Uninterruptible sleep) ,以及状态为 Z (Zombie) 的垃圾进程。这些垃圾进程要么是求而不得,像怨妇一般等待资源(D),要么是僵而不死,像冤魂一样等待超度(Z),它们在 CPU run_queue 里滞留不去,把 Load Average 弄的老高老高,没看过我前一篇blog的国际友人还以为这儿民怨沸腾又出了什么大事呢。怎么办?开枪!kill -9!看你们走是不走。但这两种垃圾进程偏偏是刀枪不入的,不管换哪种枪法都杀不掉它们。无奈,只好reboot,像剿灭禽流感那样不分青红皂白地一律扑杀!
怨妇 D,往往是由于 I/O 资源得不到满足,而引发等待,在内核源码 fs/proc/array.c 里,其文字定义为“ "D (disk sleep)", /* 2 */ ”(由此可知 D 原是Disk的打头字母),对应着 include/linux/sched.h 里的“ #define TASK_UNINTERRUPTIBLE 2 ”。举个例子,当 NFS 服务端关闭之时,若未事先 umount 相关目录,在 NFS 客户端执行 df 就会挂住整个登录会话,按 Ctrl+C 、Ctrl+Z 都无济于事。断开连接再登录,执行 ps axf 则看到刚才的 df 进程状态位已变成了 D ,kill -9 无法杀灭。正确的处理方式,是马上恢复 NFS 服务端,再度提供服务,刚才挂起的 df 进程发现了其苦苦等待的资源,便完成任务,自动消亡。若 NFS 服务端无法恢复服务,在 reboot 之前也应将 /etc/mtab 里的相关 NFS mount 项删除,以免 reboot 过程例行调用 netfs stop 时再次发生等待资源,导致系统重启过程挂起。
冤魂 Z 之所以杀不死,是因为它已经死了,否则怎么叫 Zombie(僵尸)呢?冤魂不散,自然是生前有结未解之故。在UNIX/Linux中,每个进程都有一个父进程,进程号叫PID(Process ID),相应地,父进程号就叫PPID(Parent PID)。当进程死亡时,它会自动关闭已打开的文件,舍弃已占用的内存、交换空间等等系统资源,然后向其父进程返回一个退出状态值,报告死讯。如果程序有 bug,就会在这最后一步出问题。儿子说我死了,老子却没听见,没有及时收棺入殓,儿子便成了僵尸。在UNIX/Linux中消灭僵尸的手段比较残忍,执行 ps axjf 找出僵尸进程的父进程号(PPID,第一列),先杀其父,然后再由进程天子 init(其PID为1,PPID为0)来一起收拾父子僵尸,超度亡魂,往生极乐。注意,子进程变成僵尸只是碍眼而已,并不碍事,如果僵尸的父进程当前有要务在身,则千万不可贸然杀之。
2)分析不可被中断的睡眠进程:
2.1)重现:
终端1)
vi test.c
#include
void main() {
if (!vfork()) sleep(100);
}
gcc test.c -o test
./test
终端2)
ps aux|grep test
root 19884 0.0 0.0 3640 360 pts/0 D+ 16:38 0:00 ./test
root 19885 0.0 0.0 3640 360 pts/0 S+ 16:38 0:00 ./test
2.2)分析:
系统进入这种不可中断是很少发生的,即使发生也是一个短暂的状态,引起这种状态的发生一般是驱动程序.
例如:驱动程序可能正在特殊的设备上等待通过检测的响应,但又要保证自己不在可中断睡眠状态(S)被中断.所以驱动程序会把进程切换到不可中断的睡眠状态,直到硬件已返回到已知状态.
可以通过访问一个慢设备来观察不可中断的睡眠状态,比如CDROM这样的设备
例如:
dd if=/dev/cdrom f=/dev/null &
进程在一个不可中断的状态是十分危险的,你不能用kill -9杀掉它
例如:
一个有问题的驱动程序访问一个有问题的设备,设备不给驱动程序响应,驱动程序永远得不到响应,而又永远等待响应.
3)分析被跟踪或被停止的进程状态(T)
3.1)重现被跟踪时的状态:
终端1)
strace top
终端2)
ps auxf|grep top
root 980 9.4 0.0 1716 608 pts/0 S 00:31 0:12 | \_ strace top
root 981 3.7 0.1 10084 7076 pts/0 T 00:31 0:05 | \_ top
在用strace跟踪top执行的时候,top进程为T的状态
3.2)重现被停止的进程状态:
停止进程有三种手段:
3.2.1)发送SIGTSTP信停止进程.
-SIGTSTP的信号相当于CTRL+Z的组合键来终止正在前台运行的进程.
终端1)
vi /etc/passwd
终端2)
kill -SIGTSTP 12029
查看进程状态:
ps auxf
root 10297 0.0 1.0 5124 2696 pts/0 Ss+ Dec16 0:00 \_ -bash
root 12029 0.0 0.8 5348 2200 pts/0 T 05:15 0:00 | \_ vi test.c
终端1)
查看放到后台的作业
jobs
[1]+ Stopped vi test.c
用fg将作业切换到前台
fg
3.2.2)进程自已终止自己,标准输入引起进程停止
一个终端利用常规的后台和前台进程管理进程,一个终端有且只有一个前台进程,只有这个进程可以接受键盘的输入,其它任何开始于终端的进程都被认为是后台进程,但是当后台进程试图从标准输入读取数据时,终端将发送一个SIGTTIN终端它,因为这里只有一个输入设备键盘,只能用于前台进程.
这里的前后台进程概念仅限于终端的范围.
SIGTTIN 当后台作业要从用户终端读数据时, 该作业中的所有进程会收到SIGTTIN 信号. 缺省时这些进程会停止执行.
终端1)
尝试在后台运行read命令,因为后台进程不能从终端获取标准输入,所以进程将会收到信号SIGTTIN,使进程进入停止状态.
read x &
[1] 12057
[1]+ Stopped read x
终端2)
jobs
[1]+ Stopped read x
查看进程,12057这个PID就是read x&,现在看到是-bash,它的状态已经是T了
ps aux
root 12057 0.0 0.5 5124 1476 pts/0 T 05:26 0:00 -bash
用SIGCONT来唤醒
kill -SIGCONT 12057
终端1)
输入回车后,12057的进程依然会进入停止状态,也就是阻塞,只有会放到前台后,它才能完成输入.
fg
read x
hello
3.2.3)进程自已终止自己,标准输出引起进程停止
终端有一个tostop(终端输出终止)设置,在大多数系统里默认是关闭.
当是关闭的状态时,后台进程可以随时在终端写内容,如果是开启状态时,后台进程向标准输出写数据时就会被终止.
开启tostop
stty tostop
向标准输出写数据,被停止了
echo hello world &
[1] 12125
[1]+ Stopped echo hello world
jobs
[1]+ Stopped echo hello world
关闭tostop
stty -tostop
jobs
[1]+ Stopped echo hello world
向标准输出写数据恢复正常了
fg
echo hello world
hello world
4)分析进程的可中断睡眠态与运行态
编写一个小程序测试睡眠态与运行态之后的转换 :
=====================================================
#include
#include
#include
#include
void
run_status(void)
{
double pi = M_PI;
double pisqrt;
long i;
for (i=0; i<100000000; ++i){
pisqrt = sqrt(pi);
}
}
int
main (void)
{
run_status();
sleep(10);
run_status();
exit(EXIT_SUCCESS);
}
======================================================
编译链接后:
gcc -Wall -o pisqrt a.c -lm
终端1)
监控pisqrt进程
watch -n 1 "ps aux|grep pisqrt|grep -v ps|awk '{print $2}'|sort -k 8"
终端2)
strace ./pisqrt
显示如下:
read(3, "\177ELF\1\1\1\0\0\0\0\0\0\0\0\0\3\0\3\0\1\0\0\0\200X\1"..., 512) = 512
fstat64(3, {st_mode=S_IFREG|0755, st_size=1572440, ...}) = 0
old_mmap(NULL, 1279916, PROT_READ|PROT_EXEC, MAP_PRIVATE, 3, 0) = 0x49e000
old_mmap(0x5d1000, 12288, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED, 3, 0x132000) = 0x5d1000
old_mmap(0x5d4000, 10156, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x5d4000
close(3) = 0
set_thread_area({entry_number:-1 -> 6, base_addr:0xb75e3a60, limit:1048575, seg_32bit:1, contents:0, read_exec_only:0, limit_in_pages:1, seg_not_present:0, useable:1}) = 0
munmap(0xb75e4000, 75579) = 0
此时切换到终端1看pisqrt进程状态,此时为R状态:
root 3792 99.9 0.0 1516 268 pts/2 R 02:40 0:01 ./pisqrt
root 3801 0.0 0.0 3700 672 pts/1 S 02:40 0:00 grep pisqrt
root 3791 1.0 0.0 1728 564 pts/2 S 02:40 0:00 strace ./pisqr
之后pisqrt进程进入S状态,因为执行了sleep(10)函数,10秒之后pisqrt再次进入R状态.最终退出.
分析:
pisqrt占用CPU时间片时状态为R,而在调用sleep函数后为S,系统大多数进程状态都为S,比如APACHE和ORACLE,
而处于S状态不一定是调用了sleep函数,因为IO也会让进程处于睡眠态.
而我们可以启动多个pisqrt程序,这时在系统中会有多个R状态的进程.也就是说CPU个各数与R进程是没有直接关联的.
5)分析进程的僵死态(Z)
当一个进程退出时,它并不是完全消失,而是等到它的父进程发出wait系统调用才会完全消失,除非父进程发出wait系统调用终止进程,
否则该进程将一直处于所谓的僵死状态,等待它的父进程终止它.如果父进程终止了运行而没有撤销子进程,那么这些进程将被进程init收养.
init进程定期调用wait来收养这些未被撤消的进程.
先制造一段僵尸程序,如下:
=============================
#include
#include
#include
#include
#include
int
main (){
if(!fork()){
printf("child pid=%d\n", getpid());
exit(5);
}
sleep(20);
printf("parent pid=%d\n", getpid());
exit(EXIT_SUCCESS);
}
===========================================
编译
gcc -Wall defunct.c -o defunct
终端1:
watch -n 1 "ps auxf|grep defunct|grep -v ps|grep -v grep|awk '{print $2}'|sort -k 8"
终端2:
执行./defunct
查看终端1:
root 7280 0.0 0.0 1380 240 pts/2 S 03:05 0:00 | \_ ./defunct
root 7281 0.0 0.0 0 0 pts/2 Z 03:05 0:00 | \_ [defunct
20秒后查看终端2:
child pid=7281
parent pid=7280
关于信号集的描述:/usr/include/bits/signum.h
#define SIGCLD SIGCHLD /* Same as SIGCHLD (System V). */
#define SIGCHLD 17 /* Child status has changed (POSIX). */
在上面程序的基础上加入wait函数即可将SIGCHLD信号回收
修改后的程序如下:
=================================
#include
#include
#include
#include
#include
int
main (){
int status,i;
if(!fork()){
printf("child pid=%d\n", getpid());
exit(5);
}
wait(&status);
i = WEXITSTATUS(status);
sleep(20);
printf("parent pid=%d,child process exit/status=%d\n", getpid(),i);
exit(EXIT_SUCCESS);
}
==========================================
6)最后的进程X (dead)
指死掉的进程
最后4种附加的状态....
W状态:不驻留内存
<状态:nice小于0
N状态:nice大于0
L状态:有锁住的页面
这部分在ps的源代码(output.c)有描述:
===================================
static int
pr_stat(void)
{
int end = 0;
outbuf[end++] = pp->state;
if (pp->rss == 0 && pp->state != 'Z')
outbuf[end++] = 'W';
if (pp->nice < 0)
outbuf[end++] = '<';
if (pp->nice > 0)
outbuf[end++] = 'N';
if (pp->vm_lock)
outbuf[end++] = 'L';
outbuf[end] = '\0';
return end;
}
=====================================
Linux进程通过一个task_struct结构体描述,在linux/sched.h中定义,通过理解该结构,可更清楚的理解linux进程模型。 包含进程所有信息的task_struct数据结构是比较庞大的,但是该数据结构本身并不复杂,我们将它的所有域按其功能可做如下划分:
1. 进程状态(State)
进程执行时,它会根据具体情况改变状态。进程状态是调度和对换的依据。Linux中的进程主要有如下状态,如表1所示。
表3.1 Linux进程的状态
内核表示 |
含义 |
TASK_RUNNING |
可运行 |
TASK_INTERRUPTIBLE |
可中断的等待状态 |
TASK_UNINTERRUPTIBLE |
不可中断的等待状态 |
TASK_ZOMBIE |
僵死 |
TASK_STOPPED |
暂停 |
TASK_SWAPPING |
换入/换出 |
1).可运行状态:处于这种状态的进程,要么正在运行、要么正准备运行。正在运行的进程就是当前进程(由current所指向的进程),而准备运行的进程只要得到CPU就可以立即投入运行,CPU是这些进程唯一等待的系统资源。
2).等待状态:处于该状态的进程正在等待某个事件(event)或某个资源,它肯定位于系统中的某个等待队列(wait_queue)中。
3).暂停状态:此时的进程暂时停止运行来接受某种特殊处理。通常当进程接收到SIGSTOP、SIGTSTP、SIGTTIN或 SIGTTOU信号后就处于这种状态。例如,正接受调试的进程就处于这种状态。
4).僵死状态:进程虽然已经终止,但由于某种原因,父进程还没有执行wait()系统调用,终止进程的信息也还没有回收。顾名思义,处于该状态的进程就是死进程,这种进程实际上是系统中的垃圾,必须进行相应处理以释放其占用的资源。
进程状态转换如下图:
2.进程调度信息
调度程序利用这部分信息决定系统中哪个进程最应该运行,并结合进程的状态信息保证系统运转的公平和高效。这一部分信息通常包括进程的类别(普通进程还是实时进程)、进程的优先级等。表2描述了跟进程调度有关的字段,表3.3说明了几种常用的进程调度算法及这些算法的使用范围,如先来先服务主要用于实时进程的调度。
表2 进程调度信息
域名 |
含义 |
need_resched |
调度标志 |
Nice |
静态优先级 |
Counter |
动态优先级 |
Policy |
调度策略 |
rt_priority |
实时优先级 |
表3 进程调度的策略
名称 |
解释 |
适用范围 |
SCHED_OTHER |
其他调度 |
普通进程 |
SCHED_FIFO |
先来先服务调度 |
实时进程 |
SCHED_RR |
时间片轮转调度 |
只有root用户能通过sched_setscheduler()系统调用来改变调度策略。
3.标识符(Identifiers)
每个进程有进程标识符、用户标识符、组标识符,如表4所示。
不管对内核还是普通用户来说,怎么用一种简单的方式识别不同的进程呢?这就引入了进程标识符(PID:process identifier),每个进程都有一个唯一的标识符,内核通过这个标识符来识别不同的进程,同时,进程标识符PID也是内核提供给用户程序的接口,用户程序通过PID对进程发号施令。PID是32位的无符号整数,它被顺序编号:新创建进程的PID通常是前一个进程的PID加1。然而,为了与16位硬件平台的传统Linux系统保持兼容,在Linux上允许的最大PID号是32767,当内核在系统中创建第32768个进程时,就必须重新开始使用已闲置的PID号。
表4 各种标识符
域名 |
含义 |
Pid |
进程标识符 |
Uid、gid |
用户标识符、组标识符 |
Euid、egid |
有效用户标识符、有效组标识符 |
Suid、sgid |
备份用户标识符、备份组标识符 |
Fsuid、fsgid |
文件系统用户标识符、文件系统组标识符 |
4.进程通信有关信息(IPC:Inter_Process Communication)
为了使进程能在同一项任务上协调工作,进程之间必须能进行通信即交流数据。
Linux支持多种不同形式的通信机制。它支持典型的Unix 通信机制(IPC Mechanisms):信号(Signals)、管道(Pipes),也支持System V 通信机制:共享内存(Shared Memory)、信号量和消息队列(Message Queues),如表5
表5 进程通信有关信息
域名 |
含义 |
Spinlock_t sigmask_lock |
信号掩码的自旋锁 |
Long blocked |
信号掩码 |
Struct signal *sig |
信号处理函数 |
Struct sem_undo *semundo |
为避免死锁而在信号量上设置的取消操作 |
Struct sem_queue *semsleeping |
与信号量操作相关的等待队列 |
5.进程链接信息(Links)
程序创建的进程具有父/子关系。因为一个进程能创建几个子进程,而子进程之间有兄弟关系,在task_struct结构中有几个域来表示这种关系。
在Linux系统中,除了初始化进程init,其他进程都有一个父进程(parent process)或称为双亲进程。可以通过fork()或clone()系统调用来创建子进程,除了进程标识符(PID)等必要的信息外,子进程的task_struct结构中的绝大部分的信息都是从父进程中拷贝,或说“克隆”过来的。系统有必要记录这种“亲属”关系,使进程之间的协作更加方便,例如父进程给子进程发送杀死(kill)信号、父子进程通信等,就可以用这种关系很方便地实现。
每个进程的task_struct结构有许多指针,通过这些指针,系统中所有进程的task_struct结构就构成了一棵进程树,这棵进程树的根就是初始化进程init的task_struct结构(init进程是Linux内核建立起来后人为创建的一个进程,是所有进程的祖先进程)。表6是进程所有的链接信息。
表6 进程链接信息
名称 |
解释 [指向哪个进程] |
p_opptr |
祖先 |
p_pptr |
父进程 |
p_cptr |
子进程 |
p_ysptr |
弟进程 |
p_osptr |
兄进程 |
Pidhash_next、 Pidhash_pprev |
进程在哈希表中的链接 |
Next_task、 prev_task |
进程在双向循环链表中的链接 |
Run_list |
运行队列的链表 |
6.时间和定时器信息(Times and Timers)
一个进程从创建到终止叫做该进程的生存期(lifetime)。进程在其生存期内使用CPU的时间,内核都要进行记录,以便进行统计、计费等有关操作。进程耗费CPU的时间由两部分组成:一是在用户模式(或称为用户态)下耗费的时间、一是在系统模式(或称为系统态)下耗费的时间。每个时钟滴答,也就是每个时钟中断,内核都要更新当前进程耗费CPU的时间信息。
表7是和时间有关的域,上面所说的counter是指进程剩余的CPU时间片,也和时间有关,所以这里我们再次提及它。表8是进程的所有定时器。
表7与时间有关的域
域名 |
含义 |
Start_time |
进程创建时间 |
Per_cpu_utime |
进程在某个CPU上运行时在用户态下耗费的时间 |
Per_cpu_stime |
进程在某个CPU上运行时在系统态下耗费的时间 |
Counter |
进程剩余的时间片 |
表8 进程的所有定时器
定时器类型 |
解释 |
什么时候更新 |
用来表示此种定时器的域 |
ITIMER_REAL |
实时定时器 |
实时更新,即不论该进程是否运行 |
it_real_value |
it_real_incr |
|||
real_timer |
|||
ITIMER_VIRTUAL |
虚拟定时器 |
只在进程运行于用户态时更新 |
it_virt_value |
it_virt_incr |
|||
ITIMER_PROF |
概况定时器 |
进程运行于用户态和系统态时更新 |
it_prof_value |
it_prof_incr |
7.文件系统信息(File System)
进程可以打开或关闭文件,文件属于系统资源,Linux内核要对进程使用文件的情况进行记录。task_struct结构中有两个数据结构用于描述进程与文件相关的信息。其中,fs_struct中描述了两个VFS索引节点(VFS inode),这两个索引节点叫做root和pwd,分别指向进程的可执行映象所对应的根目录(home directory)和当前目录或工作目录。file_struct结构用来记录了进程打开的文件的描述符(descriptor)。如表9所示。
表9 与文件系统相关的域
定义形式 |
解释 |
Sruct fs_struct *fs |
进程的可执行映象所在的文件系统 |
Struct files_struct *files |
进程打开的文件 |
在文件系统中,每个VFS索引节点唯一描述一个文件或目录,同时该节点也是向更低层的文件系统提供的统一的接口。
8.虚拟内存信息(Virtual Memory)
除了内核线程(kernel thread),每个进程都拥有自己的地址空间(也叫虚拟空间),用mm_struct来描述。另外Linux2.4还引入了另外一个域active_mm,这是为内核线程而引入。因为内核线程没有自己的地址空间,为了让内核线程与普通进程具有统一的上下文切换方式,当内核线程进行上下文切换时,让切换进来的线程的active_mm指向刚被调度出去的进程的active_mm(如果进程的mm域不为空,则其active_mm域与mm域相同)。内存信息如表10所示
表10 虚拟内存描述信息
定义形式 |
解释 |
Struct mm_struct *mm |
描述进程的地址空间 |
Struct mm_struct *active_mm |
内核线程所借用的地址空间 |
9.页面管理信息
当物理内存不足时,Linux内存管理子系统需要把内存中的部分页面交换到外存,其交换是以页为单位的。有关页面的描述信息如表11。
表11 页面管理信息
定义形式 |
解释 |
Int swappable |
进程占用的内存页面是否可换出 |
Unsigned long min_flat, maj_flt,nswap |
进程累计的次(minor)缺页次数、 主(major)次数及累计换出、换入页面数 |
Unsigned long cmin_flat, cmaj_flt,cnswap |
本进程作为祖先进程,其所有层次子进程的累计的次(minor)缺页次数、主(major)次数及累计换出、换入页面数 |
10.进程内核栈及current宏
在Linux-2.6内核中堆栈这么定义:
union thread_union { struct thread_info thread_info; unsigned long stack[THREAD_SIZE/sizeof(long)]; };
根据内核的配置,THREAD_SIZE既可以是4K字节(1个页面)也可以是8K字节(2个页面)。thread_info是52个字节长。下图是当设为8KB时候的内核堆栈:Thread_info在这个内存区的开始处,内核堆栈从末端向下增长。进程描述符不是在这个内存区中,而分别通过task与thread_info指针使thread_info与进程描述符互联。所以获得当前进程描述符的current定义如下:
#define current get_current() static inline struct task_struct * get_current(void) { return current_thread_info()->task; } static inline struct thread_info *current_thread_info(void) { struct thread_info *ti; __asm__("andl %%esp,%0; ":"=r" (ti) : "" (~(THREAD_SIZE - 1))); return ti; }
根据THREAD_SIZE大小,分别屏蔽掉内核栈的12-bit LSB(4K)或13-bit LSB(8K),从而获得内核栈的起始位置,及当前进程描述符的指针。
fg、bg、jobs、&、ctrl+z命令
一、 &
加在一个命令的最后,可以把这个命令放到后台执行 ,如sh start.sh &
二、ctrl + z
可以将一个正在前台执行的命令放到后台,并且处于暂停状态,不可执行。
三、jobs:查看当前有多少在后台运行
jobs -l选项可显示所有任务的PID,jobs的状态可以是running, stopped, Terminated,但是如果任务被终止了(kill)
四、fg:将后台中的命令调至前台运行
如果后台中有多个命令,可以用 fg %jobnumber将选中的命令调出,%jobnumber是通过jobs命令查到的后台正在执行的命令的序号,而不是pid
五、bg:将一个在后台暂停的命令,变成继续执行 (后台执行)
如果后台中有多个命令,可以用bg %jobnumber将选中的命令调出,%jobnumber是通过jobs命令查到的后台正在执行的命令的序号(不是pid)
将任务转移到后台运行:先ctrl + z;再bg,这样进程就被移到后台运行,终端还能继续接受命令。
当前任务
如果后台的任务号有2个,[1],[2];如果当第一个后台任务顺利执行完毕,第二个后台任务还在执行中时,当前任务便会自动变成后台任务号码“[2]” 的后台任务。所以可以得出一点,即当前任务是会变动的。当用户输入“fg”、“bg”和“stop”等命令时,如果不加任何引号,则所变动的均是当前任务
进程的终止
后台进程的终止:
方法一:
通过jobs命令查看job号(假设为num),然后执行kill %num
方法二:
通过ps命令查看job的进程号(PID,假设为pid),然后执行kill pid
前台进程的终止:
ctrl+c
kill的其他作用
kill除了可以终止进程,还能给进程发送其它信号,使用kill -l 可以察看kill支持的信号。
SIGTERM是不带参数时kill发送的信号,意思是要进程终止运行,但执行与否还得看进程是否支持。如果进程还没有终止,可以使用kill -SIGKILL pid,这是由内核来终止进程,进程不能监听这个信号。
后台进程的挂起:
在redhat中,不存在stop命令,可通过执行命令kill -stop PID,将进程挂起;
当要重新执行当前被挂起的任务时,通过bg %num 即可将挂起的job的状态由stopped改为running,仍在后台执行;当需要改为在前台执行时,执行命令fg %num即可。
前台进程的挂起:
ctrl+Z