进程即一个程序的动态执行。引用apue上的一句话:"A thorough understanding of the UNIX System's process control is essential for advanced programming".
一.总述
1.进程的开始
在C语言中,进程是由一个main函数开始。
int main(int argc,char *argv[])
我们可以向程序传入参数,以字符串数组的形式存储在argv,同时argc记录传入参数的个数。而且还可以在main的第三个参数中传入环境变量的数组,但一般不这样做。用全局environ更好。
2.进程中
在一个正在执行的进程中,可以fork出子进程(copy 出一个进程),也可以vfork出子进程(与新的进程share地址空间),同时父进程需要wait子进程,否则子进程就会变成僵尸进程,无家可归。我们还可以用exec function进行替换程序(但进程ID不变)。在一个进程中,system()可以执行一个shell命令,但要注意system对set-user-ID会造成权限的传递,这一点在后面会详细叙述。
3.进程结束
进程的结束有很多种方式,大致可以分为正常结束与异常结束。具体的我就不赘述了。主要解释exit,_exit,_Exit的区别。_exit,_Exit在结束的时候不会fllush缓冲,而exit在结束时会fllush缓冲,而且在某些UNIX系统中,exit还会关闭标准I/O流,以及与标准I/O流相关联的STDOUT_FILENO与STDIN_FILENO文件描述符。在这样的情况下,只需要用dup先复制文件描述符就可以了。
二.system call 以及 一些知识点
我喜欢用函数来总结知识点。
1.atexit()。可以使用atexit()来注册若干个exit handler,参数是函数的地址。当进程结束时,就会执行每一个注册了的exit handler,即执行函数。但要注意,这个执行顺序是栈的顺序,也就是"先注册,后执行"。比如说,当我做完一件事(即exit时),我先注册的A执行funcA_1,再注册的B执行funcB_2,最后注册的A执行funcA_3。但最后的执行顺序却是反的。程序如下:
1 #include <apue.h> 2 3 static void funcA_1() 4 { 5 printf("funcA_1\n"); 6 } 7 static void funcB_2() 8 { 9 printf("funcB_2\n"); 10 } 11 static void funcA_3() 12 { 13 printf("funcA_3\n"); 14 } 15 16 int main() 17 { 18 if(atexit(funcA_1)!=0) 19 err_sys("I can't register funcA_1"); 20 if(atexit(funcB_2)!=0) 21 err_sys("I can't register funcB_2"); 22 if(atexit(funcA_3)!=0) 23 err_sys("I can't register funcA_3"); 24 exit(0); 25 }
执行结果如下:
funcA_3
funcB_2
funcA_1
|
2.一个C程序在内存中主要分为4个部分:
a.初始化了的数据区域。比如int a=1;且a是在函数之外定义的,那么a就是存储在这里的。这里的a即是C语言中的静态全局变量。
b.非初始化数据区域。比如int b;且b是定义在所有函数之外,那么b就存储在此,即未初始化的静态全局变量,会被初始化为0或者null.
c.栈。存储自动变量,也就是在函数之内声明的变量。还有call stack,很熟悉的东西。
d.堆。动态内存分配。malloc尽职尽责。
3.每个进程都有一个唯一的ID。getpid()可以得到本进程的process ID,getppid()可以得到父进程的ID,但无法得到子进程的ID,因为一个进程的父进程是唯一的,但可以有多个子进程。Process ID 0是系统进程的ID,process ID 1是init进程的ID,当一个父进程先于子进程结束时,子进程就会被init进程所继承,那么该子进程的父进程就变成了init.这一点会在后面与僵尸进程作比较。
4.fork().这是进程控制中很核心的system call. fork产生一个新的进程,这个进程除了代码与父进程共享(代码段在执行阶段是不会发生变化的)外,其他的都是父进程的一份copy(包括分配的内存,等等),除了继承关系外,与父进程就没什么关系了。而vfork是产生一个与父进程共享一切的子进程。
5.wait,waitpid.在子进程exit后,需要wait才能使得子进程不变成僵尸进程。如果子进程exit后,父进程没有在wait,那么这个子进程就会变成一个僵尸进程。而子进程被init所继承的原因是其父进程提前结束,即使这个父进程没有wait也不会造成僵尸进程,因为它已经提前结束了而子进程还没有exit,所以子进程会被init所继承了。由此也可说明init进程是在wait它的所有子进程的。下面是一个形成僵尸进程的程序:
1 #include <apue.h> 2 3 int main() 4 { 5 pid_t pid; 6 if( (pid=fork())<0 ) 7 err_sys("error fork"); 8 else if(pid==0) //child 9 exit(1); 10 11 sleep(2); 12 if(system("ps -o pid,ppid,state,tty,command")<0) 13 err_sys("system() error"); 14 15 exit(0); 16 }
以下是结果:
3435 3434 Z pts/0 [test] <defunct>
|
这里的Z就表示这是一个僵尸进程了。
6.race condition.当fork后生成子进程后,我们并不知道是子进程先执行还是父进程先执行,这时就会产生“race”。而vfork会确保子进程先于父进程执行。
7.exec function.这是一组函数。exec有些像替换程序的意思。当执行exec后,当前进程的代码就被exec所要执行的程序代码所替换,换句话说,就是当前进程在exec之后的语句都无效了。
8.system(). system("data>file"),即执行一个shell命令。我很喜欢这个函数,我觉得这个函数可以很轻易就实现与shell的简单交互,在某些地方会非常有用。但要注意的是,system可能会引起一个权限的传递。比如,program1有在root权限下设置的set-user-ID,这就意味着任何用户在执行program1时都会具有superuser permission.那么,如果在program1中调用system("program2"),就会将program1的superuser permission传递给program2,而这显然是我们不愿意看到的。
三.我看书时的一些笔记与想法(有些摘自apue)
1. Three functions terminate a program normally:_exit and _Exit,which return to the kernel immediately,and exit,which performs certain cleanup processing and then returns to the kernel.
2.Returning an integer value from the main function is equivalent to calling exit with the same value.Thus exit(0) is the same as return (0) from the main function.
3.Note that the only way a program is executed by the kernel is when one of the exec functions is called.
4.Stack,where automatic variables are stored,along with information that is saved each time a function is called.
5.We can affect the environment of only the current process and any child processed that we invoke.We cannot affect the environment of the parent process,which is often a shell.
6.*Process ID 0* is usually the scheduler process and is often known as the swapper.No program on disk corresponds to this process,which is part of the kernel and is known as a *system process*.
7.*Process ID 1* is usually the *init* process and is invoked by the kernel at the end of the bootstrap procedure. It is a normal user process,not a system process within the kernel ,like the swapper,although it does run with superuser privileges.
8.fork() function is called once but returns twice.
9.*For example,the child process gets a copy of the parent's data space,heap,and stack.Note that this is a copy for the child share the text segment.
10.注意,如果char buf[100],这是一个固定大小的数组,那么sizeof()就会返回其字节数100; 如果:char *str=(char *)malloc(100*sizeof(char));那么sizeof(str)就是返回指针的大小。而strlen是返回当前不为'\0'的字节数。sizeof(buf)得到的字节数是要包括'\0'的 .strlen不会包括'\0'.
11.The standard output is line buffered if it's connected to a terminal device;otherwise,it's fully buffered.
12.Indeed,one characteristic of fork is that all file descriptors that are open in the parent are duplicated in the child.
13.*file table 中包含了current file offset,所以,共享一个file table就相当于共享current file offset.这很重要*
14.Regardless of how a process terminates,the same code in the kernel is eventually executed.This kernel code closes all the open descriptors for the process,releases the memory that it was using,and the like.
15.在进程异常终止后,是内核,而不是进程本身来生成 结束状态 。
16.当一个进程结束的时候,不管是正常结束还是异常终止,内核都会通知它的父进程(发送SIGCHLD信号给父进程)。
17.wait,waitpid的一个作用就是父进程用来等待子进程,防止子进程变成僵尸进程。僵尸进程,就是在一个子进程结束(*也就是exit()。一定要注意是exit()后再去找wait()*)的时候,没有父进程在wait,那么这个进程就变成了僵尸进程。
18.With wait(),the only real error is if the calling process has no children.(Another error return is possible,in case the function call is interrupted by a signal)。
19.With fork,we can create new processes;and with the exec functions,we can initiate new programs.
20.ARG_MAX is the total size of the argument list and the environment list.This value must be at least 4096 bytes on a POSIX.1 system.
21.file descriptors的close-on-exec flag的作用是什么? Every open file descriptor in a process has a close-on-exec.If this flag is set ,the descriptor is closed across an exec() function.Otherwise,the descriptor is left open across the exec function.
22.Only a superuser process can change the real user ID.
23.使用exec function后,该程序所在的进程就被exec所指定的程序给替换了,原程序中exec function后面的语句就不会执行了。如果那些语句还执行了,就说明exec function的执行出现了错误。
24.在exec function中可以使用./ 当前路径 比如:execl("./hello"......)。
25.注意 execl("./hello",arg0,arg1,...,(char *)0);这里的arg0是该命令行的第0个参数。一定要注意,这里很容易将arg0当成该命令行的第1个参数,因为在执行命令行的时候是把程序名称当作第0个参数的。这里很容易出错. 我觉得在使用execl的时候最好将arg0赋一个不需要的值然后弃用arg0,从arg1开始用,这样与平常使用命令行的思路才一样,更不容易出错。 我觉得这与a[100],从a[1]开始使用是同样的思想。当然这也不是必需的。比如./hello是一个shell,那么就需要传入类似"sh"这样的arg0参数。视情况而定。
26.What happens if we call system() from a set-user-ID program? Doing so is a security hole and should never be done.The system() function should never be used from a set-user-ID or a set-group-ID program.
27.A new process record is *initialized* by the kernel for the child after a fork(),not when a new program is executed.Each accounting record is written when the process *terminates* .
28.scanf,printf,在标准输入输出流被关闭的情况下会返回-1。
29.uid=0 即root
参考资料:apue
如果您觉得我的文章对您有帮助,请顶一下,非常感谢!