linux c进程创建,Linux C编程--进程创建fork

单纯会C的语法是做不了事儿的。

作为底层语言,还得熟悉其运行的操作系统,熟悉需要解决的问题,然后才可以用C语言去设计对应的“数据结构和算法”,从而解决问题。

对于操作系统来说,进程是其核心概念之一。围绕着进程,有一系列的系统API(Linux下又称系统调用,system call),每个API又对应着内核里的一系列复杂的机制,来管理进程的整个生命周期:创建、调度、运行、退出。

以下关于CPU寄存器的,以x86 32位为例。

1,进程的创建----fork()系统调用

在Linux命令行输入man fork,可以看到关于fork的介绍,需要包含unistd.h头文件,函数原型:pid_t fork(void);

fork()执行一次,返回两次,子进程返回0,父进程返回子进程的pid(进程号)。

#include

#include

#include

int main()

{

pid_t pid = fork();

//在该行之后,就是两个进程了,之后的代码位于不同的“进程位面”了,要互相通信,就得依赖OS提供的“进程间通信”(IPC)机制

if (0 == pid) {

printf("child process\n");

exit(0);

} else {

printf("father process, and child process id: %d\n", pid);

}

return 0;

}

fork依照父进程为模版,创建了子进程,在fork之前的数据被“复制”了一份给子进程。然后,父子两个就位于不同的“进程位面”了。它们的数据是独立的,可以在不同进程里有不同值,实际对应的内存也是不一样的,所以pid可以在子进程为0、在父进程为子进程的pid。

Linux有API getpid()获取自己的进程号,getppid()获取父进程的进程号。但因为一个进程可以创建多个子进程,所以没有获取子进程号的API,这需要父进程在fork的返回值中获取。

进程之间,与同一进程的各个线程之间不同,前者的内存是独立的,而后者是共享的。进程之间通信,需要使用IPC机制,例如发信号signal、共享内存、管道pipe、网络socket等,现在多使用AF_UNIX域的socketpair,详见《UNIX环境高阶编程》,暂不展开。

2,fork系统调用在内核的大概原理

fork()系统调用,在用户态的使用非常简单,但在内核里则复杂很多。在内核里,进程是由task_struct结构体描述的,一个简单的task_struct如下(仅表示原理,实际内核的复杂的多):

struct task_struct {

pid_t pid; //进程id

pid_t ppid; //父进程id

struct list_head child_list; //子进程队列

struct list_head brother_list; //兄弟进程队列,表头是父进程的child_list

struct list_head sched_list; //调度队列,实际内核用红黑树,提高效率

struct mm* mm;//内存管理结构(内存页表)的指针

这两个是最关键的进程运行的上下文,执行进程切换时必需,

unsigned long esp; //进程的内核堆栈指针

unsigned long eip; //进程的内核指令指针

};

有了task_struct,就可以实现fork系统调用了,内核里对应的就叫sys_fork吧:

1,进入内核态之后,用户态的上下文就都压栈在内核栈上了,

2,申请子进程的task_struct,

3,申请子进程的内存页表,并且和父进程的内容设置成一样的,并且设置为只读。

当父子任何一个进程需要写内存时,触发缺页中断。在缺页中断的处理程序中,对要写的内存页申请一份新的COPY替换掉写进程的对应页,这就是“写时复制”。

4,申请个新的pid作为子进程的pid,设置到子进程的task_struct中,依次填充其他的数据项,

5,设置子进程的esp,与父进程一样

6,设置子进程的eip,该地址是子进程第一次执行时的地址。

并把保存在“子进程内核栈”上的、用户态eax寄存器对应的、内存,设置为0,以保证fork返回用户态后,返回值为0。

7,父进程把保存在“内核栈”上的、用户态eax寄存器对应的、内存,设置为子进程的pid,以保证fork返回用户态后,返回值为子进程的pid。

8,子进程的task_struct通过其sched_list变量被挂上调度器,而且调度优先级排在父进程前面,保证子进程先于父进程执行。

这样,子进程如果开始就执行execve系统调用,加载新的程序,就不会触发写时复制了。而父进程先执行,那么“几乎肯定”要写内存,执行“写时复制”。然并卵,子进程很大概率不接着执行原代码,而是execve了,白复制了:浪费感情,哦,浪费CPU和内存。

这类情况,在shell里执行命令时,就是这么发生的。shell先fork一个子进程,然后加载你输入的命令,并执行。

9,做完上面几步后,就完成了子进程的创建了。但是,我们还在父进程的上下文里,如果直接返回用户态,那么父进程就会接着运行。为了实现第8条需要的、让子进程先运行的目标,我们执行进程调度函数:schedule();

选择权交给调度器了,接下来哪个进程先运行,我们也不知道。但是,在没有“其他改变调度优先级的因素”的情况,肯定子进程先于父进程运行。

如果发生了这类因素,那就顺其自然吧,毕竟我们无法判断这因素到底对用户多重要。

用户是上帝,惹不起:(

3,fork系统调用在用户态的大概步骤

因为不需要传递参数,只要把fork的系统调用号放入对应的寄存器(一般是eax),然后

int $0x80,使用CPU的软件中断,进入内核态,剩下的事儿,就是Linus大神的事儿了。

系统调用号,在不同版本的Linux上不大一样。

我的是ubuntu 16.04,在/usr/include/asm-generic/unistd.h文件里可以查到,都是以__NR_开头。

4,在内核里获取当前进程的task_struct,直接使用current宏就行。

printk("current->pid: %d\n", current->pid );

因为内核态时,堆栈和thread_info是绑定在连续的几个内存页上的(最早是4K的一个页,现在记得是8K,两个页,不知道新版有没变化),只要把esp寄存器的最低N位清0就是thread_info的指针,然后thread_info里存有task_struct的指针。

getpid()函数在内核对应的pid_t sys_getpid()的实现,可以这么写:

pid_t sys_getpid()

{

return current->pid;

}

关于fork的,就先说到这里了。

具体怎么使用,详见man手册。

具体怎么实现的,详见Linux内核源码。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值