通过几个程序了解下进程创建及运行过程中所涉及的知识点和我对进程的一些理解。
#include<stdio.h>
2 #include<unistd.h>
3
4 int main()
5 {
6 int i=0;
7 for(;i<2;++i)
8 {
9 pid_t id=fork();
10 if(id==0)
11 {
12 printf("child,pid: %d\n",getpid());
13 }
14 else
15 {
16 printf("father,pid:%d\n",getpid());
17 }
18 return 0;
19 }
20 }
分析这段代码,我们应该可以想到此处输出六句话,实际情况确实如此
因为在这段程序中一共创建了三个进程,但输出顺序不定,我们可以给让父进程休眠2s,让子进程先运行,结果如下
file结构体和文件描述符
在Linux系统中一切皆可以看成是文件,文件又可分为:普通文件、目录文件、链接文件和设备文件。文件描述符(file descriptor)是内核为了高效管理已被打开的文件所创建的索引,其是一个非负整数(通常是小整数),用于指代被打开的文件,所有执行I/O操作的系统调用都通过文件描述符。程序刚刚启动的时候,0是标准输入,1是标准输出,2是标准错误。如果此时去打开一个新的文件,它的文件描述符会是3。
struct file结构体定义在/linux/include/linux/fs.h(Linux 2.6.11内核)中,其原
struct file {
/*
* fu_list becomes invalid after file_free is called and queued via
* fu_rcuhead for RCU freeing
*/
union {
struct list_head fu_list;
struct rcu_head fu_rcuhead;
} f_u;
struct path f_path;
#define f_dentry f_path.dentry
#define f_vfsmnt f_path.mnt
const struct file_operations *f_op;
atomic_t f_count;
unsigned int f_flags;
mode_t f_mode;
loff_t f_pos;
struct fown_struct f_owner;
unsigned int f_uid, f_gid;
struct file_ra_state f_ra;
unsigned long f_version;
#ifdef CONFIG_SECURITY
void *f_security;
#endif
/* needed for tty driver, and maybe others */
void *private_data;
#ifdef CONFIG_EPOLL
/* Used by fs/eventpoll.c to link all the hooks to this file */
struct list_head f_ep_links;
spinlock_t f_ep_lock;
#endif /* #ifdef CONFIG_EPOLL */
struct address_space *f_mapping;
};
结构体代表一个打开的文件,系统中的每个打开的文件在内核空间都有一个关联的struct file。它由内核在打开文件时创建,并传递给在文件上进行操作的任何函数。在文件的所有实例都关闭后,内核释放这个数据结构。在内核创建和驱动源码 中,struct file的指针通常被命名为file或filp。
分析下面这段程序
1 #include<stdio.h>
2 #include<unistd.h>
3 int glob=6;
4 char buf[]="a write to stdout\n";
5
6
7 int main()
8 {
9 int var;
10 pid_t pid;
11 var =88 ;
12 if(write(STDOUT_FILENO,buf,sizeof(buf)-1)!=sizeof(buf)-1)
13 {
14 perror("write error");
15 }
16 printf("before fork\n");
17 if((pid=fork())<0)
18 {
19 perror("fork error");
20 }
21 else if(pid==0)
22 {
23 glob++;
24 var++;
25 }
26 else
27 {
28 sleep(2);
29 }
30 printf("pid=%d,glob=%d,var=%d\n",getpid(),glob,var);
31 exit(0);
32 }
当输出到终端屏幕上时,结果如下
当输出到文件中,文件内容如下
一般来说fork之后父进程和⼦子进程的执行顺序是不确定的,这取决于内核的调度算法。在上面的程序中,父进程是自己休眠2秒钟,以使⼦子进程先执行。程序中fork与I/O函数之间的关系:write是不带缓冲的,因为在fork之前调⽤用write,所以其数据只写到标准输出⼀一次。标准I/O是缓冲的,如果标准输出到终端设备,则它是行缓冲,否则它是全缓冲。当以交互方式运行该程序时,只得到printf输出的行⼀一次,因为标准输出到终端缓冲区由换行符冲洗。但将标准输出重定向到⼀一个⽂文件时,由于缓冲区是全缓冲,遇到换行符不输出,当调用fork时,其printf的数据仍然在缓冲区中,该数据将被复制到子进程中,该缓冲区也被复制到⼦子进程中。于是父子进程的都有了带改⾏行内容的标准I/O缓冲区,所以每个进程终止时,会冲洗其缓冲区中的数据,得到第一个printf输出两次。
fork和文件描述符的关系:
fork的⼀一个特性是父进程的所有打开⽂文件描述符都被复制到⼦子进程中。父⼦子进程的每个相同的打开描述符共享一个文件表项。
在fork之后处理的⽂文件描述符有两种常见的情况:
1. 父进程等待⼦子进程完成。在这种情况下,父进程⽆无需对其描述符做任何处理。当子进程终⽌止后,子进程对⽂文件偏移量的修改已执⾏行的更新。
2. 父子进程各自执⾏行不同的程序段。这种情况下,在fork后,父⼦子进程各自关闭他们不需要使用的文件描述符,这样就不会⼲干扰对⽅方使用文件描述符。这种方法在网络服务进程中经常使用。
vfork()
vfork⽤用于创建一个新进程,⽽而该新进程的目的是exec一个新程序。vfork与fork都创建一个⼦子进程,但它不将父进程的地址空间复制到子进程中,因为子进程会立即调用exec,于是不会存访问该地址空间。相反,在子进程调用exec或exit之前,它在父进程的空间中运行,也就是说会更改父进程的数据段、栈和堆。vfork和fork另一区别在于:vfork保证子进程先运行,在它调用exec或(exit)之后⽗父进程才可能被调度运行。
分析下面这段代码
#include<stdio.h>
2 #include<unistd.h>
3 int glob=6;
4
5
6 int main()
7 {
8 int var;
9 pid_t pid;
10 var =88 ;
11 if((pid=vfork())<0)
12 {
13 perror("fork error");
14 }
15 else if(pid==0)
16 {
17 ++var;
18 ++glob;
19 exit(0);
20 //return 0;
21 }
22 else
23 {
26 printf("pid=%d,glob=%d,var=%d\n",getpid(),glob,var);
27 }
28 return 0;
29 }
分析此段代码,得到的结果如下
vfork⽤用于创建⼀一个新进程,⽽而该新进程的⽬目的是exec⼀一个新程序。vfork与fork都创建⼀一个⼦子进程,但它不将⽗父进程的地址空间复制到⼦子进程中,因为⼦子进程会⽴立即调⽤用exec,于是不会存访问该地址空间。相反,在⼦子进程调⽤用exec或exit之前,它在⽗父进程的空间中运⾏行,也就是说会更改⽗父进程的数据段、栈和堆。vfork和fork另⼀一区别在于:vfork保证⼦子进程先运⾏行,在它调⽤用exec或(exit)之后⽗父进程才可能被调度运⾏行。
如果在vfork后的子进程中使用return ,此时程序的结果很诡异,执行后的结果如下
原因如下:
首先了解下exit 和return
1. exit用于结束正在运行的整个程序,它将参数返回给OS,把控制权交给操作系统;而return 是退出当前函数,返回函数值,把控制权交给调用函数。
2. exit是系统调用级别,它表示一个进程的结束;而return 是语言级别的,它表示调用堆栈的返回。
3. 在main函数结束时,会隐式地调用exit函数,所以一般程序执行到main()结尾时,则结束主进程。exit将删除进程使用的内存空间,同时把错误信息返回给父进程。
4. void exit(int status); 一般status为0,表示正常退出,非0表示非正常退出。
1、exit函数和return函数的主要区别是:
1)exit用于在程序运行的过程中随时结束程序,其参数是返回给OS的。也可以这么讲:exit函数是退出应用程序,并将应用程序的一个状态返回给OS,这个状态标识了应用程序的一些运行信息。main函数结束时也会隐式地调用exit函数,exit函数运行时首先会执行由atexit()函数登记的函数,然后会做一些自身的清理工作,同时刷新所有输出流、关闭所有打开的流并且关闭通过标准I/O函数tmpfile()创建的临时文件。exit是系统调用级别的,它表示了一个进程的结束,它将删除进程使用的内存空间,同时把错误信息返回父进程。通常情况:exit(0)表示程序正常, exit(1)和exit(-1)表示程序异常退出,exit(2)表示系统找不到指定的文件。在整个程序中,只要调用exit就结束。
2)return是语言级别的,它表示了调用堆栈的返回;return是返回函数值并退出函数,通常0为正常退出,非0为非正常退出,请注意,如果是在主函数main, 自然也就结束当前进程了(也就是说,在main()里面,你可以用return n,也能够直接用exit(n)来做),如果不是在main函数中,那就是退回上一层调用。在多个进程时,如果有时要检测上个进程是否正常退出,就要用到上个进程的返回值。
正常情况下,vfork后子进程先运行,在运行结束后,子进程调用exit() 没有修改函数栈,所以,父进程得以顺利执行。
如果在子进程中return,基本就是以下的过程:
1)子进程的main() 函数 return了,于是程序的函数栈发生了变化。
2)而main()函数return后,通常会调用 exit()或相似的函数(如:_exit(),exitgroup())
3)这时,父进程收到子进程exit(),开始从vfork返回,但此时栈已经被return了(注:栈会返回一个诡异一个栈地址,对于某些内核版本的实现,直接报“栈错误”就给跪了,然而,对于某些内核版本的实现,于是有可能会再次调用main(),于是进入了一个无限循环的结果,直到vfork 调用返回 error)
用vfork()创建子进程,执行后程序一直不断地重复运行,不断创建子进程,结尾用exit(0)代替return(0)后问题就能解决
return 0在一个函数中是正常的返回过程,它会使得程序返回到函数被调用处,回复之前的执行流程,return 语句不会被执行。
这有一篇详细讲解vfork子进程ruturn的文章http://www.guokr.com/blog/806477/