APUE学习进程.docx
《APUE学习进程.docx》由会员分享,可在线阅读,更多相关《APUE学习进程.docx(13页珍藏版)》请在冰豆网上搜索。
![APUE学习进程.docx](https://file1.bdocx.com/fileroot1/2023-1/25/ef699202-0f6d-48f3-9614-7a70dc9217f8/ef699202-0f6d-48f3-9614-7a70dc9217f81.gif)
APUE学习进程
APUE学习--进程
(1)
分类:
Unix环境编程2013-05-2311:
41 36人阅读 评论(0) 收藏 编辑 删除
编程APUELinux进程
先来区分下面这些概念:
程序、进程、线程、任务
程序。
首先是一个文件,这个文件是可以被执行的;有时我们也将一段执行代码称为程序
进程。
要理解进程,先来说下进程的四要素:
1.“剧本”,一段执行代码供其执行,这段执行代码可以是独有的也可以是与其他进程共用的(fork后的父子进程)
2.该进程专用的内核堆栈空间3.在内核中存在一个task_struct结构,这个就是我们常说的PCB(进程控制块),内核根据这个PCB对进程进行调度执行
4.拥有独立的内存空间。
四要素全部具备的才能成为进程。
进程与程序概念上的区别可以用一句话来阐述:
进程是程序的动态执行的形式。
想要更好的了解进程,可以参照PCB的定义,在内核代码中include/linux/sched.h中structtask_struct;
线程。
我们一般称线程是轻量级的进程(二者之间没有明显的分界线)。
如果一个进程只具备四要素中的前三个那么就是线程了,也就说是线程并非拥有独立的内存空间。
进程和线程的区别可以这样阐述:
进程是系统分配(内存)资源的最小单位,而线程是OS调度的最小单位。
任务。
是一个比较抽象的概念,可以是进程也可以是线程,用来描述为了达成某种目的的一系列操作。
————————————————————————————————————————————————
进程的开始始于main()函数(通过shell执行一个新进程)、fork()创建(vforkclonesystempopen等)。
进程终止可以是从main()函数返回、调用exit()、调用_exit()、被信号终止等。
exit()与_exit():
都可以使进程立即终止,但是exit()在调用时先进行一些处理再进入内核,而_exit()直接进入内核。
处理包括:
关闭打开的文件(stdout会刷新缓冲区)、执行注册的终止处理函数。
注册终止处理函数使用atexit(),可注册的处理函数必须是无参无返回值的,注册后在主函数退出(或exit())时进程终止前执行,并且执行的顺序按注册顺序的倒序执行(注册多个)。
最多可注册32个。
进程的运行环境使用环境变量来描述,环境变量相当于进程中的一些全局变量,这些全局变量描述了进程运行时所处的环境,这些环境变量使用一个"x=value"这样的字符串来存储,所有的环境变量构成一张环境表,这张表是一个数组,数组中的成员是字符串,这样就有了环境表的定义char**environ;如果源文件中向访问环境表,可以使用externchar**environ;声明,然后通过遍历这个字符串数组访问所有的环境变量,遍历的终止条件可以是environ[i]==NULL
当然,linux系统并不鼓励这样去访问或修改环境变量,系统提供给我们一系列函数可以获取或设置环境变量:
[cpp] viewplaincopy
1.char *getenv(const char *name);
2.int putenv(char *string);
3.int setenv(const char *name, const char *value, int overwrite);
4.int unsetenv(const char *name);
getvenv()根据环境变量获得该环境变量的字符串;putenv()可以设置一个环境变量,要求string的形式为"x=value";setenv()也可以设置全局变量,若不存在则增加,存在且overwrite非0则可修改;unsetenv()用来删除一个环境变量
除了环境变量以外,进程中使用的资源有一些限制,可以通过下面的函数获得/设置:
[cpp] viewplaincopy
1.int getrlimit(int resource, struct rlimit *rlim);
2.int setrlimit(int resource, const struct rlimit *rlim);
3.struct rlimit {
4. rlim_t rlim_cur; /* Soft limit */
5. rlim_t rlim_max; /* Hard limit (ceiling for rlim_cur) */
6.};
每一种资源的限制,有硬限制和软限制之分,软限制是OS设置的一个保护性的限制,资源的宏定义如下:
RLIMIT_AS 进程可用内存最大字节数
RLIMIT_CORE core文件的最大字节数,若其值为0则阻止创建core文件
RLIMIT_CPU CPU时间的最大量值(秒),当超过此软限制时,向该进程发送SIGXCPU信号
RLIMIT_DATA 一个进程的数据段最大字节长度。
数据段中初始化数据、非初始化数据以及堆的总和
RLIMIT_FSIZE 可以创建的文件的最大字节长度。
当超过此软限制时,则向该进程发送SIGXFSZ信号
RLIMIT_NOFILE 每个进程能打开的最多文件数
RLIMIT_STACK 栈的最大字节长度。
系统不会动态增加栈的大小限制
RLIMIT_VMEM 可映照地址空间的最大字节长度
在编译一个程序时可以链接一个动态库,使用参数-l。
在程序运行中也可以动态的加载动态库,多用于动态加载模块,这样做不需要重新编译。
相关的函数如下:
[cpp] viewplaincopy
1.void *dlopen(const char *filename, int flag);
2.char *dlerror(void);
3.void *dlsym(void *handle, const char *symbol);
4.int dlclose(void *handle);
这些函数使用时需要-ldl参数。
dlopen()打开filename指定的动态库,flag中指定了一些选项(RTLD_LAZYRTLD_NOW),返回一个访问这个动态库的句柄。
dlsym()用来获得hanle指定的动态库中的一个全局变量或者函数的地址。
dlclose()用来卸载被装载的动态库。
dlerror()可以获得这些函数出错时的错误描述字符串。
这套函数用来为项目增加功能模块时经常和glob()配合使用来加载某个路径下的所有动态库。
可参考下面的例子(计算器)
[cpp] viewplaincopy
1.#define PLUGINDIR "./plugins/*.op"
2.#define OPMAX 10
3.typedef int op_func_t(int, int);
4.struct op_st {
5. op_func_t *func;
6. int sym;
7. char *desc;
8. void *handle;
9.};
10.static int op_nr = 0;
11.static struct op_st op[OPMAX];
12.
13.
14.#define OPFUNC(x) (op[x].func)
15.#define OPSYM(x) (op[x].sym)
16.#define OPDESC(x) (op[x].desc)
17.
18.
19.int main()
20.{
21. int i, a, b, ret;
22. glob_t globres;
23. void *handle;
24. char * errstr;
25.
26. ret = glob(PLUGINDIR, GLOB_NOSORT, NULL, &globres);
27. if ( ret !
= 0 ) {
28. perror("glob()");
29. exit
(1);
30. }
31.
32. for ( i=0; i33. op[op_nr].handle = dlopen(globres.gl_pathv[i], RTLD_LAZY);
34. if ( op[op_nr].handle == NULL ) {
35. perror("dlopen()");
36. continue;
37. }
38.
39. OPFUNC(op_nr) = dlsym(op[op_nr].handle, "op_func");
40. if ( errstr = dlerror() ) {
41. fprintf(stderr, "load %s fail...\n", globres.gl_pathv[i], errstr);
42. continue;
43. }
44. OPSYM(op_nr) = *(int*)dlsym(op[op_nr].handle, "op_sym");
45. if ( errstr = dlerror() ) {
46. fprintf(stderr, "load %s fail...\n", globres.gl_pathv[i], errstr);
47. continue;
48. }
49.
50. OPDESC(op_nr) = *(char **)dlsym(op[op_nr].handle, "op_desc");
51. if ( errstr = dlerror() ) {
52. fprintf(stderr, "load %s fail...\n", globres.gl_pathv[i], errstr);
53. continue;
54. }
55. fprintf(stderr, "load %s success:
\n", globres.gl_pathv[i], errstr);
56. fprintf(stderr, "module's func:
%p sym:
%c desc:
%s\n",
57. OPFUNC(op_nr), OPSYM(op_nr), OPDESC(op_nr));
58. op_nr++;
59. }
60.
61. globfree(&globres);
62.
63. printf("input two number:
");
64. scanf("%d %d", &a, &b);
65.
66. for ( i=0; i67. printf("%d%c%d=%d\n", a, OPSYM(i), b, OPFUNC(i)(a, b));
68. }
69.
70. for ( i=0; i71. dlclose(op[i].handle);
72. }
73.
74. return 0;
75.}
最后在来说下goto的问题,goto的作用是在跳转,一般都用于函数内的错误处理,也就是说goto不能跨函数跳转,因为goto没有对上下文环境进行处理。
linux为我们提供了可以跨函数跳转的函数:
[cpp] viewplaincopy
1.int setjmp(jmp_buf env);
2.int sigsetjmp(sigjmp_buf env, int savesigs);//这个是可移植的
3.void longjmp(jmp_buf env, int val);
4.void siglongjmp(sigjmp_buf env, int val);//可移植的
setjmp()longjmp()这两个函数可以实现的原因是,setjmp()保存了当前位置的上下文环境,longjmp()取出保存的上下文环境实现跳转。
setjmp()的功能是设置跳转点,设置时返回0,当由longjmp()跳转时,setjmp()返回longjmp()的第二个参数。
带前缀的两个函数增加的功能是:
sigsetjmp()当savesigs非0时保存信号屏蔽字,siglongjmp()将恢复信号屏蔽字。
APUE学习--进程
(2)
分类:
Unix环境编程2013-05-2315:
09 26人阅读 评论(0) 收藏 编辑 删除
APUE编程进程Linux
这篇文章,我们围绕着fork()wait()exec()。
fork()的功能就是创建子进程,这个函数最大的特点并不像线程创建函数和clone()那样让被让创建的子进程从某个指定的函数开始执行,而是父子进程共享接下来的执行代码。
fork()函数执行一次,返回两次,一次是父进程返回(返回值为子进程pid),一次是子进程返回(返回值为0)。
大家都知道Linux中pid最小的进程是init进程(是OS的第一个进程),init进程的pid为1,所以不会出现父进程返回为0的情况;并且一个父亲可以由多个子进程(所以pid需要返回),而子进程只有一个父亲(返回0就可以了getppid()获取父进程pid)。
上面一段阐述了进程四大要素中第一个要素“剧本”,第二个要素和第三个要素是内核中处理,系统编程中无须做过多的了解,我们在来看第四个要素“独立的内存空间”。
每个进程都有独立的内存空间,子进程也有独立的内容空间,子进程的内存空间是在fork()时将父进程的内存空间拷贝一份过来。
注意这里父子进程的内存空间在刚刚fork()之后是一样的,但父子进程并非共享这段内存,而是分别独占,之后不再互相影响。
既然不再影响,父子进程中一个结束也不会影响另一个。
但有一个问题需要考虑一下,子进程的ppid是父进程的pid,那么父进程先结束了(pid也就不存在了),子进程的ppid又该是多少?
这里涉及到一个孤儿进程的概念,当父进程退出后,所有未退出的子进程都成为孤儿进程,孤儿进程被init进程收养,也就是说父进程退出的子进程的ppid为1。
如果子进程先退出了父进程未退出,由会有什么情况?
结合psaux命令观察,这样的子进程仍然存在且进程状态为Z,Z是zombie的缩写(PlantsvsZombie应该都玩过)即僵尸的意思(或者将死),这种进程虽然自己已经退出了,但还会残留尸体在OS中,一直到父进程收尸才真正结束。
子进程退出后,父进程不是默认收尸的,父进程可以通过调用wait()族函数进行收尸或者等父进程退出后由init进程为其自动收尸(init进程能做到及时收尸)。
父子进程的关系比较简单,但需要注意的是父子进程对fork()之前父进程打开的文件拥有什么样共享或独占的特性。
之前文件部分提到过内核会为每个进程维护一张打开文件的表,在fork()后,子进程会拷贝一份父进程的这张表,也就是说子进程完全继承父进程所打开的文件描述符,包括父进程对标准输入输出进程重定向的情况,并且标准输出的缓冲区中的数据也会被继承,但在fork()之后父子进程中的一个进程重定向了标准输入输出不会影响另一个进程。
文件操作还有一个非常重要的概念就是文件的读写位置,父子进程对fork()前打开的文件的读写位置是否是共享?
简单写一个程序就可以看出来,读写位置父子进程是共享的,原因也不难理解,和重定向一个文件描述符一样,虽然是两个描述符但读写位置并不在文件表的表项中所以读写位置是相同的。
经常会在考试中提到的一个函数是vfork(),说实在的很少有程序员在实际编程中使用vfork(),但一定要知道vfork()和fork()的区别:
1.vfork()的子进程并不复制一份父进程的空间,在子进程结束前或者调用exec()族函数前子进程使用的是父进程的空间;2.vfork()执行后会使父进程阻塞至子进程退出或者执行exec()族函数。
也就是说vfork()这个函数式专门为exec()族函数设计的。
之前说到僵尸进程的概念,收尸操作使用wait()组族函数进行。
[cpp] viewplaincopy
1.pid_t wait(int *status);
2.pid_t waitpid(pid_t pid, int *status,int options);
3.int waitid(idtype_t idtype, id_t id, siginfo_t *infop, int options);
4.pid_t wait3(int *status,int options, struct rusage *rusage);
5.pid_t wait4(pid_t pid, int *status, int options, struct rusage *rusage);
这些函数中,wait()和waitpid()是常用的,wait()族函数会阻塞进程至收尸成功。
这两个函数的返回值是被收尸的子进程的pid,参数status是一个出参,带出来的是子进程退出状态。
注意进程的退出状态并不是进程的退出值,退出状态包括退出值和退出原因,如何解析status这个退出状态在下面介绍。
waitpid()也可以根据参数pid来指定收哪个子进程,当pid>0时指定收进程id为pid的子进程、当pid==0指定收进程组id为当前进程组id的任意的子进程、当pid==-1时指定收任意子进程、当pid<-1时收指定进程组id为|pid|的任意子进程。
waitpid()最后一个参数可以指定一些特殊操作,WNOHANG选项提供了waitpid()的非阻塞版本(即没有子进程可以收尸也会立即返回),如果收尸成功返回子进程pid,否则返回0;__WCLONE可以收由clone()创建的子进程。
watid()和waitpid类似,只是将waitpid()中pid的参数用了idtypeid两个参数代替,并能带回导致waitid接触阻塞的信号的信息。
wait3()和wait4()比之前几个的功能要多一个,要求内核返回由终止进程极其子进程使用的资源的汇总放到参数rusage保存的地址中。
我们来详细说下如何获得status带回的退出状态。
首先是WIFEXITED(status)WIFSIGNALED(status)这两个宏,分别用于测试子进程是正常退出还是由于信号导致的退出。
参照头文件
[cpp] viewplaincopy
1.// /usr/include/sys/wait.h
2.# define WIFEXITED(status) __WIFEXITED(__WAIT_INT(status))
3.# define WIFSIGNALED(status) __WIFSIGNALED(__WAIT_INT(status))
4.// /usr/include/bits/waitstatus.h中:
5.#define __WIFEXITED(status) (__WTERMSIG(status) == 0)
6.#define __WTERMSIG(status) ((status) & 0x7f)
7.#define __WIFSIGNALED(status) (((signed char) (((status) & 0x7f) + 1) >> 1) > 0)
WIFEXITED()取出status的最低7位,如果是0就是成长退出 WIFSIGNALED()测试1~7为不全为0时由于信号退出
正常退出对应着取出返回值,信号退出对应着是由哪个信号导致退出,对应的宏分别为WEXISTSTATUS(status)WTERMSIG(status)
[cpp] viewplaincopy
1.// /usr/include/sys/wait.h中:
2.# define WEXITSTATUS(status) __WEXITSTATUS(__WAIT_INT(status))
3.# define WTERMSIG(status) __WTERMSIG(__WAIT_INT(status))
4.// /usr/include/bits/waitstatus.h中:
5.#define __WEXITSTATUS(status) (((status) & 0xff00) >> 8)
6.#define __WTERMSIG(status) ((status) & 0x7f)
WEXITSTATUS()取出status的9~16位,所以进程的退出值为8位整数;WTERMSIG()取出的就是最低7位,也就是说进程是由于信号退出的话,最低7位存储信号编号,如果全为0就是非信号退出否则就是信号退出。
最后我们来说下exec族函数,这些函数完成的功能是让当前的进程去执行另一个程序(当前进程被被替换)。
其中execve()是系统调用,其余为库提供的多种版本。
[cpp] viewplaincopy
1.int execve(const char *filename, char *const argv[], char *const envp[]);
2.int execl(const char *pathname, const char *arg0, ..., /* (char *)0 */);
3.int execlp(const char *filename, const char *arg, ..., /* (char *)0 */);
4.int execle(const char *pathname, const char *arg0, ..., /* (char *)0, char *const envp[] */);
5.int execv(const char *pathname,