系统编程--进程
进程环境
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则先执行特定清理处理然后从内核返回
#include <stdlib.h> void exit(int status); void _Exit(int status); #include <unistd.h> void _exit(int status);
atexit函数
根据ISO C,一个进程可以最多注册32个函数,这些函数由exit函数调用的函数。这些被称为exit处理器,并通过调用atexit函数来登记这些函数。
#include <stdlib.h> int atexit(void (*func)(void)); //成功返回0,错误返回非0值。
传递一个函数地址作为atexit的参数。当这个函数被调用时,不传入任何参数也不返回任何值。exit函数以它们注册的顺序的相反顺序调用这些函数。每个函数都被调用和它被注册的一样多的次数。
3.命令行参数
当一个程序被执行时,使用exec的进程可以传递命令行参数给这个新的程序。这是UNIX系统shell的普通操作的一部分
4.环境表
每个程序还被传入一个环境列表。就像参数列表那样,环境列表是一个字符指针的数组,每个指针包含一个以null终止的C字符串的地址。这个指针数组的地址包含在全局变量environ里:
extern char **environ;
5.程序的存储空间布局
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).堆,动态内存分配通常发生的地方。历史上,堆一直放在未初始化数据和栈之间。
6.存储器分配
ISO C为内存分配规定了三个函数:
1).malloc:分配指定字节数量的内存。内存的初始值是不确定的。
2).calloc:为指定数量的指定尺寸的对象开辟空间。这个空间被初始化为0。
3).realloc:增加或减少之前开辟的区域。当长度增加时,它可能会导致把之前开辟的空间移到其它地方,来在尾部提供额外的空间。还有,当长度增加时,在旧对空和新区域尾部之间的空间的初始值是不确定的。
#include <stdlib.h> void *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,可以用其环境变量值,但是该标准又称环境的内容是由实现定义的
#include <stdlib.h> char *getenv(const char *name); //返回和name相关的值的指针,没有找到则返回NULL。
注意这个函数返回一个name=value的字符串的指针。我们应该使用getenv来从环境得到指定的值,而不是直接访问environ。
include <stdlib.h> int putenv(char *str); int setenv(const char *name, const char *value, int rewrite); int unsetenv(const char *name); //三者成功返回0,错误返回非0.
1.putenv函数取形式为name=value的字符串,并把它放在环境列表中。如果name已经存在,它的旧的定义会首先被移除。
2.setenv将name设置为value,如果name存在于环境中,那么a、如果rewrite为非0,则存在的name的定义首先被移除;b、如果rewrite为0,name的已存在的定义不被删除,name不会被设置为新的value,也没有错误发生。
3.unsetenv函数删除任何name的定义。
注意putenv和setenv的区别。setenv必须开辟内存来创建它参数的name=value的字符串,putenv可以把字符串直接传给环境。
#include <stdlib.h> #include <stdio.h> int main(void) { printf("PATH=%s\n", getenv("PATH")); setenv("PATH", "hello", 1); printf("PATH=%s\n", getenv("PATH")); return 0; }
8.getrlimit和setrlimit函数
每个进程都有一堆资源限制,其中一些可以用getrlimit和setrlimit函数查询和更改。
#include <sys/resource.h> int 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 */ };
查看进程资源限制
cat /proc/self/limits
ulimit -a
进程控制
1.进程标识符
要点:
1).每个进程都有一个非负整型表示的唯一的进程ID。因为进程ID是一个总是唯一的,常将其用作其他标示符的一部分以保证其唯一性。例如:应用程序有时包含进程ID作为文件名的一部分,来产生唯一的文件名。
2).虽然唯一,但是进程ID可以重用
3).进程ID 0通常是调用进程,常常称为对换程序(swapper)。在硬盘上没有对应于这个进程的程序,它是内核的一部分而被熟知为一个系统进程。进程ID 1通常是init进程,在启动过程结束后被内核调用。
下列函数返回这些标识符:
#include <unistd.h> 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。
2.fork函数
一个存在的进程可以调用fork函数来创建一个新的进程
#include <unistd.h> pid_t fork(void); //在子进程里返回0,在父进程里返回子进程ID,错误返回-1。
1)由fork函数创建的新进程被称为子进程。函数被调用一次却被返回两次。返回的唯一的区别是子进程的返回值是0,而父进程的返回值是新的子进程的ID。
2)子进程和父进程都持续执行fork调用之后的指令。子进程是父进程的一个复制品。例如,子进程得到父进程数据空间,堆和栈的拷贝。注意这对一个子进程来说是一个拷贝。父进程和子进程没有共享内存。父进程和子进程共享代码段。
3)当前实现不执行一个父进程数据、栈和堆的完全拷贝,linux2.6内核中引入读时共享写时复制技术(COW)。
在fork之后有两种处理描述符的普通情况:
4)父进程等待子进程完成。在这种情况下,父进程不用对它的描述符做任何事情。当子进程终止时,子进程写过或读过的任何共享的描述符都有相应地更新它们的偏移量。
5)父进程和子进程独立工作。这里,在fork之后,父进程关闭它不需要的描述符,而子进程做同样的事。这样,两者都不干涉另一个打开的描述符。这种情景通常是网络服务的情况。
有两种fork的使用:
6)当一个进程想复制它自己以便父进程和子进程可以同一时间执行不同的代码段
7)当一个进程想执行一个不同的程序时。这对shell很普遍。
#include <sys/types.h> #include <unistd.h> #include <stdio.h> #include <stdlib.h> int main(void) { pid_t pid; char *message; int n; pid = fork(); if (pid < 0) { perror("fork failed"); exit(1); } if (pid == 0) { message = "This is the child\n"; n = 6; } else { message = "This is the parent\n"; n = 3; } for(; n > 0; n--) { printf(message); sleep(1); } return 0; }
文件共享
文件描述符合打开文件之间的关系
每一个文件描述符会与一个打开文件相对应,同时,不同的文件描述符也会指向同一个文件。相同的文件可以被不同的进程打开也可以在同一个进程中被多次打开。系统为每一个进程维护了一个文件描述符表,该表的值都是从0开始的,所以在不同的进程中你会看到相同的文件描述符,这种情况下相同文件描述符有可能指向同一个文件,也有可能指向不同的文件。具体情况要具体分析,要理解具体其概况如何,需要查看由内核维护的3个数据结构。
1. 进程级的文件描述符表
2. 系统级的打开文件描述符表
3. 文件系统的i-node表
进程级的描述符表的每一条目记录了单个文件描述符的相关信息。
1. 控制文件描述符操作的一组标志。(目前,此类标志仅定义了一个,即close-on-exec标志)
2. 对打开文件句柄的引用
内核对所有打开的文件的文件维护有一个系统级的描述符表格(open file description table)。有时,也称之为打开文件表(open file table),并将表格中各条目称为打开文件句柄(open file handle)。一个打开文件句柄存储了与一个打开文件相关的全部信息,如下所示:
1. 当前文件偏移量(调用read()和write()时更新,或使用lseek()直接修改)
2. 打开文件时所使用的状态标识(即,open()的flags参数)
3. 文件访问模式(如调用open()时所设置的只读模式、只写模式或读写模式)
4. 与信号驱动相关的设置
5. 对该文件i-node对象的引用
6. 文件类型(例如:常规文件、套接字或FIFO)和访问权限
7. 一个指针,指向该文件所持有的锁列表
8. 文件的各种属性,包括文件大小以及与不同类型操作相关的时间戳
一些总结:
1. 由于进程级文件描述符表的存在,不同的进程中会出现相同的文件描述符,它们可能指向同一个文件,也可能指向不同的文件
2. 两个不同的文件描述符,若指向同一个打开文件句柄,将共享同一文件偏移量。因此,如果通过其中一个文件描述符来修改文件偏移量(由调用read()、write()或lseek()所致),那么从另一个描述符中也会观察到变化,无论这两个文件描述符是否属于不同进程,还是同一个进程,情况都是如此。
3. 要获取和修改打开的文件标志(例如:O_APPEND、O_NONBLOCK和O_ASYNC),可执行fcntl()的F_GETFL和F_SETFL操作,其对作用域的约束与上一条颇为类似。
4. 文件描述符标志(即,close-on-exec)为进程和文件描述符所私有。对这一标志的修改将不会影响同一进程或不同进程中的其他文件描述符
3.vfork函数
1).vfork函数倾向于创建一个新的进程,当这个新进程的目的是exec一个新的程序时。vfork函数创建这个进程,就像是fork一样,并不把父进程的地址空间拷贝到子进程里,因为子进程不会引用那个地址空间。子进程在vfork后马上简单地调用exec(或exit)。相反,当子进程在运行时并直到它调用exec或exit,子进程在它父进程的地址空间运行。这种优化在UNIX的一些换页虚拟内存实现上提供了一个效率上的收获。(如我们在上一节看到的,实现用写时复制来提高fork之后紧接exec的效率,然而完全不拷贝仍然比做一些拷贝要快。)
2).两个函数之间的另一个区别是,vfork保证子进程先运行,直到子进程调用exec或exit。当子进程调用这些函数的任一个时,父进程恢复执行。(如果子进程在调用这两个函数的任一个之前依赖于父进程更多的操作,这可能会造成死锁。)
4.exit函数
一个进程的5种退出方式:
1).从main函数里执行一个return。
2).调用exit函数。这个函数由ISO C定义并包含调用所有通过调用atexit注册的退出处理器,和关闭所有的标准I/O流。因为ISO C不处理文件描述符、多进程(父进程和子进程)、和工作控制,所以这个函数的定义对于UNIX系统来说是不完全的。
3).调用_exit或_Exit函数。ISO C定义_Exit为一个进程提供不运行exit处理器或信号处理器的终止方法。标准I/O流是否被冲洗取决于实现。在UNIX系统上,_Exit和 _exit是同义词,并不冲洗标准I/O流。_exit函数被exit函数调用并处理UNIX系统相关的细节
4).在进程的最后一个线程的启动例程里执行一个return。但是这个线程的返回值不被用途进程的返回值。当最后一个线程从它的启动例程里返回时,进程以终止状态0退出。
5).在进程的最后一个线程里调用ptherad_exit函数。和上一种情况一样,进程的返回状态始终为0,而不管传递给pthread_exit的参数。我们将在11.5节看到更多关于pthread_exit的细节
以下是三种异常退出的形式:
1)调用abort。这是下一项的特殊情况,因为它产生SIGABRT信号。
2).当进程收到特定信号时。(我们在第10章描述信号。)系统可以由进程本身产生--例如,通过调用abort函数--由别的进程产生,或者由内核产生。由内核产生信号的例子包括进程引用一块不在其地址空间的内存地址,或者尝试除以0。
3).最后一个线程响应一个取消请求。默认情况下,取消发生在一个延后的行为:一个线程请求另一个取消,在一段时间后,目标线程终止。
5.wait和waitpid函数
一个调用wait或waitpid的进程会:
● 阻塞,如果它所有的子进程都还在运行;
● 如果一个子进程终止并等待它的终止状态被获取,则随着子进程的终止状态立即返回;
● 如果它没有任何子进程,则立即返回一个错误。
如果进程是因为收到SIGCHLD信号而正调用wait,那么我们预期wait会立即返回。但是如果我们在任何随机的时间点时调用它,那么它会阻塞。
#include <sys/wait.h> pid_t wait(int *statloc); pit_t waitpid(pid_t pid, int *statloc, int options); //两者成功返回进程ID,失败返回0或-1
这两个函数的区别如下:
● wait函数可以阻塞调用者,直到一个子进程终止,而waitpid有选项可以避免它阻塞;
● waitpid函数不等待最先终止的子进程;它有许多选项来控制进程等待哪个进程
如果一个子进程已经终止并成为一个僵尸,那么wait会用子进程的状态立即返回。否则,它会阻塞调用者,直到子进程终止。如果调用者阻塞并有多个子进程,那么wait当某个进程终止时返回。我们总是可以知道哪个子进程终止了,因为这个函数返回这个进程ID。
waitpid函数提供了三个不被wait函数提供的特性:
1).waitpid函数让我们等待一个特定的进程,而wait函数返回任何终止的子进程的状态。
2).waitpid函数提供了一个wait的非阻塞版本。有时我们想得到子进程的状态,而不想阻塞。
3).waitpid函数用WUNTRACED和WCONTINUED选项提供了对工作控制的支持。
6.waitid函数
waitid允许一个进程指定要等待哪些子进程。不是把这个信息和进程ID或进程组ID一起合并到单个参数里,而是使用两个单独的参数,参数选项详见APUE
#include <sys/wait.h> int waitid(idtype_t idtype, id_t id, siginfo_t *infop, int options); //成功返回0,失败返回-1
一个进程在终止时会关闭所有文件描述符,释放在用户空间分配的内存,但它的PCB还保留着,内核在其中保存了一些信息:如果是正常终止则保存着退出状态,如果是异常终止则保存着导致该进程终止的信号是哪个。这个进程的父进程可以调用wait或waitpid获取这些信息,然后彻底清除掉这个进程。我们知道一个进程的退出状态可以在Shell中用特殊变量$?查看,因为Shell是它的父进程,当它终止时Shell调用wait或waitpid得到它的退出状态同时彻底清除掉这个进程。
如果一个进程已经终止,但是它的父进程尚未调用wait或waitpid对它进行清理,这时的进程状态称为僵尸(Zombie)进程。任何进程在刚终止时都是僵尸进程,正常情况下,僵尸进程都立刻被父进程清理了,为了观察到僵尸进程,我们自己写一个不正常的程序,父进程fork出子进程,子进程终止,而父进程既不终止也不调用wait清理子进程:
#include <unistd.h> #include <stdlib.h> int main(void) { pid_t pid=fork(); if(pid<0) { perror("fork"); exit(1); } if(pid>0) { /* parent */ while(1); } /* child */ return 0; }
若调用成功则返回清理掉的子进程id,若调用出错则返回-1。父进程调用wait或waitpid时可能会:
* 阻塞(如果它的所有子进程都还在运行)。
* 带子进程的终止信息立即返回(如果一个子进程已终止,正等待父进程读取其终止信息)。
* 出错立即返回(如果它没有任何子进程)。
这两个函数的区别是:
* 如果父进程的所有子进程都还在运行,调用wait将使父进程阻塞,而调用waitpid时如果在options参数中指定WNOHANG可以使父进程不阻塞而立即返回0。
* wait等待第一个终止的子进程,而waitpid可以通过pid参数指定等待哪一个子进程。可见,调用wait和waitpid不仅可以获得子进程的终止信息,还可以使父进程阻塞等待子进程终止,起到进程间同步的作用。如果参数status不是空指针,则子进程的终止信息通过这个参数传出,如果只是为了同步而不关心子进程的终止信息,可以将status参数指定为NULL。
例 waitpid
#include <sys/types.h> #include <sys/wait.h> #include <unistd.h> #include <stdio.h> #include <stdlib.h> int main(void) { pid_t pid; pid = fork(); if (pid < 0) { perror("fork failed"); exit(1); } if (pid == 0) { int i; for (i = 3; i > 0; i--) { printf("This is the child\n"); sleep(1); } exit(3); } else { int stat_val; waitpid(pid, &stat_val, 0); if (WIFEXITED(stat_val)) printf("Child exited with code %d\n", WEXITSTATUS(stat_val)); else if (WIFSIGNALED(stat_val)) printf("Child terminated abnormally, signal %d\n", WTERMSIG(stat_val)); } return 0; }
7.竞争条件
1).竞争条件当多个进程尝试用共享数据时发生,而最终的结果取决于进程运行的顺序,fork函数被调用时候容易产生竞争条件
2).对于一个父进程和子进程的关系,我们经常有以下的情景。在fork之后,父进程和子进程都有一些事情做。例如,父进程可能利用子进程ID更新一个日志文件的记录,而子进程可能必须为父进程创建一个文件。
8.exec函数
fork的一个用法是创建一个新进程(子进程)然后调用某个exec函数然执行另一个程序。当一个进程调用某个exec函数时,这个进程被新程序完全取代,而新程序开始执行它的main函数。在调用exec时进程的ID并没有发生变化,因为没有一个新的进程被创建;exec只是把当前的进程--它的代码、数据、堆和栈--替换为从硬盘而来的全新的程序。
6个不同的exec函数
#include <unistd.h> int execl(const char *pathname, const char *arg0, ... /* (char *) 0 */ ); int execv(const char *pathname, char * const argv[]); int execle(const char *pathname, const char *arg0, ... /* (char *)0, char *const envp[] */ ); int execve(const char *pathname, char *const argv[], char *const envp[] ); int execlp(const char *filename, const char *arg0, ... /* (char *)0 */ ); int execvp(const char *filename, char *const argv[]); //六个函数错误都返回-1,成功不返回。
exec使用
使用v的,使用数组作为参数,第一个参数为程序本身 使用l的,使用参数列表,第一个参数为程序本身,最后一个参数为(char *)0 使用p的,可以使用环境变量中的程序,没有p的,必须使用绝对路径 execv execl execlp execvp char *const ps_argv[] ={"ps", "-o", "pid,ppid,pgrp,session,tpgid,comm", NULL}; char *const ps_envp[] ={"PATH=/bin:/usr/bin", "TERM=console", NULL}; execl("/bin/ps", "ps", "-o", "pid,ppid,pgrp,session,tpgid,comm", NULL); execv("/bin/ps", ps_argv); execle("/bin/ps", "ps", "-o", "pid,ppid,pgrp,session,tpgid,comm", NULL, ps_envp); execve("/bin/ps", ps_argv, ps_envp); execlp("ps", "ps", "-o", "pid,ppid,pgrp,session,tpgid,comm", NULL); execvp("ps", ps_argv)
9.system函数
ISO C定义了system函数,但是它的操作有很强的系统依赖性。POSIX.1包含了system接口,基于ISO C定义展开来描述在一个POSIX环境里的它的行为。
#include <stdlib.h> int system(const char *cmdstring); //返回值如下。
system()会调用fork()产生子进程,由子进程来调用/bin/sh-c string来执行参数string字符串所代表的命令,此命>令执行完后随即返回原调用的进程。在调用system()期间SIGCHLD 信号会被暂时搁置,SIGINT和SIGQUIT 信号则会被忽略
因为system实现中调用fork、exec和waitpid来,所以有三种返回值的类型:
1).如果fork失败或waitpid返回一个错误而不是EINTR,system返回-1,并设置errno为指定错误。
2).如果exec失败,暗示shell不能被执行,返回值就好像shell执行了exit(127)。
3).否则,所有三个函数--fork、exec和waitpid--成功,从system的返回值是外壳的终止状态,以waitpid指定的格式。一些 system的早期实现返回一个错误(EINTR),如果waitpid被一个捕获到的信号中断。因为没有一个程序可以使用的清理策略来从这种错误类型返回,POSIX随后加上需求,system在这种情况不返回一个错误
system()自身实现 fork exec waitpid,所以不要创建子进程后再调用
10.进程时间
前面描述过三个可以测量的三种时间:挂钟时间、用户CPU时间和系统CPU时间。任一进程可以调用times函数来为它自己和任何终止的子程序来获得这些值。
#include <sys/times.h> clock_t times(struct tms *buf); //如果成功返回逝去的挂钟时间,错误返回-1 struct tms { clock_t tms_utime; /* user CPU time */ clock_t tms_stime; /* system CPU time */ clock_t tms_cutime; /* user CPU time, terminated children */ clock_t tms_cstime; /* system CPU time, terminated children */ };
nice值 0~2*nzero-1,内核自动限幅 int nzero=sysconf(_SC_NZERO); int nice(int nice) 设置的值会向现有的nice上加,设置成功则返回值和设置值相等 若返回-1,同时errno!=0,则错误 #include <error.h> #include <errno.h> if((ret=nice(20))==-1&&errno!=0) perror("nice"); printf("nice=%d\n",nzero+ret);
测量进程使用的cpu时间
#include <sys/times.h> struct tms tmsstart,tmsend; clock_t start=0,end=0; long clktck=0; if((start=times(&tmsstart))==-1) perror("time error"); system() if((end=times(&tmsend))==-1) perror("time error"); if(clktck==0){ if((clktck=sysconf(_SC_CLK_TCK))<0) perror("sysconf error"); } printf("start= %d\n",start); printf("end= %d\n",end); printf(" real: %7.8f\n",(end-start)/(double)clktck); printf(" user: %7.8f\n",(tmsend.tms_utime-tmsstart.tms_utime)/(double)clktck); printf(" sys : %7.8f\n",(tmsend.tms_stime-tmsstart.tms_stime)/(double)clktck); printf(" child user: %7.8f\n",(tmsend.tms_cutime-tmsstart.tms_cutime)/(double)clktck); printf(" child sys : %7.8f\n",(tmsend.tms_cstime-tmsstart.tms_cstime)/(double)clktck);