——本篇文章参考《LINUX内核设计与实现 第三版》完成
进程是正在执行的程序以及它所包含的资源的总称。
这里的资源不仅包括代码,还包括:打开的文件、挂起的信号、内核内部数据、处理器状态、地址空间、一个或者多个执行线程、存放全局变量的数据段等其他资源。
(需要注意:程序本身并不是进程,实际上可能存在两个或者多个不同的进程执行同一个程序)
线程是进程中活动的对象,也是内核调度的对象。对于linux而言,线程是一种特殊的进程。
进程存放在名为任务队列(task list)的双向循环链表中。链表中每一项都是类型为task_struct,称为进程描述符的结构。进程描述符中包含的数据能完整的描述一个正在执行的程序:它打开的文件、进程的地址空间、挂起的信号、进程的状态等信息。
Linux通过slab分配器分配task_struct结构,这个结构可以重复使用。
在内核中访问任务通常需要获得指向task_struct的指针,因为内核中大部分处理进程的代码都是直接通过task_struct进行的。那么如何获得进程描述符的地址呢?
在寄存器并不富裕的X86这样的体系结构中:
在栈底创建一个新的结构:struct thread_info,这个结构体中有一个指向进程描述符的指针,从而方便找到进程描述符的位置。
然后通过current宏从thread_info的task域中提取并返回进程描述符的地址。
而有的硬件体系结构,比如PPC,有足够多的寄存器,就能够却能够拿出一个专门寄存器来存放指向当前进程描述符的指针。
①TASK_RUNNING(运行):进程是可执行的,它或者正在执行、或者在运行队列中等待执行。这是进程在用户空间中执行唯一可能的状态,也可以应用到内核空间中正在执行的进程。
②TASK_INTERRUPTIBLE(可中断):进程正在被阻塞,当某些条件达成之后,内核就会把这个进程状态设置为运行。
③TASK_UNINTERRUPTIBLE(不可中断):除了不会因为接收到信号而被唤醒从而投入到运行之外,这个状态和上面的可中断状态相同。这个状态通常在进程必须在等待时间不受干扰或者等待事件很快就会发生时出现。
④TASK_ZOMBIE(僵死):该进程已经结束,但是父进程没有调用wait4()系统调用。为了父进程能够获知它的消息,子进程的进程描述符仍然保留着,当父进程调用了wait4(),进程描述符就会被释放。
⑤TASK_STOPPED(停止):进程停止执行,不能投入运行。通常这种状态发生在接收到SIGSTOP\SIGTSTP\SIGTTIN\SIGTTOU等信号的时候,除此之外,在调试期间接收到任何信号,都会使进程进入这种状态。
使用set_task_state(task,state)函数,将任务“task”的状态设置为“state”
Linux和Unix系统的进程之间都存在一个明显的继承关系。所有的进程都是PID为1的init进程的后代。内核在系统启动的最后阶段启动init进程,init进程的进程描述符是作为init_task静态分配的。
系统中的每一个进程都必然有一个父进程,有零个或者多个子进程,拥有同一个父进程的所有进程被称为兄弟,进程间的关系也被存放在进程描述符中。
进程的创建首先是在新的地址空间里创建进程,然后读入可执行文件,最后执行。但是Unix把上述步骤分解到两个单独的函数:fork()和exec()。
fork()铜鼓拷贝当前进程创建一个子进程,
exec()函数负责读取可执行文件并将其载入到地址空间开始执行。
*path:新替换的程序的路径名称
*arg:传给新程序主函数的第一个参数,一般为程序的名字
*arg后面是剩余参数列表,参数个数可变,必须以空指针作为最后一个参数
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 * path, char* const argv[],char* const envp[]);
Linux的fork()使用写时拷贝实现。所谓写时拷贝就是:
资源的复制只有在需要写入的时候才进行,在此之前,父子进程都只是以只读方式共享。
pid_t fork(void);
函数返回类型 pid_t 实质是 int 类型。
fork 函数会新生成一个进程,调用 fork 函数的进程为父进程,新生成的进程为子进程。
在父进程中返回子进程的 pid,在子进程中返回 0,失败返回-1。
Linux系统中,fork()、vfork()和_clone()库函数都根据各自需要的参数标志去调用clone(),然后clone()去调用do_fork()。do_fork完成了创建中的大部分工作,它会调用copy_process()函数,然后让进程开始u运行。下面简述copy_process()函数完成的工作:
· 调用dup_task_struct()为新进程创建一个内核栈、thread_info结构和进程描述符,这些值与当前进程的值相同。此时子进程和父进程的进程描述符是完全相同的。
· 检查新创建的这个子进程后,当前用户所拥有的进程数目没有超过给他分配的资源的限制。
· 区分父子进程:子进程进程描述符内许多成员被设为初始值,而进程描述符中的数据共享父进程的数据。
· 子进程被设置为不可中断状态。
· copy_process()调用copy_flags()来更新进程描述符中的flags成员。表明进程是否拥有超级用户权限的PF_SUPERPRIV标志被清零。表明进程还没有调用exec()函数的PF_FORKNOEXEC标志被设置。
· 调用get_pid()为新进程获取一个有效的PID。
· 根据传递给clone()的参数标志,copy_process()拷贝或共享打开的文件、文件系统信息、信号处理函数等。
· 让父子进程平分剩余的时间片。
· copy_process()做扫尾工作并返回一个指向子进程的指针。
再回到do_fork()函数,如果copy_process()函数成功返回,新创建的子进程被唤醒并投入运行。
①下列程序输出几个"A"
代码一:
int main(int argc, char* argv[],char* envp[])
{
int i = 0;
for( ; i < 2; i++ )
{
fork();
printf("A\n");
}
exit(0);
}
(主要考察fork())
输出6个A。
代码二:
int main(int argc, char* argv[],char* envp[])
{
int i = 0;
for( ; i < 2; i++ )
{
fork();
printf("A");
}
exit(0);
}
(主要考察fork()和printf输出问题:满足“①缓冲区满、②强制刷新缓冲区fflish③程序结束时”这三个里面任何一个条件们就会将printf函数的内容输出到屏幕)。
输出8个A。
代码三:
int main()
{
fork() || fork();
printf("A\n");
exit(0);
}
输出3个A
代码四:
int main()
{
fork() && fork();
printf("A\n");
exit(0);
}
(代码三和四主要考察运算符和fork()
“||”:
表达式1为真,不用判断表达式2
表达式1为假,需要判断表达式2
“&&“:
表达式1为真,需要判断表达式2
表达式1为假,不用判断表达式2)
输出3个A
②写一个程序:父进程复制产生子进程,子进程用新的程序替换自身:
//父子进程
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <assert.h>
4 #include <unistd.h>
5 int main(int argc,char* argv[],char* envp[])
6 {
7 printf("main pid=%d\n",getpid());
8 pid_t pid=fork();
9 assert(pid!=-1);
10 if(pid==0)
11 {
12 //子进程替换b进程
13 char *myargv[]={"b","hello","world",(char*)0};
14 execve("./b",myargv,envp);
15 exit(0);
16 }
17 wait(NULL);
18 return 0;
19 }
//被替换的进程
1 #include <stdio.h>
2 #include <assert.h>
3 #include <stdlib.h>
4 #include <unistd.h>
5 int main(int argc,char* argv[],char* envp[])
6 {
7 printf("b pid=%d\n",getpid());
8
9 printf("argc=%d\n",argc);
10
11 for(int i=0;i<argc;i++)
12 {
13 printf("argv[%d]=%s\n",i,argv[i]);
14 }
15
16 for(int i=0;envp[i]!=NULL;i++)
17 {
18 printf("envp[%d]=%s\n",i,envp[i]);
19 }
20 return 0;
21 }
vfork()系统调用和fork()的功能相同,除了不拷贝父进程的页表项。子进程作为父进程的一个单独的线程在它的地址空间里运行,父进程被阻塞,知到子进程退出或执行exec()。
Linux把所有的线程都当作进程来实现。线程有自己的进程描述符,只是与其他一些进程共享某些资源,比如地址空间。
所以线程的创建和普通进程类似,只不过在调用clone()的时候需要传递一些参数标志来指明需要共享的资源。
fork()的实现:
clone(CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND,0);
vfork()的实现:
clone(CLONE_VFORK|CLONE_VM|SIGCHLD,0);
传递给clone()的参数标志决定了新创建进程的行为方式和父子进程之间共享的资源种类。下面是clone()用到的参数标志以及作用:
内核线程和普通的进程间的区别在于内核线程没有独立的地址空间。他们只是在内核空间运行,从来不会切换到用户空间去。然而内核进程和普通进程一样,可以被调度,可以被抢占。
内核线程只能由其他内核线程创建,在现有内核线程中创建一个新的内核线程的方法如下:
int kernel_thread(int (*fn)(void *),void *arg,unisgned long flage)
上面的函数返回时,父线程退出,并返回一个指向子线程进程描述符的指针,子线程开始运行fn指向的函数,arg时运行时需要用到的参数。一个特殊的clone标志CLONE_KERNEL定义了内核线程常用的参数标志:CLONE_FS,CLONE_FILES,CLONE_SIGHAND。大部分内核线程把这个标志传递给他们的flags参数。
进程终结依靠do_exit()完成。
进程终结时所需的清理工作和进程描述符的删除被分开执行。在父进程获得已经终结的子进程信息后,或者通知内核它并不关注子进程的那些信息,子进程的task_struct结构才会被释放。
wait()这一族函数都是通过唯一的一个系统调用wait4()实现的。它的标准动作是挂起调用它的进程,直到其中的一个子进程退出,此函数就会返回孩子进程的PID。
最终释放进程描述符,release_task()会被调用。
如果父进程在子进程之前退出,为了避免子进程的退出信息没有父进程来获取,从而导致子进程成为孤儿进程并处于僵死状态,白白耗费内存,那么解决这个问题的方法是:子进程在当前线程中内找一个线程作为父亲,如果不行,就让init做他们的父进程。 init进程会例行调用wait()来等待其子进程,清除所有预期相关的僵死进程。
由于下面的一些问题我有自己的理解,但是不知道是否标准,因此没有写出,欢迎大家在讨论区讨论。
(1) c 语言中所谓的内存泄漏问题
(2) malloc 申请一块空间,直到进程结束都没有释放,是否产生内存泄漏?
(3) malloc 申请 1G 的内存空间是否能成功?
(4) 在物理内存只有 2G 的系统中,malloc 能否申请 2G 空间,怎么思考?
(5) malloc 与 fork,父进程堆区申请的空间复制后,子进程也会有一份,也需要释放?
①打开文件:
int open(const char* pathname, int flags);//用于打开一个已存在的文件
int open(const char* pathname, int flags,mode_t mode);//用于新建一个文件
/*
pathname:将要打开的文件路径和名称
flags : 打开标志,如 O_WRONLY 只写打开
O_RDONLY 只读打开
O_RDWR 读写方式打开
O_CREAT 文件不存在则创建
O_APPEND 文件末尾追加
O_TRUNC 清空文件,重新写入
mode: 权限 如:“0600”
返回值:为文件描述符
*/
②读取文件:
ssize_t read(int fd, void* buf, size_t count);
/*
fd 对应打开的文件描述符
buf 存放数据的空间
count 计划一次从文件中读多少字节数据
返回值:为实际读到的字节数
*/
③写入文件
ssize_t write(int fd, const void* buf,size_t count);
/*
fd 对应打开的文件描述符
buf 存放待写入的数据
count 计划一次向文件中写多少数据
*/
④关闭文件:
int close(int fd);
//fd 要关闭的文件描述符
文件描述符在形式上是一个非负整数。实际上,它是一个索引值,指向内核为每一个进程所维护的该进程打开文件的记录表。当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符。(摘自百度百科)
比如:
标准输入的文件描述符是0
标准输出的文件描述符是1
标准错误输出的文件描述符是2
由于 fork 创建的子进程的 PCB 是拷贝父进程的,子进程的 PCB 中的文件表指向打开文
件的指针只是拷贝了父进程 PCB 中的值,所以父子进程会共享父进程 fork 之前打开的所有
文件描述符。(PCB:进程控制块,是进程存在的唯一标志,用来描述进程的属性信息)
区别: 系统调用的实现在内核中,属于内核空间,库函数的实现在函数库中,属于用
户空间。