您的位置:首页 > 其它

Unix原理与应用(第四版)学习笔记3--系统调用之进程篇

2013-08-07 22:47 561 查看
进程篇
进程这个抽象的概念可以使我们了解计算机内部的工作过程。进程是操作系统的核心抽象,操作系统的中其他的抽象(比如文件)都紧紧的围绕在进程这个核心抽象概念周围。下面将介绍进程的控制,内核的进程控制机制和相关的数据结构,详细分析了fork-exec-wait周期的整个过程,利用内核的描述符复制文件,实现shell中的重定向(redirect)和管道(pipe),信号控制机制。

1进程的几个抽象概念

Unix支持抢占式多任务系统(preemptive multitasking)。当一个进程的时间片用完时,内核就强制停止这个进程的运行,并让另一个进程占用CPU,当时这个模式并不是一成不变的,特别是在执行系统调用时。系统调用的控制的机制不尽相同,有些系统调用(系统调用集限定了操作系统所用进行的操作的集合)使用一些特别的控制方式。
内核不仅为进程本身内存,也为用于进程切换的控制信息(主要是一些堆栈和进程表)分配内存。两个重要的概念:进程地址空间,进程表。

1.1虚拟地址空间

通常,进程是源于磁盘上的可执行的程序。当我们运行一个C程序时,装载程序就把磁盘程序文件的二进制代码装载到内存中。每当进程序需要内存空间时(如调用函数),由内核为它分配额外的内存空间,进程可以存取的内存分配空间集合称为虚拟地址空间,这个虚拟地址空间又分很多段:



图 进程的虚拟地址空间(注意:指令部分处于栈顶)

指令段(TextSegment)这个段保存了将要执行的代码。一个程序的多个实例共享这个段。假如有三个用户同时使用vi,他们使用同一个指令段。

数据段(DataSegment)保存了程序使用的一些常量,全局变量和静态变量

栈(Stack)保存了函数的参数和局部变量,以及函数的返回地址。栈的大小随着函数调用的开始和结束而变化。

堆(heap)程序运行期间动态分配内存要到堆(如使用malloc)。堆和栈之间隔着一段没有分配的空间,他们相互朝着对方靠拢

命令行参数(Command line arguement)和环境变量,他们保存在栈底位置

链接程序(Linux中ld命令)给进程的每个段(指令段,数据段,堆和栈等)分配的地址是虚拟地址,因为他们并不是内存实际的物理地址。大多数程序的虚拟地址是从0开始的,但是他们运行时并不会产生冲突,这是因为,在运行时,
MMU(Memory Management Unit)利用一组地址转换表把它们的虚拟地址转换为内存的物理地址。MMU包含一组硬件寄存器,他们指向正在运行的进程的装换表。注意,只有支持MMU的处理器才可以使用虚拟地址空间
每个进程都有一个上下文(context),它代表进程运行时可以使用的整个环境。这个环境包括硬件寄存器的状态,地址空间和保存在进程表里的信息。在内核上下文切换强制某个进程把CPU资源让给另一个进程之前,需要保存进程上下文。然后内核把寄存器设置为新进程装换表的地址,由于每个进程都有自己的一组转换表,因此一个进程不能访问另一个进程的地址空间。

1.2进程表

每个活动进程的属性都保存在一个非常大的结构里,而这个结构是进程表的一个记录。现在的Unix系统不再把进程表的部分内容换出到磁盘上,而是把整个进程表都保存在内存里。进程表具有以下重要的属性:

进程的PID和PPID

进程的状态—运行态,休眠态,死亡态等

进程的真实UID和真实GID

进程的有效UID和有效GID

文件描述表

创建文件时需要的权限屏蔽字

CPU使用的统计表

正在等待处理的信号,是一个正在等待处理的信号列表,进程根据这个数据项的值判断是否有信号等待处理

信号处理表他说明当进程收到一个信号时需要采取的动作

一个新创建的子进程,它在进程记录中的许多内容来源子它的父进程。当子进程执行一个程序时,进程表中的记录不变(PID)而进改变其中部分数据项。例如:需要关闭某些文件描述表。当一个进程死亡时,只有其父进程得到子进程的退出状态值后,内核才会从进程表里删除子进程记录。SUID位置可以允许非特权用户读取一个供超级用户读取的文件。

进程表的结构体表(Linux内核2.X)







state
进程状态(见上述6种状态)
flags
进程标记(共有10多种标志)
priority
进程静态优先数
rt_priority
进程实时优先数
counter
进程动态优先数(时间片)
policy
调度策略(0基于优先权的时间片轮转、1基于先进先出的实时调度,2基于优先权轮转的实时调度)




signal
记录进程接收到的信号,共32位,每位对应一种信号
blocked
进程屏蔽信号的屏蔽位,置位为屏蔽,复位为不屏蔽
*sig
信号对应的自定义或缺省处理函数






*next_task
*prev_task
进程PCB双向链接指针、即前向和后向链接指针
*next_run
*prev_run
运行或就绪队列进程PCB 双向链接指针
*p_opptr,*p_pptr
*p_cptr
*p_ysptr,*p_osptr
指向原始父进程、父进程,子进程、新老兄弟进程的队列指针




uid,gid
运行进程的用户标识和用户组标识
Groups[NGROUPS]
允许进程同时拥有的一组用户组号
euid,egid
有效的uid 和gid,用于系统安全考虑
fsuid,fsgid
文件系统的uid 和gid,Linux特有,用于合法性检查
suid,sgid
系统 调 用 改 变 uid/gid时,用于 存 放 真 正 的
uid/gid
pid,pgrp,session
进程标识号、组标识号、session标识号
leader
是否是session的主管,布尔量






timeout
进程间隔多久被重新唤醒,用于软实时,tick为单位
it_real_value
it_real_incr
用于间隔计时器软件定时,时间到发SIGALRM

real_timer
一种定时器结构(新定时器)
it_virt_value
it_virt_incr
进程用户态执行间隔计时器软件定时,时间到发SIGVTALRM
it_prof_value
it_prof_incr
进程执行间隔计时器软件定时(包括用户和核心
态),时间到发SIGPROF
utime,stime,cutime
cstime,start_time
进程在用户态、内核态的运行时间,所有层次子进程在用户态、内核态运行时间总和,创建进程的时间
信号量
*semundo
进程每次操作信号量对应的undo
操作
*semsleeping
信号量集合对应的等待队列的指针



*ldt
进程关于段式存储管理的局部描述符指针
tss
保存任务状态信息,如通用寄存器等
saved_kernel_stack
为MSDOS仿真程序保存的堆栈指针
saved_kernel_page
内核态运行时,进程的内核堆栈基地址




*fs
保存进程与VFS 的关系信息
*files
系统打开文件表,包含进程打开的所有文件
link_count
文件链的数目






*mm
指向存储管理的mm_struct
结构
swappable
指示进程占用页面是否可以换出,1为可换出
swap_address
进程下次可换出的页面地址从此开始
min_flt,maj_flt
该进程累计的minor和major
缺页次数
nswap
该进程累计换出的页面数
cmin_flt,cmaj_flt
该进程及其所有子进程累计的缺页次数
cnswap
该进程及其所有子进程累计换入和换出的页面计数
swap_cnt
下一次循环最多可以换出的页数
SMP
支持
processor
SMP系统中,进程正在使用的CPU
last_processor
进程最后一次使用的CPU
lock_depth
上下文切换时系统内核锁的深度
其他
used_math
是否使用浮点运算器FPU
Comm[16]
进程正在运行的可执行文件的文件名
rlim
系统使用资源的限制,资源当前最大数和资源可有的最大数
errno
最后一次系统调用的错误号,0表示无错误
Debugreg[8]
保存调试寄存器值
*exec_domain
personality
与运行iBCS2 标准程序有关
*binfmt
指向 全 局 执 行 文 件 格 式 结 构 ,包括a.out,script,elf, java等4

exit_code,exit_signal
引起进程退出的返回代码,引起出错的信号名
dumpable
出错时是否能够进行memorydump,布尔量
did_exec
用于区分新老程序代码,POSIX要求的布尔量
tty_old_pgrp
进程显示终端所在的组标识
*tty
指向进程所在的显示终端的信息
*wait_chldexit
在进程结束需要等待子进程时处于的等待队列
(备注:表格是一种很好的表示知识的方式,联想关系型数据库和表格驱动编程)

1.2.1 process.c 程序-- 参看进程的权限

/*Program: process.c -- list process and user credentials: PID ,PPID,UIDS GIDS*/
#include<stdio.h>
int main(void){
printf("PID: %4d, PPID: %4d\n",getpid(),getppid());
printf("UID: %4d,  GID: %4d\n",getuid(),getgid());
printf("EUID:%4d, EGID: %4d\n",geteuid(),getegid());
return 0;
}
编译命令: cc/gcc -o process process.c

执行结果:./process
PID: 17464, PPID: 16331 //注意,PID,PPID不是固定的,系统每时每刻都在创建进程和销毁进程

UID: 1000, GID: 1000

EUID:1000, EGID: 1000

1.3创建一个进程

进程的整个生命周期围绕着四个系统调用:fork,exec,wait,_exit,(注意,理解周期这个非常的重要)

1.3.1 fork系统调用

pid_tfork(void); //Copies current process to child


fork调用返回后,父子进程除了PID和PPID都不相同以外,其他的内容几乎是完全相同的。为了区分原进程和和复制进程,fork返回两次,两次返回值是不一样的。---这里展示了复制文件描述符的好方法。
使用fork调用时,内核复制当前进程的地址空间(指令段,数据段,栈等)。并在进程表里专门建立一个记录以表示新创建的进程。新进程的许多内容是从父进程的记录中复制过来的,包括文件描述符,当前目录和权限屏蔽字,由于子进程在它自己的地址空间里运行,因此这些改变不会影响到父进程。父进程中读写操作的偏移指针对于子进程是可见的。

1.3.2 fork调用实例-fork.c

/*Program: fork.c -- A simple fork shows PID,PPID in both parent and child */
#include<stdio.h>
#include<sys/types.h>
#include<stdlib.h>
int main(void){
pid_t pid;
printf("Before fork\n");

pid = fork();	//Replicates current process
if(pid > 0){
sleep(1);//使用sleep系统调用确保父进程不会在子进程之前死亡,从而避免很多问题
printf("PARENT -- PID: %d PPID: %d, CHILD PID: %d\n",
getpid(),getppid(),pid);
}else if(pid == 0)
printf("CHILD -- PID: %d PPID: %d\n",getpid(),getppid());
else {
printf("Fork error\n");
exit(1);
}
printf("Both processs continue from here\n");
return 0;
}
编译命令:cc -o fork fork.c

执行结果: ./fork
Before fork

CHILD -- PID: 17519 PPID: 17518

Both processs continue from here

PARENT -- PID: 17518 PPID: 16331, CHILD PID: 17519

Both processs continue from here

提示:标准输入输出库中包含了一些诸如printf之类的函数使用另外一组缓存区。这些缓冲区有别于内核使用的高速缓冲区,用fork调用创建的进程也复制了这些缓冲区里的内容,这意味着同一个缓冲区对于子进程是可用的,这会使得fork调用时使用printf等函数带来一些问题。
write调用是立即写的模式,printf函数从终端输出时使用的缓冲区模式,写入文件中时采用的满缓冲模式—只有当缓冲区沾满时,才向磁盘写入数据。

1.3.3 getenv和setenv---使用环境变量

由于环境变量是进程地址空间的一部分,因此在创建进程时,其父进程的环境变量会传递给子进程,这环境变量保存在environ[]变量中,在C程序中,需要按下列的方式说明外部变量。
externchar **environ;
environ是一个指向char类型的指针数组,当是这个数组以name/value(变量名=值)形式保存环境变量字符串的指针。POSIX规范:

char* getenv(const char * name);
int setenv(const char * envname,const char* envval,int overwrite);


getenv函数返回一个指针值,它指向name参数所代表的环境字符串。
char* path = getenv(“PATH”);
getenv函数需要三个参数,前两个参数表示环境变量和它的值,第三个参数overwrite决定是否覆盖环境变量中的值。如果这个环境变量已经定义,且overwirte参数为非零,则覆盖,否则变量值不变,

1.3.4 childenv.c-- 传递环境变量

/*Program:childenv.c -- Changes child's environment and than checks the effect in parent */
#include<stdio.h>
#include<sys/types.h>
#include<stdlib.h>
#define PATH_LENGTH 100
int main(void){
pid_t pid;
int x = 100;
char newdir[PATH_LENGTH +1];
getcwd(newdir,PATH_LENGTH);
printf("BERORE FORK --Current directory: %s\n",newdir);
pid = fork();
switch(pid){
case -1:
perror("fork");
exit(1);
case 0:
printf("CHILD -- Inherited value of x: %d\n",x);
x = 200;
printf("CHILD -- Changed value of x: %d\n",x);
printf("CHILD -- Inherited value of PATH: %s\n",getenv("PATH"));
setenv("PATH",".",1);
printf("CHILD -- New value of PATH: %s\n",getenv("PATH"));
if(chdir("/etc") != -1){
getcwd(newdir,PATH_LENGTH);
printf("CHILD -- Current directory changed to: %s\n",newdir);
}
break;
exit(0);
default:
sleep(2);
getcwd(newdir,PATH_LENGTH);
printf("PARENT -- Value of x affter change by child: %d\n",x);
printf("PARENT -- Current directory is still: %s\n",newdir);
printf("PARENT -- Value of PATH is unchanged: %s\n",getenv("PATH"));
exit(0);
}
}
编译命令: cc -o childenv childenv.c

执行结果: ./childenv

BERORE FORK --Current directory: /home/xiajian/project file/C++project/TestOSAPI/linux/process_thread
CHILD -- Inherited value of x: 100
CHILD -- Changed value of x: 200
CHILD -- Inherited value of PATH: /usr/lib/lightdm/lightdm:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games
CHILD -- New value of PATH: .
CHILD -- Current directory changed to: /etc
PARENT -- Value of x affter change by child: 100
PARENT -- Current directory is still: /home/xiajian/project file/C++project/TestOSAPI/linux/process_thread
PARENT -- Value of PATH is unchanged: /usr/lib/lightdm/lightdm:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games


子进程中环境改变没有反馈到父进程里,这是因为子进程自己保留了这些参数的副本。
为了让子进程在父进程之前死亡,使用sleep调用让父进程休眠了一段时间。这并不是一个令人满意的解决方法,理想情况下,父进程应该等子进程死亡后才能死亡,这个可以用wait调用来实现。

1.3.5 _exit和exit—终止进程

对于一些精灵进程,只要系统在工作,它就一直在运行。当是但多数进程都要死亡的。当一个进程死亡时,内核关闭它打开的所有文件,并释放与该进程有关的内存(如地址空间)。是否要同时删除进程表里的记录取决于父进程是否要等待子进程死亡.进程终止的几种方式:

当遇到程序的结束。如果程序的main函数中没有exit或return语句,就是属于这种情形。进程隐含调用一个return语句。

在main函数里遇到return语句

在程序的任何位置使用exit函数或_exit系统调用

接受收到进程终止信号

void _exit(int status);
void exit(int status);


exit系统调用的作用和exit命令相似,_exit调用关闭所有打开的文件,并终止进程,但是它并不执行其他的清除操作。通常我们不使用_exit调用,因为exit函数的内部也调用_exit,而且函数适合用来终止程序。
无论使用哪种方法终止进程,内核都要向父进程发送一个信号(SIGCHLD),把子进程的死亡消息通知给父进程,exit函数的参数值是保存到进程表中的,这个值就是退出状态值,父进程利用wait或者waitpid系统调用可以获得这个值.

1.4 等待状态---读取子进程的退出状态

fork– exec(使用新程序的地址空间取代子进程的地址空间)
当子进程在运行时,父进程可以做什么呢?可以做这两件事:

等待获取子进程的退出状态值

不等待子进程,继续执行(如果需要,以后在获取子进程的状态值)

在shell提示符输入一个命令时,可以以正常的方式来执行,也可以在后台执行(在命令之后加上一个&)。

1.4.1 wait–等待子进程死亡

当一个子进程终止时,它的退出状态和其他信息保存在进程表里,它的父进程利用wait读取这个退出状态值。wait调用只有一个参数,它是一个指向整数的指针变量:

pid_t wait(int *stat_loc);


当至少有一个子进程还在运行时,wait使父进程处于阻塞的状态,等待子进程死亡。返回第一个死亡的子进程的PID,并把它的退出状态保存在stat_loc变量里,然后内核释放进程表中分配给此子进程的内存空间,父进程继续执行wait其后的语句。

1.4.2 wait.c -- 说明wait和WEXISTATUS的用法

/*Program: wait.c -- Uses wait to obtain child's termination status.
The WEXITSTATUS macro fetches the exit status. */
#include<stdio.h>
#include<fcntl.h>
#include<sys/wait.h>
#include<stdlib.h>
int main(int argc,char **argv){
int fd,exitstatus;
int exitval = 10;

fd = open(argv[1],O_WRONLY | O_CREAT | O_TRUNC,0644);
write(fd,"Original process writes\n",24);

switch(fork()){
case 0:
write(fd,"Child written\n",14);
close(fd);
printf("CHILD: Terminating with exit value %d\n",exitval);
exit(exitval);
default:
wait(&exitstatus);
printf("PARENT: Child terminated with exit value %d\n",
WEXITSTATUS(exitstatus));
write(fd,"Parent writes\n",14);
exit(20);
}
}


编译命令:cc -o wait wait.c
执行命令: ~/p/C/T/l/process_thread> ./wait test.txt

CHILD: Terminating with exit value 10

PARENT: Child terminated with exit value 10
~/p/C/T/l/process_thread> cat test.txt

Original process writes

Child written

Parent writes
参数stat_loc不仅保存了退出状态,还保存了其他的一些数据:进程的状态,促使进程保持此状态的信号量。通过专门为它们定义的宏可以获取全部的信息,退出状态保存在stat_loc的低八位—WEXITSTATUS.
借住wait,我们可以进行简单的进程间通信,当然进程间通信的具体进制很多(共享内存,信号量,pipe)。

1.4.3 waitpid –一个功能更强大的等待机制

wait调用存在很多的局限,进程死亡前,父进程一直处于阻塞的状态,即调用wait的进程不能做任何事情。此外,如果一个进程创建多个进程,则只要其中的一个子进程死亡了,wait调用就立即返回,不会等待某个特定的子进程死亡,wait不能用于进程组。
pid_twaitpid(pid_t pid,int *stat_loc,int options);
第二个参数和stat_loc的意义与它在wait中意义相同,进程的退出状态值就保存在这个参数里。Pid可以取四类值:

pid=-1,waitpid处于阻塞状态,直到子进程死亡或者进程的状态改变

pid>0,waitpid等待PID为pid的进程死亡,--不知道对进程组有没有限制

pid=0,waitpid等待进程组中任何一个进程的死亡,这一组进程是由同一个进程创建的

pid<-1,waitpid进程等待PGID等于pid绝对值的进程死亡(PGID是进程组标识代码,将在一节说明)

waitpid的执行方式也受options参数值的影响。options值可以零,也可以是WNOHANG,WUNTRACED,WCONTINUED这三个常量中的一个或他们的组合。options的以下两种情形:

当options为0时,waitpid处于阻塞状态,直到子进程的状态发生变化,当时进程的执行方式也受到pid值的影响

当options设置为WNOHANG时,waitpid按非阻塞模式运行,不管子进程的状态是否发生变化,它立即返回。

1.5 进程组

每个进程都属于某个进程组(Berkeley小组提出的控制一组具有共同特性的进程)。组中的每个进程具有同一个进程组标识号(processgroup-id,PGID)。进程组有一个领导者,领导者的GPID就是进程组的PID。waitpid系统调用可以等待进程组中任何一个成员的死亡.
Shell对进程组的处理方式取决于它是否支持作业控制。Bourneshell中,shell环境变量里运行的命令具有与shell本身相同的PGID,在其他的shell(csh,ksh,Bash)中并非如此,这三个shell都支持作业控制,每个命令有自己的进程组。在这三个shell中,管道也是作业,管道里的命令组成一个进程组。在一个登录会话中,一个用户可以有多个进程组,但是其中只有一个进程组在前台运行,其余的都在后台运行。前台运行的进程直接连接到会话的控制终端,可以利用键盘向组中的进程输入数据,也可以向进程发送信号。
不能通过终端向后台进程发送信号,但是可以通过其他的方式向它们发送信号(如kill命令)。一个后台进程是否能把数据写到终端,这要取决于系统的设置。当后台进程组试图从终端读取数据时,终端驱动程序就会发现,并立刻发送一个信号,挂起这个进程组。

1.6 僵尸进程和孤儿进程

shell的正常模式需要等待子进程死亡,实际上这不可行,shell支持作业控制,进程或者进程组后台运行或者处于挂起状态。如果父进程不等待子进程死亡,会出现以下的情况:

子进程死亡,父进程继续存活。---会形成僵尸进程

父进程死亡,子进程继续存活。---会形成孤儿进程

僵尸状态:内核清除进程地址空间,但是保留它在进程表中的记录.僵尸进程只能通过wait或者waitpid获取其退出状态并清除它在进程表中的记录。
孤儿进程:父进程死了,子进程变成孤儿,内核清除父进程在进程表里的记录,但是在清除之前,内核先检查它是否还有子进程存活,如果有,改变子进程的PPID,把init进程变成它的父进程。

1.6.1 orphan.c ---生成一个孤儿进程并且被init“收养”

/*Program: orphan.c -- Create an orphan by letting child sleep for 2 minutes
Parent doesn't call wait and dies immediately*/
#include<stdio.h>
#include<stdlib.h>
int main(void){
int pid;
if( pid = fork())
exit(10);
else if(pid == 0) { // child process
sleep(2);		//let parent die in this frame
printf("CHILD's PID is %d, Adopted by init now, PPID: %d\n",                                      getpid(),getppid());
exit(0);
}
}
编译命令: cc -o orphan orphan.c

执行命令: ./orphan
CHILD's PID is 17816, Adopted by init now, PPID: 1

init进程具有这样的功能,可以运行多个子进程而不会处在阻塞状态,每当有子进程死亡时,他就会及时读取他们的退出状态值。

1.7 exec –进程创建中的最后一个步骤

exec命令是由一个系统调用+五个库函数来实现的。exec组或者exec命令系列。实际上并没有一个exec的系统调用,只有一个execve系统调用,其他五个库函数都是以它为基础的。exec命令系列:execl组和execv组(l表示固定的参数列表,v表示可变的参数)。

int execl(const char * path,const char arg0,…/* (char *) 0 */); --execl.c


path是程序的路径名,可以是绝对路径,也可以是相对路径。Excel不能利用PATH环境变量定位wc命令,因此我们必须在execl的第一个参数里设置命令的路径。
为什么需要NULL指针:main把命令行中的字符串保存到*argv[]变量里,这个数组的最后一个元素是NULL指针,可以据此求得参数的个数并赋值给argc参数。使用execl运行一个程序时,无法知道参数的个数,exec必须使用手动的方式来计算。
int execv(const char *path,char *const argv[]);

17.1 execv.c

/*Program: execv.c -- Stuffs all command line arguments to an array to
be used with execv */
#include<stdio.h>
#include<unistd.h>
int main(int argc,char **argv){
char *cmdargs[] = {"grep","-i","-n","xiajian","/etc/passwd",NULL};
execv("/bin/grep",cmdargs);
printf("execv error\n");
}


可以在当前进程中使用exec函数,也可以在子进程使用exec系列函数。

1.7.2 exec_and_fork.c

/*Program: exec_and_fork.c -- Uses fork,exec and wait to run a Unix command*/
#include<stdio.h>
#include<wait.h>

int main(int argc,char **argv){
int returnval;
switch(fork()){
case 0:
if((execv(argv[1],&argv[2])<0)){
fprintf(stderr,"execl error\n");
exit(20);
}
default:
wait(&returnval);
fprintf(stderr,"Exit status:%d\n",WEXITSTATUS(returnval));
exit(0);
}
}


execlp和execvp函数提供了利用PATH环境变量定位命令的方法,更容易使用。

int execlp(const char * file,const char * arg0,... /*,(char *)0*/);
int execvp(const char *file,const * const argv[]);


注意:用exec命令系列的函数运行shell,awk,perl脚本程序时,要用execlp和execvp。默认情况下,exec函数创建一个shell进程,后者读取命令行中的脚本的命令。当然,我们也可以使用不采用这种方法,在脚本的开头加上一行解释器行。
execle和execve通过使enviro[]变量对新创建的进程有用,前四个函数自动将当前进程的环境传递给exec函数创建的进程。当需要让新的程序在新的运行环境时,就需要使用execle,execvp(执行脚本)。

int execle(const char* path,const char* arg0, … /*,(char *) 0,char*const envp[] */)
int execve(const char* path,const char* arg[],char* const envp[]) // 系统调用


1.7.3 编写一个功能有限的shell

/* Program: shell.c -- Accepts usr input as a command to be executed,
the strtok library function for parsing command line*/
//notice:shell只能执行所有的外部的命令,不可以执行bash的内部命令
#include<stdio.h>
#include<unistd.h>
#include<string.h>	//for strtok
#include<wait.h>

#define BUFSIZE 200
#define ARGVSIZE 40
#define DELIM "\n\t\r"

int main(int argc,char ** argv){
int i,n;
char buf[BUFSIZE+1];		//Stores the entered command line
char *clargs[ARGVSIZE+1];	//Stores the argument strings
int returnval;				//Used by wait
for(;;){					//loop forever
n =1;
write(STDOUT_FILENO,"shell>",7);	//display a prompt
read(STDIN_FILENO,buf,BUFSIZE);		//read user input into buf
if(!strcmp(buf,"exit\n"))			//terminate if user enters exit
exit(0);
clargs[0] =strtok(buf,DELIM);		//read first word
//continue parsing until all word are extracted
while((clargs
=strtok(NULL,DELIM))!=NULL)
n++;
clargs
= NULL;		//set last arguments pointer to NULL
switch(fork()) {
case 0:			//run command in child
if((execvp(clargs[0],&clargs[0]))<0)
exit(200);
default:		//in the parent process
wait(&returnval);//after the command has completed
printf("Exit status of command:%d\n",                                                                    WEXITSTATUS(returnval));
//initialize both the argument array and buffer that stores command
for(i=0;i<=n;++i)
clargs[i] ="\0";
for(i=0;i<BUFSIZE+1;++i)
buf[i] ='\0';
}
}
}


注意:使用system库函数会更加的方便,system库函数就是建立在fork,exec和wait系统调用上的。system函数只用一个参数,表示整个命令行。system利用shell执行命令,使用PATH环境变量,可以使用重定向和管道符。

2 文件描述符的使用

要使得前一个程序具有重定向和管道连接的功能,必须要深入了解Unix内核用于处理文件描述特性,内核总是把一个可用的最小整数作为打开文件的新的描述符。
close(STDOUT_FILENO); //关闭标准输出的描述符
open(“foo”,O_WRONLY|O_CREAT|O_TRUNC,S_IRUSR|S_IWUSR);
printf(“Thisstatement goes to foo;standard output redirected!\n”)
将上述三条语句加入到程序中,加入必要的头文件(fcntl.h,sys/stat.h,unistd.h)

2.1dup---复制文件描述符

两次打开一个文件—文件描述符指向两个文件表,各有各的文件偏移指针;复制文件描述符,共用一个文件偏移指针—这个正是redirect和pipe所需要的。
int dup(int filedes)

2.2 dup.c程序

/* Program :dup.c -- Uses dup to achieve both input and output redirection
closes standard stream first before using dup*/
#include<unistd.h>
#include<stdio.h>
#include<sys/stat.h>
#include<fcntl.h>
#define MODE600 (S_IRUSR|S_IWUSR)

int main(int argc,char **argv){
int fd1,fd2;
fd1 = open(argv[1],O_RDONLY);
fd2 = open(argv[2],O_WRONLY|O_CREAT|O_TRUNC,MODE600);

close(STDIN_FILENO);	//This should return descriptor 0
dup(fd1);
close(STDOUT_FILENO);	//This should return descriptor 1
dup(fd2);

execvp(argv[3],&argv[3]);//Execute any filter
printf("Failed to exec filter");
}


2.3 dup2 --复制文件描述符更好的方法

使用dup系统调用时是基于在close和dup语句之间不会发生任何意外事件的假设。实际上信号处理程序可能会在这个期间建立一个文件。dup2将close操作和dup操作合并为一个原子操作,从而使得dup操作更加的安全。

int dup2(int filedes,int filedes2)


dup2把filedes的内容复制到filedes2里,并返回filedes2,如果filedes2已经打开,则dup2先关闭它。

2.4 dup2.c

/* Program: dup2.c -- Open files in the parent and uses dup2 to
the child to reassign the descriptors */
#include<unistd.h>
#include<stdio.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<wait.h>
#include<stdlib.h>
#define OPENFLAGS (O_WRONLY | O_CREAT | O_TRUNC)
#define MODE600 (S_IRUSR|S_IWUSR)

void quit(char * message,int exit_status){
perror(message);
exit(exit_status);
}

int main(int argc,char * argv[]){
int fd1,fd2,exit_status,rv;
if(fork()==0){		//Child process
if((fd1 = open(argv[1],O_RDONLY)) == -1)
quit("Error in opening file for reading\n",1);
if((fd2 = open(argv[2],OPENFLAGS,MODE600))==-1)
quit("Error in opening file for waitting\n",1);
dup2(fd1,0); 	//Close standard input simultaneously
dup2(fd2,1);	//Close standard output simultaneously
execvp(argv[3],&argv[3]);
quit("exec error",2);
} else {			//Parent process
wait(&rv);
printf("Exit status: %d\n",WEXITSTATUS(rv));
}
}


fcntl—比dup和dup2更好的函数
POSIX规范把dup以及dup2称为多余的函数,建议使用fcntl系统调用,描述符复制只是fcntl众多功能之一,以下是等效功能:

dup或者dup2
fcntl相应的用法
fd=dup(filedes)
fd=fcntl(filedes,F_DUPFD,0)
fd=dup2(filedes,filedes2)
close(filedes2)
fd=fcntl(filedes,F_DUPFD,filedes2)
注意:复制文件描述符的两种方法:fork和dup(dup2);fork:父进程-->子进程。dup(dup2作用在同一个进程内)

3.pipe系统调用– 进程之间的通信

Unix在进程间通信采用了一种复杂的机制,子进程通过进程表中的退出码将退出状态传递给它的父进程,这是最原始的通信方式,信号机制,内核向进程发送信号,shell的管道机制。
缓冲区读写问题—生产者消费者问题。管道机制—半双工通信机制(数据的单向流通),管道是一种可以用read和write读取和写入的,它是由pipe系统调用创建的:

int pipe(int filedes[2]);


pipe调用的参数是一个包含两个整数的数组,这个数组保存了两个文件描述符,写入到filedes[1]的任何内容都可以从filedes[0]中读取。0,1的意义在管道里也没有发生变化(标准输入输出),filedes[1]的write调用把数据写入到一个固定大小的缓冲区里(4~8kb),filedes[0]的read调用从缓冲区读取数据。管道符可以由两个进程共享,也可以由一个进程单独使用,pipe的文件类是FIFO,我们可以使用S_ISFIFO宏对它进行检查。

3.1pipe与fork一起使用

为了让pipe与fork一起使用,通常的做法是在创建一个进程之前,建立一个管道。

3.2 pipe.c

/* Program:pipe.c -- Shares a pipe between two process.
We went the data to flow from the parent to the child*/
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>

void quit(char * message,int exit_status){
perror(message);
exit(exit_status);
}
int main(void){
int n, fd[2];		//fd[2] to be used up by pipe()
char buf[100];		//Buffer to be used by read()

if(pipe(fd) <0)		//fd[0]--read,fd[1]--write
quit("pipe",1);
switch(fork()){		//pipe has four descriptors after fork invoked
case -1: quit("Fork error",2);
case 0://CHILD process close write end of pipe and read from read end
printf("This is child process context:\n");
close(fd[1]);
n = read(fd[0],buf,100);
write(STDOUT_FILENO,buf,n);
break;
default://PARENT process close read end and write to write end
close(fd[0]);
write(fd[1],"Writing to pipe\n",16);
}
return 0;
}


3.3pipe2.c -- 在管道中运行Linux/Unix命令

/* Program: pipe2.c -- Runs two program in pipeline,Child runs cat,parent runs tr */
#include<unistd.h>
#include<stdio.h>
#include<stdlib.h>

void quit(char *message,int exit_status){
perror(message);
exit(exit_status);
}
int main(void){
int fd[2];

if(pipe(fd) < 0)
quit("pipe",1);
switch(fork()){
case -1 :
quit("fork",2);
case 0:
close(fd[0]);	//CHILD -close read end first
dup2(fd[1],STDOUT_FILENO);//connect stdout to write end and close original descriptor
close(fd[1]);
execlp("cat","cat","/etc/passwd",(char*) 0);
quit("cat",3);
default:
close(fd[1]);    //PARENT - close write end first
dup2(fd[0],STDIN_FILENO);//connect stdin to read end and close original descriptor
execlp("tr","tr","'[a-z]'","'[A-Z'",(char *) 0);
quit("tr",4);
}
}


4信号机制

信号是内核用来把一件事情的发生传递给进程的一个机制。通常进程以终止自己运行来相应信号,进程也可以对信号作出其他响应,例如不对信号作任何响应,或者调用自定义的函数。信号响应—信号处理(signaldisposition).
信号的来源:

键盘:ctrl+c SIGINT,ctrl+/ SIGOUT,ctrl+z SIGTSTP

硬件:SIGFPC,SIGILL,SIGSEGV

C程序:alarm系统调用的设定某个时刻会产生SIGALRM信号,abort系统调用产生SIGABRT信号

其他的信号源:内核,父子进程

消息类型(定义在bits/signum.h文件中)

信号
信号名
默认动作
简单说明
1
SIGHUP|HUP
终止进程
挂断。终端通信连接或者控制进程终止时产生的信号
2
SIGINT|INT
终止进程
中断。按下Ctrl-C时发出的信号
3
SIGQUIT|QUIT
终止进程,生成内存映像文件(core)
推出。按quit键(Ctrl-\|Ctrl+
shift+\)时产生的信号
4
SIGLL|ILL
终止进程,生成内存映像文件(core)
非法指令。执行非法的机器指令时产生的信号
5
SIGTRAP|TRAP
终止进程,生成内存映像文件(core)
硬件故障或断点跟踪等情况产生的信号
6
SIGABRT|ABRT
终止进程,生成内存映像文件(core)
异常终止信号(abort()函数产生的异常终止的信号)
7
SIGBUS|BUS
终止进程,生成内存映像文件(core)
总线故障
8
SIGFPE|FPE
终止进程,生成内存映像文件(core)
浮点运算异常。
9
SIGKILL|KILL
终止进程
无条件的终止进程信号
10
SIGUSR1|USR1
终止进程
用户定义信号,编程使用。
11
SIGSEGV|SEGV
终止进程,生成内存映像文件(core)
内存地址越界或者访问权限不足时产生的信号(当访问地址超出进程地址空间)
12
SIGUSR2|USR2
终止进程
用户定义信号,编程使用。
13
SIGPIPE|PIPE
终止进程
管道断开信号
14
SIGALRM|ALRM
终止进程
alarm()系统调用产生的时钟超时信号
15
SIGTERM|TERM
终止进程
进程终止信号,kill命令的默认信号
16
SIGSTKFLT|STKFLT
终止进程
栈故障信号
17
SIGCHLD|CHLD
忽略
子进程状态发生变动信号
18
SIGCONT|CONT
继续(或忽略)
令进程继续运行的信号,作业控制使用。
19
SIGSTOP|STOP
终止进程
停止进程运行信号,用于作业控制。
20
SIGTSTP|TSTP
终止进程
键盘停止信号Ctrl-Z
21
SIGTTIN|TTIN
终止进程
后台进程试图从控制终端读取数据时产生的信号
22
SIGTTOU|TTOU
终止进程
后台进程试图向控制终端输出数据时产生的信号
23
SIGURG|URG
0
当网络链接中收到数据的数据发生错误,通知进程出现紧急情况是发送的信号
24
SIGXCPU|XCPU
终止进程,生成内存映像文件(core)
进程超过最大软性CPU时间限制是产生的信号
25
SIGXFSZ|XFSZ
终止进程,生成内存映像文件(core)
当进程创建文件超过其能创建的软性最大文件容量限制是产生的信号
26
SIGVTALRM|VTALRM
终止进程
settimer系统调用设置的虚拟间隔时钟超时信号
27
SIGPROF|PROF
settimer系统调用设置的内核抽样间隔时钟超时信号
28
SIGWINCH|WINCH
0
窗口大小发生变动是产生的消息
29
SIGIO|IO
0
异步I/O事件出现后的信号
30
SIGPWR|PWR
0
电源故障信号,改由UPS提供系统电源供电时的信号
31
SIGSYS|SYS
终止进程,生成内存映像文件(core)
系统调用有误。非法系统调用的错误

4.1 信号处理

一个进程响应某个动作取决于进程的程序设置,程序收到信号时,通常使用三种措施:

忽略这个信号

终止进程

挂起进程

上述的处理方法通常是某个信号默认的响应的方式,通过signal系统调用捕获信号,可以确定信号的相应方法。signal的处理的方法:

忽略信号

恢复默认相应的方式

调用信号处理函数

4.2 内核处理信号的过程

与进程一样,信号也有生周周期,每个周期分为几个阶段。一个信号首先被产生,然后发给一个进程,当信号的响应多做已被执行,可以认为信号已经转交给接受者。信号在转交接受者之前处于挂起状态,
每当一个信号发送给一个进程时,内核就把进程表的挂起进程屏蔽字中的某一位置1,表示进程收到某类信号。这个屏蔽字为每类信号保留了一个二进制位。然后进程检查这个屏蔽字和信号处理表,从而采取相应的响应方式:忽略,终止或者调用一个信号处理程序。
当进程正在执行系统调用时,内核会密切监视进程的执行过程,根据具体的情况考虑是否立即发信号给进程。

4.3 与信号有关的系统调用

信号处理机制,BSD小组 vs SVR4,延续至今,信号不可靠。

signal设置信号处理方式

kill作用类似于kill命令,kill调用是用来发送信号,而不是终止进程。库函数raise利用kill调用向当前进程发送任何信号,kill可以给任何进程发送任何信号。

alarm设置一个定时器,在某个设定的时刻发出SIGALRM信号,sleep调用使用了alarm

pause相当与shell的read语句,暂停程序的运行

4.3.1 signal调用

signhandler_t signal(int signum, signhandler_t  handler);


signal调用的第二个参数可以是函数指针或者SIG_IGN(ignore),SIG_DFL(default)

4.3.2 signal.c --相应SIGALRM信号

/*Program: signal.c -- waits for 5 seconds for user input and then generates
SIGALRM that has a handler specified*/
#include<stdio.h>
#include<unistd.h>
#include<signal.h>
#include<stdlib.h>
#define BUFSIZE 100

void alrm_handler(int signo);	//Prototype declarations for signal handler and quit function
void quit(char*,int);
char buf[BUFSIZE] ="foo\n";		//Global variable
int main(void){
int n;
if(signal(SIGALRM,alrm_handler) == SIG_ERR)//signal return SIG_ERR on error
quit("sigalrm",1);
fprintf(stderr,"Enter filename: ");
alarm(5);//set alarm clock,will deliver SIGALRM in 5 seconds
n = read(STDIN_FILENO,buf,BUFSIZE);
if(n>1)	//if user inputs string within 5 seconds,this will be perform
printf("Filename: %s\n",buf);
return 0;
}
void alrm_handler(int signo){	     //Invoked with process receives SIGALRM
signal(SIGALRM,alrm_handler);//Resetting signal handler
fprintf(stderr,"\nSignal %d received,default filename:%s\n",signo,buf);
exit(1);
}
void quit(char* message,int exit_status){
perror(message);
exit(exit_status);
}


4.3.2 为何signal调用不可靠

SystemV系统(AT&T发布的版本)中,信号处理函数在被调用执行时,信号响应方式会被设置为默认方式。如果希望信号处理方式前后一致,需要在信号处理函数开始位置重新设置信号处理函数。这种方式会导致一种竞争的状态。
Linux采用的BSD的信号,信号处理程序没有复位到默认状态,但是重新安装处理程序总是一种安全的措施。
捕获来自键盘终端的信号,SIGINT和SIGTSTP信号—signal2.c

4.3.3 kill系统调用

int kill(pid_t pid,int sig);

kill系统调用中的参数pid并不总是代表单个进程,与waitpid类似,可以有四类取值,表明可以把一个信号发送给任何一个进程组。

4.3.4 killprocess.c --利用kill产生信号

/*Program:killprocess.c -- Uses fork and exec to run a user-defined program
and kill it if it doesn't in 5 seconds.*/
#include<stdio.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<signal.h>

pid_t pid;
int main(int argc,char ** argv){
int i,status;
void death_handler(int signo);	//A common signal handler function
signal(SIGCHLD,death_handler);	//death_handler will be invoked when a child dies
signal(SIGALRM,death_handler);	//or an alarm is received

switch(pid = fork()){
case -1:
printf("Fork error\n");
break;
case 0:
execvp(argv[1],&argv[1]);//execute command
perror("exec");
break;
default:
alarm(5);	//will send SIGALRM after 5 seconds
pause();	//will return when SIGCHLD signal is received
printf("Parent dies\n");
}
return 0;
}
/* This common handler picks up then exit status for normal termination but sends the
* SIGTERM signal terminate child process if command doesn't complete in 5 seconds
*/
void death_handler(int signo){
int status;
signal(signo,death_handler);
switch(signo){
case SIGCHLD:
waitpid(-1,&status,0);
printf("Child dies:exit status:%d\n",WEXITSTATUS(status));
break;
case SIGALRM:
if(kill(pid,SIGTERM) == 0)
fprintf(stderr,"5 seconds over, child killed\n");
}
}


killprocess.c运行一个由用户输入的命令(execvp),如果命令在5s内结束,就输出它的退出状态,否则父进程利用kill调用向子进程发送SIGTERM信号。
也可以使用kill调用给当前进程发送信号,raise(SIGTERM)== kill(getpid(),SIGTERM)

5小结

进程的基本概念:地址空间,进程表,系统调用fork,exec系列(execve),wait,waitpid,exit,_exit,
dup,dup2,kill. IPC高级的内容:semaphores,消息队列,共享内存--->套接字编程,可靠信号处理,POSIX(Solaris和Linux),getpid,getppid,*envirno[](getenv,setenv|putenv),pipe

参考资料

[1] Unix 原理与应用(第四版),印 Sumitabba Das著 ,吴文国译,清华大学出版社, 2008.1
[2]
Ubuntu权威指南,邢国庆等著,人民邮电出版社, 2010.1
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: