每个进程都有一个非负整型的唯一进程ID,通过ps命令可以查看到 ps -aux | more
每个进程还有一些其他标识符、下列函数返回这些标识符:
pid_t getpid(void); 返回:调用进程的进程 ID
pid_t getppid(void); 返回:调用进程的父进程 ID
uid_t getuid(void); 返回:调用进程的实际用户 ID
uid_t geteuid(void); 返回:调用进程的有效用户 ID
gid_t getgid(void); 返回:调用进程的实际组 ID
gid_t getegid(void); 返回:调用进程的有效组 ID
一个现存进程调用 fork 函数是 Unix 内核创建一个新进程的唯一方法;
pid_t fork(void);
功能:创建新进程。
由fork创建的新进程被称为子进程,此函数返回两次,对于子进程的返回值是0,父进程的返回值则是新子进程的进程ID;
注意:fork 后的子进程复制父进程的环境,堆栈,代码段,数据段等,这是子进程所拥有的拷贝,父子进程不共享这些存储空间部分,此时父子进程的数据是分开的,所以父子进程要进行数据交换的话就需要进行进程间通信。
fork有两种用法:
进程有三种正常终止法及两种异常终止法:
正常终止:
异常终止
注意:不管进程如何终止,最后都会执行内核中的同一段代码。这段代码为相应进程关闭所有打开描述符,释放它所使用的内存空间等等。
pid_t wait (int *statloc);
pid_t waitpid (pid_t pid , int *statloc , int options);
功能:等待一个进程的终止。然后返回此进程的ID,此进程的终止状态存于整形指针statloc中;
区别:
在子进程终止前调用,wait 会阻塞,waitpid 可使调用者不阻塞
若一个进程存在多个子进程,调用这两个函数的话,wait 等待第一个终止的子进程就返回,而 waitpid 可根据参数 pid 等待任意的子进程的终止;
在 UNIX 系统中,一个进程结束了,但是他的父进程没有等待(调用 wait / waitpid)他,那么他将变成一个僵尸进程,但是如果该进程的父进程已经先结束了,那么该进程就不会变成僵尸进程,因为每个进程结束的时候,系统都会扫描当前系统中所运行的所有进程,看有没有哪个进程是刚刚结束的这个进程的子进程,如果是的话,就由 Init 来接管他,成为他的父进程。
waitpid 的参数 pid 解释如下:
参数 options 进一步控制 waitpid 的操作,此参数或者 0,或者是下列常数的或运算:
4.3+BSD 提供了两个附加函数 wait3 和 wait4,定义如下:
pid_t wait3(int *statloc,int options,struct rusage *rusage);
pid_t wait3(int *statloc,int options,struct rusage *rusage);
这两个函数相比较 wait 和 waitpid 函数多了一个参数rusage,该参数要求内核返回由终止进程及其所有子进程使用的资源摘要:资源信息包括用户 CPU 时间总量、系统 CPU时间总量、缺页次数、接收到信号的次数等.
举例 :由下面一个程序来说明 fork 函数和 waitpid 函数的用法
#include <sys/types.h>
#include < sys/wait.h > //提供函数waitpid
#include ”ourhdr.h”
int main(void)
{
pid_t pid;
If((pid = fork()) < 0) // 复制当前进程,如果返回负数,函数 fork出错
err_sys (“fork error”);
else if (pid == 0){ // 如果返回的PID为0,则为子进程
If((pid = fork()) < 0) // 复制第二个子进程,如果返回负数,函数 fork 出错
err_sys(“fork error”);
else if (pid > 0) // 第一个子进程结束,这时导致第二个子进程成为孤儿进程
exit(0); // 孤儿进程由 init 进程收养,所以第二个子进程永远不会是僵死进程
sleep(2);
printf (“second child,parent pid=%d\n”,getpid());
exit(0);
}
if (waitpid (pid,NULL,0) != pid) // NULL 表示我们不关心子进程的终止状态
err_sys(“waitpid error”);
exit(0);
}
上面的例子是对进程控制的一个简单的说明,实现了调用两次 fork 避免僵死进程的小技巧,利用的是 init 进程永远不会终止这一条件,当我们编程时可以避免僵死进程的出现,这样会提高程序的运行效率。
僵尸进程的危害见本页胶片下面的备注信心说明。
用 fork 可以创建新进程,用exec可以执行新的程序。
注意:一个进程一旦调用 exec 函数,就代表它本身已经 死亡 了,系统将把此进程的代码段换成新的程序的代码段,为新程序分配新的数据段与堆栈段。但是进程的ID不变,因为 exec 不创建新进程。
前面曾提及在执行 exec 后,进程 ID 没有改变。除此之外,执行新程序的进程还保持了原进程的下列特征:
int system (const char * cmdstring);
功能:运行命令程序 cmdstring,像在 shell 下面启动一个进程一样。
cmdstring 为空指针时,仅当命令处理程序可用,system 返回非 0 值,System 函数在其实现中调用了 fork、exec、waitpid 函数,故有三种返回值:
fork 失败或者 waitpid 返回除 EINTR 之外的出错,system 返回-1,且 error 中设置了错误类型
如果 exec 失败(表示不能执行 Shell),返回值如 Shell 执行 exit(127) 一样。
三个函数都成功,且 system 的返回值是 Shell 的终止状态,其格式由 waitpid 决定
sysytem 函数的目的:system 函数的实现中存在着多种出错处理和信号处理,对于实现某个命令简单,便捷,例如:要是想实现 date 字符串代表的命令程序,直接调用 system('date') 即可
system 函数对于参数命令的实现:
system 函数的一种实现:
# include <sys/types.h>
# include <sys/wait.h>
# include <errno.h>
# include <unistd.h>
int system(const char *cmdstring){
pid_t pid;
int status;
if(cmding == NULL) // 为空指针时返回1
return(1);
if ((pid = fork() < 0)){
status = -1; // fork 出错为第一种情况
}else if (pid == 0){
execl(“/bin/sh”,”sh”,”-c”,cmdstring, (char*) 0);
_exit(127); // exec 失败 第二种情况
}else{
while (waitpid(pid, &status, 0) < 0)
if (errno != EINTR) { // waitpid 返回 EINTR 之外的出错,第一种情况.
status = -1;
break;
}
}
return(status);
}
信号集处理函数:
int sigemptyset(sigset_t* set) ; // 初始化由 set 指向的信号集,使其排除其中所有信号
int sigfillset(sigset_t* set) ;// 初始化由 set 指向的信号集,使其包括所有信号
注意:应用程序在使用信号集前,要对该信号集调用 sigemptyset 或 sigfillset 一次
int sigaddset(sigset_t *set, int signo) ;// 将信号 signo 添加到信号集中
int sigdelset(sigset_t *set, int signo) ;// 将信号 signo 从信号集中删除
int sigismember(const sigset_t *set, int signo) ;// 检测信号 signo 是否存在于信号集中。
信号未决:在信号产生和递送之间的时间间隔。
信号屏蔽字:每个进程都有一个信号屏蔽字,它规定了当前要屏蔽递送到该进程的信号集
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
功能:以检测或更改(或两者)进程的信号屏蔽字。
oset 是非空指针,进程的当前信号屏蔽字通过 oset 返回。若 set 非空,how 说明了如何修改信号屏蔽字。How 说明如下:
int sigpending(sigset_t *set);
功能:返回当前未决的信号集。Set 指定此信号集。
int sigaction(int signo, const struct sigactioan *act,struct sigactiono *oact);
功能:检查或修改(或两者)与指定信号相关联的处理动作。
signo 是要检测或修改具体动作的信号的编号数。struct sigactioan 结构如下:
struct sigaction {
void (*sa_handler) (); // 捕捉信号,忽略或者默认
sigset_t sa_mask;
int sa_flags;
}
若 act 指针非空,则要修改其动作。 act 结构的 sa_flags 字段包含了对信号进行处理的各个选择项(详见资料)。
进程间通信使用的函数除了前面说过的 signal 函数外,还有另两个重要的函数—— kill,raise 函数
int kill(pid_t pid, int signo);
int raise(int signo);
功能: kill 发送信号给指定进程。raise 发送信号给进程本身。
根据参数 pid 的不同,将信号发送给不同ID的进程。(此处的 pid 取值同 waitpid);
注意:signo 为信号编号,当为 0 时,kill 执行错误检查,但是不发送信号,此原理常被用来检查一个特定进程是否存在。如果向一个并不存在的进程发送空信号,则 kill 返回 -1,errno 则被设置为 ESRCH。
定义:当多个都企图对共享数据进行某种处理,而最后的结果取决于进程运行的顺序时,认为发生了竞态条件。
根本原因:是有两个或者多个进程对于共享的资源使用有先后顺序,从而导致输出结果的不同。
比如:如果在调用完 fork 函数后,某种逻辑显式或者隐式的依赖于在 fork 之后是父进程先运行还是子进程先运行,此时就出现了竞态条件,通常,运行顺序依赖于系统负载以及内核的调度算法。
下面举例说明竞态条件:
# include <sys/types.h>
# include “ourhdr.h”
static void charatatime(char*);
int main(void)
{
pid_t pid:
if ((pid = fork()) < 0)
err_sys(“fork error”);
else if (pid == 0){
charatatime (“output from child\n”);
}else{
charatatime (“output from parent\n”);
}
exit(0);
}
static void charatatime (char*str)
{
char * ptr;
int c ;
setbuf (stdout , NULL); //将标准输出设置为不带缓存的
for (ptr = str; c = * ptr++; )
put (c ,stdout);
}
此程序设置输出为不带缓存的。这样每输出一个字符都调用函数 write,目的是使内核尽可能多的在父子进程之间切换,以示例竞态条件;
由于存在竞态条件此程序的输出结果有多种情况,比如:
1 output from child
output from parent
2 oouupptt ffrroomm
cphairledn
3 oouupptt ffrroomm
pchairledtt
4 ooutput from patent
utput from child
可以看到当父子进程执行顺序不同时,输出的结果会差很多
下面提供 TELL,WAIT 函数避免竞态条件;对于上个程序进行修改:
#include <sys/types.h>
#include “ourhdr.h”
static void charatatime(char*);
int main(void){
pid_t pid;
TELL_WAIT();
if ((pid = fork()) < 0)
err_sys(“fork error”);
else if (pid == 0){
WAIT_PARENT(); //使父进程先运行
charatatime (“output from child\n”);
}else{
charatatime (“output from parent\n”);
TELL-CHILD(pid); //通知子进程执行
exit(0);
}
static void charatatime (char*str){
char * ptr;
int c ;
setbuf (stdout , NULL); //将标准输出设置为不带缓存的
for (ptr = str; c = * ptr++;)
put (c ,stdout);
}
此时程序的输出结果将会只有一个:
output from parent
output from child
