首页 > 代码库 > [linux]进程(四)——进程的创建

[linux]进程(四)——进程的创建

11,进程的创建

linux的进程创建可以分为两个步骤,分别为fork()和exec()函数,fork()负责创建一个子进程,和父进程的差别仅仅是PID PPID以及一些统计量,exec()函数负责读取可执行文件载入地址空间运行。


fork()函数原型

pid_t fork(void); 子进程返回0,父进程返回子进程的PID,fork()函数一次创建两次返回。


fork()函数的实现
fork()采用了写时拷贝(copy-on-write)的技术,刚创建子进程的时候父进程和子进程拥有相同的地址空间(这些区域被设定为只读),只有在子进程或者父进程要写入数据的时候,父进程相关的地址空间才会被拷贝,父子进程指向相同的物理内存。
fork()之后是父进程先执行还是子进程先执行是不确定的,通过fork()函数创建的父子进程是共享一个文件表项的,linux通过clone()系统调用实现fork()函数,
另外一个创建子进程的方法是调用vfork()函数,vfork()和fork()函数的区别是vfork()函数不拷贝父进程的页表,子进程在父进程的地址空间先运行,直到子进程退出后,父进程才可以继续运行,另外vfork()函数有一个隐患就是一旦子进程调用exec()函数执行失败。。。。。

fork()函数示例:

[cpp] view plaincopy在CODE上查看代码片派生到我的代码片
 
  1. #include <stdio.h>  
  2. #include <signal.h>  
  3. #include <stdlib.h>  
  4. #include <string.h>  
  5. #include <unistd.h>  
  6. #include <errno.h>  
  7. int glob = 5;  
  8. char buf[] ="a write to stdout\n";  
  9. int main(void)  
  10. {  
  11.     int var;  
  12.     pid_t pid;  
  13.     var = 88;  
  14.     if(write(STDOUT_FILENO,buf,(sizeof(buf)-1)) != (sizeof(buf)-1))  
  15.         printf("write error\n");  
  16.     printf("before fork\n");  
  17.     if((pid = fork()) < 0)  
  18.     {     
  19.         printf("fork error\n");  
  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. }  
  33. 输出如下:  
  34. ./a.out  
  35. a write to stdout  
  36. before fork  
  37. pid = 430,glob = 7, var = 89    
  38. pid = 429,glob = 6, var = 88  

 

[cpp] view plaincopy在CODE上查看代码片派生到我的代码片
 
  1. vfork()示例程序如下:  
  2. #include <stdio.h>  
  3. #include <signal.h>  
  4. #include <stdlib.h>  
  5. #include <string.h>  
  6. #include <unistd.h>  
  7. #include <errno.h>  
  8. int glob = 5;  
  9. char buf[] ="a write to stdout\n";  
  10. int main(void)  
  11. {  
  12.     int var;  
  13.     pid_t pid;  
  14.     var = 88;  
  15.     if(write(STDOUT_FILENO,buf,(sizeof(buf)-1)) != (sizeof(buf)-1))  
  16.         printf("write error\n");  
  17.     printf("before fork\n");  
  18.     if((pid = vfork()) < 0)  
  19.     {     
  20.         printf("fork error\n");  
  21.     }  
  22.     else if (pid == 0)  
  23.     {  
  24.         glob++;  
  25.         var++;  
  26.     //  exit(0);  
  27.     }  
  28.     else  
  29.     {  
  30.         sleep(2);  
  31.     }  
  32.     printf("pid = %d,glob = %d, var = %d\n",getpid(),glob,var);  
  33.     exit(0);  
  34. }  
  35. 函数输出如下:  
  36. ./a.out  
  37. before fork  
  38. pid = 29039,glob = 7,var =89  
  39. 子进程对变量做了加1操作结果改变了父进程中变量的值,因为子进程与父进程的地址空间相同,  

 

父子进程通信的实例程序如下:

 

[cpp] view plaincopy在CODE上查看代码片派生到我的代码片
 
  1. #define RECOVERY_API_VERSION 2.3.1  
  2. const char* binary = "/tmp/update_binary";  
  3. int pipefd[2];  
  4. pipe [pipefd];  
  5. const char** args = (const char**)malloc(sizeof(char*)*5);  
  6. args[0]=binary;  
  7. args[1]=RECOVERY_API_VERSION;  
  8. char *temp = (char*)malloc(10);  
  9. sprintf(temp,"%d",pipefd[1]);  
  10. args[2]=temp;  
  11. args[3]=(char*)path;  
  12. args[4]=NULL;  
  13. pid_t pid = fork();  
  14. if(pid == 0)  
  15. {  
  16.     close(pipefd[1]);  
  17.     execv(binary,(char*const*)args));  
  18.     printf(exec child process error);  
  19.     _exit(-1);  
  20. }  
  21. close(pipefd[1]);  
  22. char buffer[1024];  
  23. FILE* from_child = fdopen(pipefd[0],"r");  
  24. while(fgets(buffer,sizeof(buffer),from_child)!=NULL)  
  25.     char*command = strtok(buffer,"\n");  
  26. waitpid(pid,&status,0)  


fork()函数系统调用

fork()函数系统调用的入口点是sys_fork()函数,最终是调用内核的do_fork()函数(该函数是与体系结构无关的函数)

 

[cpp] view plaincopy在CODE上查看代码片派生到我的代码片
 
  1. asmlinkage int sysfork()  
  2. {  
  3.     return do_fork(SIGCHLD,regs.regs->ARM_sp,®s,NULL,NULL)  
  4. }  

 

由以上代码可以知道,子进程结束后会发送SIGCHLD信号通知父进程。

do_fork()函数主要调用了copy_process()函数,copy_process()函数返回一个新的task_struct结构体,copy_process()函数会调用dup_task_struct()函数,此时创建的子

进程和原本的父进程只有一个参数不一样,即task_struct->stack,stack通常和thread_info一起保存在一个联合中,

 

进程环境:
以C语言的main()函数为例:main()函数的原型如下:
int main(int argc,int *argv[]);
C程序的存储空间布局
正文段 BSS段 初始化数据段 栈 堆

 

补充:exec族函数

[cpp] view plaincopy在CODE上查看代码片派生到我的代码片
 
  1. extern char **environ;  
  2. int execl(const char *path, const char *arg, ...);  
  3. int execlp(const char *file, const char *arg, ...);  
  4. int execle(const char *path, const char *arg, ..., char * const envp[]);  
  5. int execv(const char *path, char *const argv[]);  
  6. int execvp(const char *file, char *const argv[]);  
  7. int execve(const char *filename, char *const argv[], char *const envp[]);  
  8. 示例程序如下:  
  9. 例子  
  10. #include <stdio.h>  
  11. #include <unistd.h>  
  12. int main()  
  13. {  
  14.     execl("/bin/ls","ls","-l",NULL);  
  15.    
  16.     printf("如果execl执行失败,这个就会打印出来了\n");  
  17.     return 1;  
  18. }  



12,内核线程
 12,内核线程
内核经常需要在后台执行一些操作,内核线程,它是独立运行在内核空间的标准进程,内核线程和普通的进程间的区别在于内核线程没有独立的地址空间(实际上它的mm指针被设置为NULL)。它们只在内核空间运行,从来不切换到用户空间去,内核进程和普通进程一样,可以被调度,也可以被抢占,Linux确实会将一些任务交给内核线程去做,像pdflush和ksoftirqd这些任务就是明显的例子,内核线程也只能由其他的内核线程创建,一般情况下,内核线程会将它在创建时得到的函数永远的执行下去,除非系统重启。

内核线程的创建:
pid_t kernel_thread(int (*fn)(void *), void *arg, unsigned long flags)
从现有的内核线程创建一个新的内核线程的方法如下:
struct task_struct *kthread_creat(int (*threadfn)(void *data),void *data,const char namefmt[]....);
新的线程将运行threadfn函数,给其传递的参数为data,线程名为namefmt,
新创建的线程处于不可运行的状态,要通过wake_up_process()或者以下函数使其运行
struct task_struct *kthread_run(int (*threadfn)(void *data),void *data,const char namefmt[]....)

内核线程的退出:
内核线程启动后一直运行直到调用do_exit()函数,或者调用如下函数int kthread_stop(struct task_struct *k),传递给kthread_stop的参数是kthread_creat()函数返回的task_struct的地址


实例:
创建线程

 

[cpp] view plaincopy在CODE上查看代码片派生到我的代码片
 
  1. struct task_struct *thread_task;  
  2. int rc;  
  3. thread_task=kthread_create(fsg_main_thread,common,"file-storage");  
  4. if(IS_ERR(thread_task))  
  5. {  
  6.     rc = PTR_ERR(thread_task);  
  7.     return ERR_PTR(rc);  
  8. }  
  9. wake_up_process(thread_task);  

 

13,进程退出

通过正常的进程结束、通过信号或是通过对exit函数的调用,不管进程如何退出,进程的结束都要借助对内核函数do_exit(在alps/kernel/kernel/exit.c内)的调用,进程的资源回收和进程描述符的回收是分开的,进程描述符是在其父进程的wait()函数返回后收回~

 

补充:UNIX环境进程异常退出的情况
 以下是参考以下网址的博客:http://www.ibm.com/developerworks/cn/aix/library/1206_xiejd_unixexception/
进程异常大致可以分为两类:
一:向进程发送信号导致进程异常退出
二:代码本身错误导致进程退出
第一类:向进程发送信号导致进程退出:
(1)向进程发送信号导致进程退出,进程收到信号后可能导致进程退出并产生coredump文件,在 UNIX环境中有三种方式将信号发送给目标进程,导致进程异常退出。
 方式一:调用函数kill()发送信号,原型是 int kill(pid_t pid,int sig)
  以下代码为示例代码:

[cpp] view plaincopy在CODE上查看代码片派生到我的代码片
 
  1.     1 #include <sys/types.h>   
  2.  2 #include <signal.h>   
  3.  3   
  4.  4 int main(int argc, char* argv[])   
  5.  5 {   
  6.  6     char* pid = argv[1];   
  7.  7     int PID = atoi(pid);   
  8.  8   
  9.  9     kill(PID, SIGSEGV);   
  10. 10     return 0;   
  11. 11 }  


 方式二:运行kill命令发送信号
  格式为 kill SIGXXX PID
 方式三:在终端使用键盘发送信号
  使用 control-C 发送 SIGINT 信号,使用 control-\ 发送 SIGQUIT 信号,使用 control-z 发送 SIGTSTP 信号,如何防止这类信号到来导致信号退出?通过调用 signal 函数绑定信号处理程序来应对信号的到来 void (*signal(int sig, void (*func)(int)))(int);插入下面的代码,以达到屏蔽信号 SIGINT 的效果  (void)signal(SIGINT, SIG_IGN);
第二类:编程错误导致进程异常退出
 当进程执行非法操作时,计算机会抛出处理器异常,异常是指 soft interrupts,是进程非法操作所导致的处理器异常, 这类异常是进程执行非法操作所产生的同步异常,比如内存保护异常,除 0 异常,缺页异常等等。系统为每个异常都分配了异常处理函数,当有相应的异常的时候就会去调用相应的异常处理函数。
 实例分析:
 (1)内存非法访问实例

[cpp] view plaincopy在CODE上查看代码片派生到我的代码片
 
  1.    1 #include<stdio.h>   
  2. int main()   
  3. 3 {   
  4. 4      char* str = "hello";   
  5. 5      str[0] = ‘H‘;   
  6. 6      return 0;   
  7. 7 }   


 (2)除0异常

[cpp] view plaincopy在CODE上查看代码片派生到我的代码片
 
  1. 1 #include <stdio.h>   
  2.  2   
  3.  3 int main()   
  4.  4 {   
  5.  5     int a = 1, b = 0, c;   
  6.  6     printf( "Start running\n" );   
  7.  7     c = a/b ;   
  8.  8     printf( "About to quit\n" );   
  9.  9 }   


对于进程异常的问题应该如何调试
 (1)利用coredump文件分析异常退出的原因

[linux]进程(四)——进程的创建