这篇博客是我看《linux环境编程+从应用到内核》这本书中的知识点总结,若读者有看不懂的地方,可以参考书籍中的具体内容。

1,进程ID的分配
每个进程都会有自己的父进程,父进程又会有自己的父进程,最终都会追溯到init进程(pid=1)。关于init进程请参考链接init进程的前世今生

进程ID是唯一的,内核分配进程ID用延迟重用算法:
1>位图记录进程ID的使用情况(0为可用,1为已被占用)
2>将上次分配的ID记录到last_pid中,从last_pid+1开始找起,从位图中寻找可用的ID
3>如果找到位图集合的最后一位仍不可用,则回滚到位图集合的起始位置,从头开始找。
对于单线程的进程,调用getpid函数,其返回值就是当前进程的ID。对于多线程的进程,每一个线程调用getpid,其返回值都是进程ID

2,进程的层次
进程组是一组相关进程的集合,会话是一组相关进程组的集合。类似于在一个公司中,进程就是员工,会话相当于这个公司,而公司中的部门就是进程组。
PID:进程唯一标识符
PGID:进程组ID,默认情况下新创建的进程会继承父进程的进程组ID
SID:会话ID,默认情况下,新创建的进程会继承父进程的会话ID

获取进程组ID

pid_t getgrp(void);

获取会话ID

pid_t getsid(pid_t pid)

3,进程组

int setpgid(pid_t pid,pid_t pgid);

这个函数用来新建进程组或修改进程组id
pid指定的子进程,不能是会话首进程。子进程如果执行了exec函数,则不能修改子进程的进程组ID。
进程组和进程的关系:

带☆的表示进程组组长。
在命令结尾加“&”表示将命令放入后台执行,在任意时刻,可能同时存在多个后台进程,但只能存在一个前台进程。
当用户在终端输入(Crtl+C、Ctrl+Z等),对应的信号只会发送给前台进程组。

4,会话

会话的意义在于将很多的工作囊括在一个终端,选取其中一个作为前台来直接接收终端的输入及信号,其他工作则可以放在后台进行。

pid_t setsid(void);//创建会话

如果调用这个函数的进程不是进程组组长,那么会发生以下情景:
1)创建一个新会话,会话ID等于进程ID,调用进程成为会话的首进程
2)创建一个进程组,进程组ID等于进程ID,调用进程成为会话的进程组组长
3)该进程没有控制,如果调用setsid前,该进程有控制终端,这种联系就会断掉
调用setsid的进程不能是进程组的组长,否则会调用失败,返回-1

5,进程的创建之fork()

pid_t for(void);

fork成功向子进程返回0,失败返回-1。
注意:绝不能对父子进程执行顺序做出任何假设,如果确实需要某一特定的执行顺序,那么需要进程间通信的手段。

fork时子进程获得父进程数据空间、堆和栈的复制,所以变量的地址(当然是虚拟地址)也是一样的。

每个进程都有自己的虚拟地址空间,不同进程的相同的虚拟地址显然可以对应不同的物理地址。因此地址相同(虚拟地址)而值不同没什么奇怪。
具体过程是这样的:
fork子进程完全复制父进程的栈空间,也复制了页表,但没有复制物理页面,所以这时虚拟地址相同,物理地址也相同,但是会把父子共享的页面标记为“只读”(类似mmap的private的方式),如果父子进程一直对这个页面是同一个页面,知道其中任何一个进程要对共享的页面“写操作”,这时内核会复制一个物理页面给这个进程使用,同时修改页表。而把原来的只读页面标记为“可写”,留给另外一个进程使用。

这就是所谓的“写时复制”。正因为fork采用了这种写时复制的机制,所以fork出来子进程之后,父子进程哪个先调度呢?内核一般会先调度子进程,因为很多情况下子进程是要马上执行exec,会清空栈、堆。。这些和父进程共享的空间,加载新的代码段。。。,这就避免了“写时复制”拷贝共享页面的机会。如果父进程先调度很可能写共享页面,会产生“写时复制”的无用功。所以,一般是子进程先调度。

执行fork,内核会复制父进程所有的文件描述符。
创建线程和执行vfork,都不用复制父进程文件描述符每增加引用计数即可,对于fork而言,需要复制父进程的文件描述符(CLOSE_FILES),文件描述符的拷贝是通过内核的dup_fd函数来完成的。dup_struct会首先给子进程分配一个file_struct结构体,然后做一些赋值操作,这个结构体是进程描述符中与打开文件夹相关的数据结构,每一个打开的文件都会记录在该结构体中。
父子进程之间拷贝的是struct file的指针,而不是struct file实例,父子类型的struct file 类型指针,都指向struct file实例。

6,进程的创建之vfork
vfork会创建一个子进程,该子进程会共享父进程的内存数据,而且系统将保证子进程先于父进程获得调度,子进程也会共享父进程的地址空间,而父进程将被一直挂起,直到子进程退出或执行exec
注意:vfork后,子进程如果返回,则不要调用return,而应该使用_exit函数,如果使用return,就会出错。
一般来说,vfork创建的子进程会执行exec,执行完exec后,应该调用_exit返回,不是exit,因为exit会导致父进程stdio缓冲区的冲刷和关闭。

7,daemon进程的创建(守护进程)
特点:1)生命周期很长,一旦启动,正常情况下不会终止
2)在后台执行,并且不与任何控制终端相关联
创建一个daemon进程的步骤:
(1)执行一个fork函数,父进程退出,子进程继续。
原因:1>父进程有可能是进程组的组长(在命令行启动的情况下),从而不能够执行后面要执行的setsid函数,子进程继承了父进程的进程组ID,并且拥有自己的进程ID,一定不会是进程组的组长,所以子进程一定可以执行后面的setsid函数。
2>如果daemon是从终端命令行启动的,那么父进程退出会被shell检测到,shell会显示shell提示符,让子进程在后台执行
(2)子进程执行如下三个步骤,以摆脱与环境的关系
1>修改进程的当前目录为根目录 chdir(“/”);
2>调用setsid函数,这个函数的目的是切断与控制终端的所有关系,并且创建一个新的会话
3>设置文件格式创建掩码 umask(0)
(3)再次执行fork,父进程退出,子进程继续
确保daemon进程不是会话的首进程,才能保证打开的终端设备不会自动成为控制终端
(4)关闭标准输入(stdin),标准输出(stdout),标准错误(strerr)
一般关闭后,会打开/dev/null,并执行dup2函数,将0,1,2重定向到/dev/null,防止后面程序在文件描述符0,1和2上执行I/O库函数而导致报错。

8,进程的终止
正常退出:
(1)从main函数调用return
(2)调用exit
(3)调用_exit
异常退出:
(1)调用abort
(2)接收到信号,由信号终止

9,_exit函数

void _exit(int status)

status函数定义了进程的终止状态,父进程可以通过wait()来获取该状态值,status仅有低8位可以被父进程所用,所以写exit(-1)结束进程时,在终端执行“$?”会返回255
用户调用_exit(),本质是调用exit_group系统调用。

10,exit函数

#include<stdlib.h>
void exit(int status);

exit()最后也会调用_exit,但在调用之前还做了:
(1)执行用户通过调用atexit函数或on_exit定义的清理函数
(2)关闭所有打开的流(stream),所有缓冲的数据均被写入flush,通过tmpfile创建的临时文件都会被删除
(3)调用_exit
exit和_exit的区别如图所示

11,僵尸进程
父进程调用fork创建子进程,子进程退出后,父进程如果不调用wait或waitpid来获取子进程的退出信息,子进程就沦为僵尸进程
清除僵尸进程的方法:
(1)父进程调用wait,为子进程“收尸”
(2)父进程退出,init进程会为子进程“收尸”
如何预防僵尸进程的产生呢?
(1)若是不关心子进程的退出状态,就将父进程对SIGCHID的处理方式设置为SIG_IGN,或在调用sigaction函数时设置SA_NOCLDWAIT标志位,子进程就不会陷入僵尸状态,而是调用release_task函数“自行了断”。
(2)若是关心子进程的退出状态,则应及时调用wait

12,等待子进程之wait()

#include<sys/wait.h>
pid_t wait(int* status);

成功时,返回已退出子进程的ID,失败返回-1。
调用wait的两种情况:
(1)子进程先退出,父进程后调用wait函数,父进程获取到子进程的状态信息,wait函数立刻返回
(2)父进程先调用wait,子进程后退出。
调用时并无子进程退出,该函数就会陷入阻塞状态,直到某个子进程退出。
注意:wait()函数等待时,任何一个子进程退出,都可以让其返回。
wait()的返回有三种可能性:
(1)等到了子进程退出,获取其退出信息,返回子进程的进程ID。
(2)等待过程中,收到了信号,信号打断了系统调用,并且注册信号处理函数时并没有设置SA_RESTART标志位,系统调用不会被重启,wait()函数返回-1,并设置errno为EINTR
(3)已经成功地等到了所有子进程,没有子进程的退出信息需要接收,在这种情况下,wait()返回-1,并设置errno为ECHID
wait()的局限性:
(1)不能等待特定的子进程,如果进程存在多个子进程,而它只想获取某个子进程的退出状态,并不关心其他子进程的退出状态,此时wait()只能一一等待,通过查看返回值来判断是否为关心的子进程。
(2)如果不存在子进程退出,wait()只能阻塞。有些时候,仅仅是想尝试获取退出子进程的退出状态,如果不存在子进程退出就立刻返回,不需要阻塞等待,类似于trywait的概念,wait()韩阿叔设有提供trywait的接口。
(3)wait能够探知子进程的死亡而不能探知暂停,也无法探知子进程恢复执行。

13,等待子进程之waitpid()

#include<sys/wait.h>
pid_t waitpid(pid_t pid,int *status,int options)

与wait函数相同的地方:
(1)返回值得含义相同,都是终止子进程或因信号停止,或因信号恢复而执行的子进程的进程ID
(2)status含义相同,都是用来记录子进程的相关事件
waitpid函数的第三个参数options是一位掩码(bit mask),可以同时存在多个标志,当options没有设置任何标志时,其行为与wait类似,即阻塞等待与pid匹配的子进程退出。
options的标志位可以是以下标志位的组合:
(1)WVNTRACE:除了关心终止子进程的信息,也关心那些因信号而停止的子进程的信息。
(2)WCONTINVED:除了关心子进程的信息,也关心那些因收到信号而恢复执行的子进程的状态信息。
(3)WNOHANG:指定的子进程并未发生状态变化,立刻返回,不会阻塞。这种情况下返回0。如果调用进程并没有与pid匹配的子进程,则返回-1。并设置errno为ECHILD,根据返回值和errno可以区分这两种情况。

kill -19  //暂停进程
kill -18  //恢复进程

waitpid的缺陷:无论用户是否关心相关子进程的终止事件,终止事件都可能会返回给用户,当waitpid返回时,可能是因为进程终止,也可能是因为子进程停止。

13,等待子进程之waitid()

#include<sys/wait.h>
int waitid(idtype_t idtype,id_t id,siginfo_t* infop,int option)

第一个入参idtype和第二个入参id用于选择用户关心的子进程
(1)idtype == P_PID:精确打击,等待进程ID等于id的子进程
(2)idtype == P_PGID:在所有的子进程中等待进程组ID等于id的进程
(3)idtype == P_ALL:等待任意子进程,第二个参数id被忽略
第四个参数options:
WEXITED:等待子进程的终止事件
WSTOPPED:等待被信号暂停的子进程事件
WCONTIUED:等待先前被暂停,但被SIGCONT信号恢复执行的子进程

WNOHANG:与id匹配的子进程若并无状态信息需要返回,则不阻塞,立刻返回,返回值是0。如果调用进程并无进程与id匹配,则返回-1,并设errno为ECHILD
WNOWAIT:只负责获取信息,不改变子进程的状态,带有WNOWAIT标志位调用waitid函数,稍后还可以调用wait或waitpid或waitid再次获得同样的信息。
第三个参数infop本质是个返回值,系统调用负责将子进程的相关信息填充到infop指向的结构体中。
对于返回值,在两种情况下会返回0:
(1)成功等到子进程的变化,并取回相应的信息
(2)设置了WNOHANG标志位,并且子进程状态无变化
为了区分这两种情况,内核是通过先将siginfo_t结构体清零,返回后,通过判断si_pid是否为0来分辨这两种情况。

14,进程退出和等待的内核实现

在do_exit中,还有两件事情需要exit_notify完成
(1)forget_aniginal_parent:负责给退出的子进程寻找新的父进程
1>为子进程寻找新的父进程,通过find_new_raper()函数完成,如果退出的进程是多线程进程,则可以将子进程他托福给自己的兄弟进程,没有没有,就“托孤”给init进程。
2>将子进程的父进程设置为第一步中找到的新的父亲。
(2)do_notify_parent:负责通知退出进程的父进程
在用户层面,可以调用pthread_exit让主线程先“死”,但在内核态中,主线程的task_struct一定要挺住,哪怕编程僵尸,也不能释放资源。
注意:只有线程组的主线程才有资格通知父进程,线程组的其他线程终止的时候。不需要通知父进程
当线程组最后一个线程退出时,如果发现:
(1)该线程不是线程组的主线程
(2)线程组的主线程已经退出,且处于僵尸状态
(3)自己是最后一个线程
同时满足这三个条件时,该子进程就需要冒充线程组的组长,即以子进程的主线程的身份来通知父进程。

15,父子进程的互动
父子进程的活动有两种方式:
(1)子进程向父进程发送SIGCHLD信号
子进程退出时,异步通知父进程,发送SIGCHLD信号。父进程收到该信号,默认行为是置之不理,此时,子进程会陷入僵尸状态。但这又会浪费系统资源,该状态会维持到父进程退出,子进程被init进程接管,init进程会等待僵尸进程,使僵尸进程释放资源。
如果父进程不太关心子进程的退出事件,可采用以下方法:
1>父进程调用signal函数或sigaction函数,将SIGCHLD信号的处理函数设置为SIG_IGN
2>父进程调用sigaction函数,设置标志位时置上SA_NOCLDNAIT位(如果不关心子进程的暂停和恢复执行,则置上SA_NOCLDSTOP位)
(2)子进程唤醒父进程(等待队列)
父进程调用wait主动等待,如果父进程调用wait陷入阻塞,那么子进程退出时,又该如何及时唤醒父进程呢?子进程会调用_wake_up_parent来唤醒父进程

16,exec家族
先来了解一下execve函数:

#include<unistd.h>
int execve(const char* filename,char* const argv[],char* const envp[]);

第一个参数是执行新程序的路径名,第二个参数argv[0]一般对应可执行文件的文件名。第三个参数与C语言的main函数中的第三个参数envp一样,也是字符串指针数组,以NULL结束,指针指向字符串的格式为name=value.一般来说,execve函数总是紧随fork之后,父进程调用fork后,子进程执行execve函数,抛弃父进程的程序段,和父进程分道扬镳。execve()成功不返回,失败返回-1。
再来看exec家族:

(1)int execl(const char *path, const char *arg, ......);(2)int execle(const char *path, const char *arg, ...... , char * const envp[]);(3)int execv(const char *path, char *const argv[]);(4)int execve(const char *filename, char *const argv[], char *const envp[]);(5)int execvp(const char *file, char * const argv[]);(6)int execlp(const char *file, const char *arg, ......);

这些函数其实本质上都是调用execve系统调用,只是使用方法略有不同。
exec的作用:一个进程需要执行另一个程序,该进程首先调用fork创建一个副本,然后其中一个副本(通常是子进程)调用exec把自身替换成新的程序。

Linux:进程控制相关推荐

  1. linux系统进程控制实验报告,Linux进程控制实验报告.doc

    里奴性进程控制实验报告 实验名称: Linux进程控制 实验要求:一.编写一个Linux系统C程序,由父亲创建2个子进程,再由子进程各自从控制台接收一串字符串,保存在各自的全局字符串变量中,然后正常结 ...

  2. 北风网 linux,linux进程控制笔记北风网分享.doc

    linux进程控制笔记北风网分享 linux 进程控制笔记 进程创建 普通函数调用完成后,最多返回 return 一次,但fork/vfork会返回二次,一次返回给父进程,一次返回给子进程 父进程的返 ...

  3. 实验四 linux进程控制实验报告,Linux系统进程控制操作系统实验报告4

    实验课程名称:操作系统 实验项目名称Linux系统进程控制实验成绩 实验者专业班级组别 同组者实验日期年月日第一部分:实验分析与设计(可加页) 实验内容描述(问题域描述) 要求:掌握Linux系统中进 ...

  4. 【转载】linux进程控制-exec系列 exec系统调用

    inux进程控制-exec系列 说是exec系统调用,实际上在Linux中,并不存在一个exec()的函数形式,exec指的是一组函数,一共有6个,分别是: #include <unistd.h ...

  5. linux进程操作相关函数,Linux进程控制简介与要素及相关函数详解

    进程是操作系统中的一个重要概念,它是一个程序的一次执行过程,程序是进程的一种静态描述,系统中运行的每一个程序都是在它的进程中运行的. 进程4要素 要有一段程序供该进程运行 进程专用的系统堆栈空间 进程 ...

  6. linux进程控制(上)

    linux进程 1.冯诺依曼体系 截至目前,我们所认识的计算机,都是有一个个的硬件组件组成 输入单元:包括键盘, 鼠标,扫描仪, 写板等 中央处理器(CPU):含有运算器和控制器等 输出单元:显示器, ...

  7. 小何讲进程: Linux进程控制编程 (fork、vfork)

    所谓进程控制,就是系统使用一些具有特定功能的程序段来创建进程.撤消进程以及完成进程在各种状态之间的转换, 从而达到多进程高效率并发执行和协调资源共享的目的.进程控制是进程管理和处理机管理的一个重要任务 ...

  8. 玩转Linux进程控制命令

    目录 1.0查看系统中的进程命令 1.1 ps命令 1.2 top命令 2.0 控制系统中的进程命令 2.1 kill命令 2.2 killall 命令 2.3 nice 命令 2.4 renice命 ...

  9. Linux进程控制:wait获取子进程退出状态 WIFEXITED和WIFSIGNALED用法

    可以使用wait函数传出参数status来保存进程的退出状态. 常用宏函数分为日如下几组: 1. WIFEXITED(status) 若此值为非0 表明进程正常结束. 若上宏为真,此时可通过WEXIT ...

  10. Linux进程控制——exec函数族

    1.简介 在Linux中,并不存在exec()函数,exec指的是一组函数,一共有6个,分别是: #include <unistd.h> extern char **environ; in ...

最新文章

  1. python里面的之前打过的记忆信息-python中的if __name__ == 'main'
  2. hibernate插入数据测试无异常,但数据库没有数据
  3. 谈谈C#的私有成员的一个有趣的现象!
  4. dubbo+rabbitmq+hystrix实现服务的高可用
  5. 术语html的含义是,术语html指的是什么
  6. android活动中的变量,在不同的活动中保持变量值Android Studio
  7. 剑指offer面试题[42]-反转单词顺序VS左旋转字符串
  8. Linux 命令行提示符路径显示
  9. fd 句柄_FD_CLOEXEC用法及原因-文件句柄
  10. 【数据结构】线性表之数组---C++语言描述
  11. 【Android 】【Monkey Demons】 针对性的进行稳定性测试
  12. android移动商城源码,o2o移动社区Android端app开源源码
  13. 电脑端微信多开操作方法
  14. SPSS基础操作(一):用幂指数型的权函数建立加权最小二乘回归方程
  15. QT警告Slots named on_foo_bar are error prone
  16. 【机器学习】某工19级智科专业机器学习期末复习资料
  17. autojs实用文档
  18. GD32 CAN波特率计算问题
  19. 【C51】基于C51单片机的LCD电子时钟设计(含代码,电路图,拿去直接用)
  20. 【总结】1263- 弄懂 SourceMap,前端开发提效 100%

热门文章

  1. 微信网页Audio自动播放(IOS安卓)
  2. Java毕设项目在线答题系统计算机(附源码+系统+数据库+LW)
  3. PyCharm谷歌翻译插件Translation提示:更新 TKK 失败,请检查网络连接
  4. max3490esa_MAX4524EUB_美信MAXIM半导体代理就找宇航
  5. 善用思维导图来整理发散的思维
  6. 用Python实现斐波那契数列代码
  7. 自动驾驶中激光雷达如何检测障碍物
  8. 计算机网络安全技术保护措施,计算机网络安全技术保护措施
  9. 英文论文如何看?转自知乎
  10. Linuxnbsp;用命令行打开docnbsp;pdf…