【进程管理】创建进程

本文详细介绍了Linux中进程的创建方法(包括fork(),vfork()),进程终结时的do_exit()函数和wait()函数,以及线程的创建和内核线程的管理。重点讨论了fork()的实现原理和写时拷贝技术,以及进程终结时资源清理过程。
摘要由CSDN通过智能技术生成

目录

一、进程创建

1、fork()函数

(1)基本介绍

(2)示例

(3)copy_process()

2、写时拷贝

3、vfork()函数

二、进程终结

1、do_exit()函数

2、wait()函数

三、线程创建

1、创建线程

2、内核线程

(1)创建内核线程

(2)停止内核线程

一、进程创建

        系统允许一个进程创建新进程,即为其子进程,子进程还可以继续创建新的子进程,形成树状的数据结构模型

1、fork()函数

(1)基本介绍

        在Linux中,使用fork()函数创建进程,此函数主要开销就是复制父进程的页表, 特点是一次调用,两次返回

#include <sys/types.h>
#include <unistd.h>

pid_t fork(void);

        使用man命令查看fork函数帮助

DESCRIPTION
    fork() creates a new process by duplicating the calling process.  The new process is referred to as the child process.  The calling process is referred to as the parent process.

RETURN VALUE
    On success, the PID of the child process is returned in the parent, and 0 is returned in the child.  
    On failure, -1 is returned in the parent, no child process is created,  and  errno  is  set appropriately

        从中可以看出,当创建失败时,会返回对应的errno,主要的两个原因是超过了线程数量限制和没有足够的内存空间

a.EAGAIN A system-imposed limit on the number of threads was encountered.。
b.ENOMEM fork() failed to allocate the necessary kernel structures because memory is tight.
(2)示例

        编写程序如下,体会fork()函数的基本使用

#include <unistd.h>
#include <stdio.h>
#include <sys/types.h>

int main()
{
  printf("I'm a process, pid[%d]parent id[%d]\n", getpid(), getppid());
  sleep(1);

  pid_t id =fork();

  if (id == 0) {
      printf("I'm child process pid[%d],ppid[%d]\n", getpid(), getppid());
  } else if (id > 0) {
      sleep(1);
      printf("I'm father process pid[%d],ppid[%d]\n", getpid(), getppid());
  } else {
    printf("fork error, errno [%d]\n", id);
  }

  printf("test end, process pid[%d]\n", getpid());

  return 0;
}
# gcc test.c -o run
# ./run

# I'm a process, pid[29886]parent id[18759]
# I'm child process pid[29887],ppid[29886]
# test end, process pid[29887]
# I'm father process pid[29886],ppid[18759]
# test end, process pid[29886]

        可以看到,在fork()创建进程后,公共部分的代码都被执行了

(3)copy_process()

        除了fork(),还有vfork()、__clone()创建进程,本质上都是由clone()系统调用实现的,最终会调用到do_fork()(Linux 4.20中还有do_fork(),最新版本叫kernel_clone()),完成创建的大部分工作,在do_fork()中,copy_process()会做收尾工作并返回一个指向子进程的指针,如果成功返回,新创建的子进程会被唤醒并让其投入运行,内核会有意选择让子进程先执行

        copy_process()主要完成了进程信息的拷贝,下面对其进行简单跟踪

a.为新进程创建一个内核栈、thread_info结构和task_struct,这些值与当前进程的值相同,此时父子进程的PCB相同

p = dup_task_struct(current, node);

b.检查创建这个进程后,当前用户所拥有的进程数目没有超过给它分配资源的限制,可以看到一个user指针,这是每个用户有且仅有的一个结构,每个用户有多个进程,但用户信息并不专属于某个进程

if (atomic_read(&p->real_cred->user->processes) >=
		task_rlimit(p, RLIMIT_NPROC)) {
	if (p->real_cred->user != INIT_USER &&
		!capable(CAP_SYS_RESOURCE) && !capable(CAP_SYS_ADMIN))
		goto bad_fork_free;
}

c.子进程与父进程区别开,子进程PCB中许多成员都要被清零或者初始化

d.子进程状态设置为TASK_UNINTERRUPTIBLE,以保证其不会投入运行

e.调用copy_flags()更新flags成员

f.调用alloc_pid()

if (pid != &init_struct_pid) {
	pid = alloc_pid(p->nsproxy->pid_ns_for_children);
	if (IS_ERR(pid)) {
		retval = PTR_ERR(pid);
		goto bad_fork_cleanup_thread;
	}
}

g.根据传递给clone()的参数,copy_process()拷贝或共享打开的文件、文件系统信息、信号处理函数、进程地址空间和命名空间等。一般情况下,这些资源会被给定的进程的所有线程共享;否则,这些资源对每个进程是不同的,因此被拷贝到这里。(将资源参数标志赋值给结构体)

h.返回do_fork()函数一个指向子进程的指针,如果copy_process()函数成功返回,新创建的子进程被唤醒并让其投入运行

2、写时拷贝

        fork()的主要开销就是复制父进程的页表以及给子进程创建PID,在fork时并不是把所有资源直接复制给新创建的进程,而是使用了写时拷贝(copy-on-write)这种推迟或者免除数据复制的技术,即子进程在创建时和父进程共享一个地址空间,只有数据修改时,才会开辟一部分空间复制一份数据供子进程使用,在此之前如果不修改数据,就不用进行拷贝,都是以只读方式共享

3、vfork()函数

        vfork()不拷贝父进程的页表项,其余与fork()功能相同,子进程会作为父进程的一个单独的线程在其地址空间运行,父进程会被阻塞直到子进程退出或者执行exec()

二、进程终结

        当一个进程终结时,内核必须释放它所有的资源,并且通知其父进程。一般发生在进程调用exit()系统调用时或者隐式的从某个程序的主函数返回,当进程接收到它不能处理也不能忽略的信号时,还可能被动终结。最终都是依靠do_exit()来完成

1、do_exit()函数

a.将task_struct中标志成员设置成PF_EXITING

b.调用del_timer_sync()删除任一内核定时器。确保没有定时器在排队,也没有定时器处理程序在运行

c.如果BSD的记账功能是开启的,do_exit()调用acct_update_integrals()来输出记账信息。

d.调用exit_mm()函数释放进程占用的mm_struct,如果没有别的进程同时使用它们(也就是说,这个地址空间没有被共享),就彻底释放它们

e.调用sem__exit()函数,如果进程排队等待IPC信号,它则离开队列。

f.调用exit_files()和exit_fs()分别递减文件描述符,文件系统数据引用计数,如果其中某个引用计数的数值降为零,那就不用代表没有进程在使用相应的资源,此时可以释放。

g.把存放在task_struct的exit_code()成员中的任务退出代码置为由exit()提供的退出代码,或者去完成任何其他有内核机制规定的退出动作。退出代码存放在这里供父进程随时检索。

h.调用exit_notify向父进程发送信号,给子进程重新找养父(其他线程或init进程),并将存放在task_struct结构中的exit_state设置为EXIT_ZOMBIE。

i.do_exit调用schedule()切换到新的进程,因为处于EXIT_ZOMBIE状态的进程不会被调度,所以这是进程所执行的最后一段代码,do_exit()永不返回

        调用之后,尽管进程已经不能运行了,但系统还保留了它的PID,直到父进程获得已终结的子进程信息后,子进程的task_struct才会被释放

2、wait()函数

        wait族函数都是通过唯一但很复杂的一个系统调用wait4()来实现的,挂起调用它的进程,直到其中的一个子进程退出,此时函数会返回子进程的PID。此外,调用此函数时提供的指针会包含子函数的退出代码。

孤儿进程:父进程先于其产生的子进程结束,子进程会被init进程收养(进程号为1),并由init进程对它们完成状态收集工作

僵尸进程:如果子进程结束了,但是父进程没有调用wait或者waitpid(),那么子进程的资源就无法得到收集释放

#include <sys/types.h>/* 提供类型pid_t的定义*/

#include <wait.h>

int wait(int *status)

        下面是一个示例,进程一旦调用了wait,就会立刻阻塞自己,由wait分析当前进程中的某个子进程是否已经退出了,如果让它找到这样一个已经变成僵尸进程的子进程,wait会收集这个子进程的信息,并将它彻底销毁后返回;如果没有找到这样一个子进程,wait会一直阻塞直到有一个出现

#include <unistd.h>
#include <stdio.h>
#include <sys/types.h>
#include <wait.h>
#include <stdlib.h>

int main()
{
  int cnt = 0;
  int status = -1;
  int i = 10;

  printf("I'm a process, pid[%d]parent id[%d]\n", getpid(), getppid());

  pid_t id =fork();

  if (id == 0) {
      printf("I'm child process pid[%d],ppid[%d]\n", getpid(), getppid());

      while(i--) {
        cnt++;
        sleep(1);
        printf("I'm child process, cnt[%d]\n", cnt)  ;
      }

      exit(5);
  } else if (id > 0) {
      printf("I'm father process pid[%d],ppid[%d]\n", getpid(), getppid());
      wait(&status);
  } else {
    printf("Fork() error, errno [%d]\n", id);
  }

  printf("Child process exit status[%d]\n", WEXITSTATUS(status));
  printf("Process end[%d]\n", getpid());

  return 0;
}
# ./run
I'm a process, pid[13834]parent id[18759]
I'm father process pid[13834],ppid[18759]
I'm child process pid[13835],ppid[13834]
I'm child process, cnt[1]
I'm child process, cnt[2]
I'm child process, cnt[3]
I'm child process, cnt[4]
I'm child process, cnt[5]
I'm child process, cnt[6]
I'm child process, cnt[7]
I'm child process, cnt[8]
I'm child process, cnt[9]
I'm child process, cnt[10]
Child process exit status[5]
Process end[13834]

        从运行结果可以看到,子进程退出后,父进程才退出

三、线程创建

        在Linux内核中,并没有线程的概念,Linux将所有的线程都当做进程来实现,仅仅被视为一个与其他进程共享某些资源的进程,每个线程都有自己的task_struct

1、创建线程

        对于Linux,线程的创建和普通进程的创建类似,只不过在clone()系统调用时候传递一些标志指明共享的资源

clone(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND, 0);
FlagsDesc
CLONE_VM父子进程共享地址空间
CLONE_FS父子进程共享文件系统信息
CLONE_FILES父子进程共享打开的文件
CLONE_SIGHAND父子进程共享信号处理程序及被阻断的信号

2、内核线程

        内核通常线程完成内核需要在后台进行的工作,只在空间被执行,没有独立的地址空间(p->mm = NULL),可以被调度和抢占

(1)创建内核线程

        内核线程只能由其他内核线程创建,通过kthreadd内核线程(PID=2)衍生出所有新的内核线程(相当于祖先线程), kthreadd就是专门负责内核线程管理工作的。

        kthread_create()函数,通过clone()系统调用创建一个内核线程,创建的线程处于不可运行状态 kthread.h - include/linux/kthread.h - Linux source code (v6.8) - Bootlin

#define kthread_create(threadfn, data, namefmt, arg...) \
	kthread_create_on_node(threadfn, data, NUMA_NO_NODE, namefmt, ##arg)

         除了  kthread_create(),kthread_run()函数,实际上就是调用kthread_create()创建线程,接着调用wake_up_process()唤醒 kthread.h - include/linux/kthread.h - Linux source code (v6.8) - Bootlin

#define kthread_run(threadfn, data, namefmt, ...)			   \
({									   \
	struct task_struct *__k						   \
		= kthread_create(threadfn, data, namefmt, ## __VA_ARGS__); \
	if (!IS_ERR(__k))						   \
		wake_up_process(__k);					   \
	__k;								   \
})
(2)停止内核线程

        内核线程启动后就一直运行直到调用do_exit()退出,或者其他进程调用下面函数停止

int kthread_stop(struct task_struct *k);

【参考博客】

[1]《Linux 内核设计与实现》

[2] https://zhuanlan.zhihu.com/p/528841667

[3] https://hqber.com/archives/393/

[4] 详解fork函数-CSDN博客

[5] https://www.cnblogs.com/atest/p/17647349.html

[6] fork函数详解-CSDN博客

[7] https://blog.csdn.net/weixin_42581177/article/details/127575249

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值