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

11,进程的创建

从操作系统角度看进程的创建:

从操作系统层面看进程的创建主要分为三个步骤:
1,创建一个独立的虚拟地址空间,虚拟空间由一组页映射函数将虚拟空间的各个页映射到物理空间。
2,读取可执行文件头,并且建立虚拟空间和可执行文件的映射关系,这种映射关系保存在进程task_struct结构中的VMA区域,即虚拟内存区域,
3,将CPU的指令寄存器设置成可执行文件的入口地址,启动运行。
经过以上三个步骤后,操作系统只是通过可执行文件的头部信息建立可执行文件和进程虚拟内存之间的映射关系,在进程正在执行的时候会产生却页错误,这个时候操作系统分配物理页并将可执行文件中相应的页载入内存中。

一个可执行文件可能有很多段,每个可执行文件中的段往往只有少数几种组合,如:以代码段为代表的权限为可读的可执行段,以数据段和BSS段为代表的权限为可读可写的段,以只读数据段为代表的权限为只读的段。操作系统将具有相同权限的段统称为segment,然后不同的segment映射到进程地址空间中不同的VMA区域。ELF可执行文件中有一个专门的数据结构叫做程序头表(Program Header Table)用来保存“Segment”的信息。
一个进程可以分为如下几种VMA区域:
·代码VMA,rx,有映像文件
·数据VMA,rwx,有映像文件
·堆VMA,rwx,无映像文件,匿名,可向上扩展
·栈VMA,rx,无映像文件,匿名,可向下扩展

操作系统加载elf格式过程

从程序角度分析进程创建:

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()函数示例:

#include <stdio.h>
#include <signal.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
int glob = 5;
char buf[] ="a write to stdout\n";
int main(void)
{
	int var;
	pid_t pid;
	var = 88;
	if(write(STDOUT_FILENO,buf,(sizeof(buf)-1)) != (sizeof(buf)-1))
		printf("write error\n");
	printf("before fork\n");
	if((pid = fork()) < 0)
	{	
		printf("fork error\n");
	}
	else if (pid == 0)
	{
		glob++;
		var++;
	}
	else
	{
		sleep(2);
	}
	printf("pid = %d,glob = %d, var = %d\n",getpid(),glob,var);
	exit(0);
}
输出如下:
./a.out
a write to stdout
before fork
pid = 430,glob = 7, var = 89  
pid = 429,glob = 6, var = 88

vfork()示例程序如下:
#include <stdio.h>
#include <signal.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
int glob = 5;
char buf[] ="a write to stdout\n";
int main(void)
{
	int var;
	pid_t pid;
	var = 88;
	if(write(STDOUT_FILENO,buf,(sizeof(buf)-1)) != (sizeof(buf)-1))
		printf("write error\n");
	printf("before fork\n");
	if((pid = vfork()) < 0)
	{	
		printf("fork error\n");
	}
	else if (pid == 0)
	{
		glob++;
		var++;
	//	exit(0);
	}
	else
	{
		sleep(2);
	}
	printf("pid = %d,glob = %d, var = %d\n",getpid(),glob,var);
	exit(0);
}
函数输出如下:
./a.out
before fork
pid = 29039,glob = 7,var =89
子进程对变量做了加1操作结果改变了父进程中变量的值,因为子进程与父进程的地址空间相同,


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

#define RECOVERY_API_VERSION 2.3.1
const char* binary = "/tmp/update_binary";
int pipefd[2];
pipe [pipefd];
const char** args = (const char**)malloc(sizeof(char*)*5);
args[0]=binary;
args[1]=RECOVERY_API_VERSION;
char *temp = (char*)malloc(10);
sprintf(temp,"%d",pipefd[1]);
args[2]=temp;
args[3]=(char*)path;
args[4]=NULL;
pid_t pid = fork();
if(pid == 0)
{
	close(pipefd[1]);
	execv(binary,(char*const*)args));
	printf(exec child process error);
	_exit(-1);
}
close(pipefd[1]);
char buffer[1024];
FILE* from_child = fdopen(pipefd[0],"r");
while(fgets(buffer,sizeof(buffer),from_child)!=NULL)
	char*command = strtok(buffer,"\n");
waitpid(pid,&status,0)

fork()函数系统调用

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

asmlinkage int sysfork()
{
	return do_fork(SIGCHLD,regs.regs->ARM_sp,®s,NULL,NULL)
}

由以上代码可以知道,子进程结束后会发送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族函数

extern char **environ;
int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execle(const char *path, const char *arg, ..., char * const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execve(const char *filename, char *const argv[], char *const envp[]);
示例程序如下:
例子
#include <stdio.h>
#include <unistd.h>
int main()
{
    execl("/bin/ls","ls","-l",NULL);
 
    printf("如果execl执行失败,这个就会打印出来了\n");
    return 1;
}



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的地址


实例:
创建线程

struct task_struct *thread_task;
int rc;
thread_task=kthread_create(fsg_main_thread,common,"file-storage");
if(IS_ERR(thread_task))
{
	rc = PTR_ERR(thread_task);
	return ERR_PTR(rc);
}
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)
  以下代码为示例代码:

		      1 #include <sys/types.h> 
			  2 #include <signal.h> 
			  3 
			  4 int main(int argc, char* argv[]) 
			  5 { 
			  6     char* pid = argv[1]; 
			  7     int PID = atoi(pid); 
			  8 
			  9     kill(PID, SIGSEGV); 
			 10     return 0; 
			 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)内存非法访问实例

	      1 #include<stdio.h> 
		  2 int main() 
		  3 { 
		  4      char* str = "hello"; 
		  5      str[0] = 'H'; 
		  6      return 0; 
		  7 } 


 (2)除0异常

		 1 #include <stdio.h> 
		  2 
		  3 int main() 
		  4 { 
		  5     int a = 1, b = 0, c; 
		  6     printf( "Start running\n" ); 
		  7     c = a/b ; 
		  8     printf( "About to quit\n" ); 
		  9 } 


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

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值