Linux 系统编程 学习:01-进程的有关概念 与 创建、回收

背景

上一讲介绍了有关系统编程的概念。这一讲,我们针对 进程 开展学习。

概念

进程的身份证(PID)

每一个进程都有一个唯一的身份证号码,称之为进程号PID(Process Identity Number)。

每一个进程都有其双亲进程,称之为父进程(或许称为双亲进程更贴切)。

所有的进程都是祖先进程init的后代,除了init进程,每一个进程都有一个父进程。

通过 pstree 命令,可以清楚地看到系统中各个进程间的内在关系.

程序计数器:程序中即将被执行的下一条指令的地址。

内存指针:包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块指针

进程优先级

优先级:相对于其他进程的优先级。

并行和并发

  • 并行:指两个或者多个事件在同一时刻发生;
  • 并发:指两个或多个事件在同一时间间隔发生

程序和进程

程序:二进制文件,占用磁盘空间

进程:运行着的程序,数据在内存中,占用系统资源、CPU、物理内存

进程的 有关流程

每个进程都有自己的生命周期,包括创建、执行、终止和删除。操作系统的运行过程中不断的重复这些过程,因此从操作系统性能的角度来看,进程的生命周期非常重要。

父进程可以通过系统调用fork()创建子进程。

fork()调用后将创建子进程的描述符和进程ID,在子进程中复制父进程的进程描述符,同时并不复制父进程的地址空间,而是在父进程的地址空间中运行。

exec(系统调用将在子进程的地址空间中复制新的程序数据。

由于共享相同的地址空间,写入新的程序数据将会导致内存的页错误,因此,在这种情况下,Linux内核将分配新的物理页给子进程。

一般情况下,子进程执行自己的程序,避免了复制完整地址空间的低效率操作。

当程序执行完成时,子进程通过系统调用exit()终止进程。

系统调用exit()释放子进程相应的资源,并发送信号给父进程,通知子进程的终止。在这个时刻,子进程被称为僵尸进程。

父进程通过系统调用wait()接收子进程的终止信号。当父进程接收到该信号,删除子进程所有的数据结构,并释放子进程的进程描述符,这个时候子进程才被完全删除。

进程的创建

在一个进程中创建另外一个进程,新创建的进程就为子进程。

在Linux中主要提供了fork、vfork、clone三个进程创建方法。

fork:

#include <unistd.h>

pid_t fork(void); // pit_t -> unsigned long int

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.
/*
成功时,返回两个值:
对于父进程,fork函数返回子程序的进程号,
而对于子程序,fork函数则返回零。
失败: 返回-1
*/

使用fork创建一个新进程后,由于其基于copy-on-write机制(也就是子进程与父进程初始时只有页表和task structure不同),不会立即将父进程的进程分布复制一份给子进程。而对于父进程在fork前所使用的资源,子进程继承了大部分,如父进程打开的文件描述符,还有部分没有继承。(例如,要是父进程打开了五个文件,那么子进程也有五个打开的文件,而且这些文件的当前读写指针也停在相同的地方。)

复制出来的子进程有自己的task_struct结构和pid,这一步所做的是复制。这样得到的子进程独立于父进程, 具有良好的并发性,但是二者之间的通讯需要通过专门的通讯机制,如: pipe,共享内存等机制, 另外通过fork创建子进程,需要将上面描述的每种资源都复制一个副本。这样看来,fork是一个开销十分大的系统调用,这些开销并不是所有的情况下都是必须的。

子进程从fork后开始执行。

fork之后,子进程继承父进程的信号处理方式。(当一个进程调用fork时,因为子进程在开始时复制父进程的存储映像,信号捕捉函数的地址在子进程中是有意义的,所以子进程继承父进程的信号处理方式。)

关于 子进程 fork 时的 资源继承

父进程创建子进程是为了其能够协助父进程完成某些操作。因此,父进程必须将其自己的一些资源分享给子进程,以便父子进程共同完成任务。

子进程继承了父进程的几乎所有的属性:

  • 实际UID、GID和有效UID,GID、附加GID
  • 调用exec()时的关闭标志
  • UID设置模式比特位、GID设置模式比特位
  • 进程组号、会话ID、控制终端、环境变量
  • 当前工作目录、根目录
  • 文件创建掩码UMASK、文件长度限制ULIMIT
  • 预定值, 如优先级和任何其他的进程预定参数, 根据种类不同决定是否可以继承,一些其它属性

但子进程也有与父进程不同的属性:

  • 进程号,子进程号与任何一个活动的进程组号不同
  • 父进程号
  • 子进程继承父进程的文件描述符或流时, 具有自己的一个拷贝;并且与父进程和其它子进程共享该资源
  • 子进程的用户时间和系统时间被初始化为0
  • 子进程的超时时钟设置为0
  • 子进程不继承父进程的记录锁
  • pending signals 也不会被继承

vfork:

不同于fork,用vfork创建的子进程与父进程共享地址空间,也就是说子进程完全运行在父进程的地址空间上,如果这时子进程修改了某个变量,这将影响到父进程。

vfork创建子进程后,父进程会被阻塞,直到子进程调用exit或exec函数族(exec,将可执行文件载入到地址空间并执行。)

注意: 用vfork()创建的子进程必须显示调用exit()来结束,否则子进程将不能结束,而fork()则不存在这个情况。

#include <sys/types.h>
#include <unistd.h> pid_t vfork(void);

一般而言,如果想调用一个外部程序,则是以vfork来创建一个子进程,通过子进程来调用exec函数组来实现的。

因为exec的系列函数有一个特点:

如果使用了exec函数,那么系统会将被调用的命令装载入当前进程中,当前进程的内容将全部消失取而代之的是调用的命令(相当于进程重生,exec系列函数有关介绍

特点:

1)共享父进程的所有资源

2)父进程在创建完子进程后,等到子进程调用exec函数或者结束之后再运行

3)调用vfork时,其中的代码要求尽可能少(因为容易不稳定)

4)vfork的函数必须以exec或exit结束

clone

系统调用fork()和vfork()是无参数的,而clone()则带有参数。

fork()是全部复制,vfork()是共享内存,而clone() 是则可以将父进程资源有选择地复制给子进程,而没有复制的数据结构则通过指针的复制让子进程共享,具体要复制哪些资源给子进程,由参数列表中的 clone_flags来决定。另外,clone()返回的是子进程的pid。

实际上, Linux clone系统调用是forkpthread_create的通用形式,它允许调用者指定在调用进程和新创建的进程之间共享哪些资源。clone()的主要用途是实现线程:在共享内存空间中并发运行的程序中的多个控制线程。

int clone(int (*fn)(void *), void *child_stack,
int flags, void *arg, ...
/* pid_t *ptid, struct user_desc *tls, pid_t *ctid */ ); /* Prototype for the raw system call */ long clone(unsigned long flags, void *child_stack,
void *ptid, void *ctid,
struct pt_regs *regs);

等待回收子进程的函数

等待回收子进程的函数有2个,waitwaitpid,它们和exit配套使用。wait或waitpid调用只能清理一个子进程,清理多个子进程需要用到循环

wait

等待任意一个子进程,将子进程的返回值填入到wait里面的status中。

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

当子进程调用exit(x),(x会被&0377之后才传给wait)

status:接受子进程的退出状态标志。(如果为空,则表示父进程不关心子进程的终止状态)

如果想获取到正确的exit的返回值,你可以调用WEXITSTATUS(status)来获取,(子进程退出时的返回值被系统处理了,若想得到正常的返回值,那就得调用WEXITSTATUS宏来操作,因为其他位数做为标志位使用了。)

waitpid:

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

**pid: **

>0: 等待id值为pid的子进程

0: 等待所在进程组当中的任一子进程

-1: 等待任一子进程,相当于 wait

<-1: 等待一个指定进程组中的任何子进程,这个进程组的ID等于pid的绝对值。

**status: 接受子进程的退出状态标志: **(同 上文)

**options: **

  • WNOHANG:若由pid指定的子进程未发生状态改变(没有结束),则waitpid()不阻塞,立即返回0
  • WUNTRACED:返回终止子进程信息和因信号停止的子进程信息
  • WCONTINUED:返回收到SIGCONT信号而恢复执行的已停止子进程状态信息

成功返回清理掉的子进程PID;如果非阻塞返回0代表没有子进程退出;如果返回-1则是失败

wait(),waitpid()区别:

  • 在一个子进程终止前,wait使其调用者阻塞,而waitpid有一个选项,可使调用者不阻塞;
  • waitpid()可以控制它所等待的进程;

例程

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h> int main(int argc, char *argv[])
{
pid_t pid = -1;
int ret;
int status; pid = fork();
if(pid == 0) // 子进程入口
{
sleep(2);
printf("Sun p\n");
exit(123);
}else if(pid > 0) // 父进程入口
{
printf("Father p\n");
ret = wait(&status); printf("%d\n", status);
printf("%d\n", WEXITSTATUS(status));
} return 0;
}

守护进程

Linux Daemon(守护进程)是运行在后台的一种特殊进程。它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。

它不需要用户输入就能运行而且提供某种服务,不是对整个系统就是对某个用户程序提供服务。

Linux系统的大多数服务器就是通过守护进程实现的。常见的守护进程包括系统日志进程syslogd 、 web服务器httpd 、 邮件服务器sendmail 和 数据库服务器mysqld等。

守护进程一般在系统启动时开始运行,除非强行终止,否则直到系统关机都保持运行。守护进程经常以超级用户(root)权限运行,因为它们要使用特殊的端口(1-1024)或访问某些特殊的资源。

一个守护进程的父进程是init进程,因为它真正的父进程在fork出子进程后就先于子进程exit退出了,所以它是一个由init继承的孤儿进程。守护进程是非交互式程序,没有控制终端,所以任何输出,无论是向标准输出设备stdout还是标准出错设备stderr的输出都需要特殊处理。

守护进程的名称通常以d结尾,比如sshd、xinetd、crond等

进程组 :

  • 每个进程也属于一个进程组
  • 每个进程主都有一个进程组号,该号等于该进程组组长的PID号 .
  • 一个进程只能为它自己或子进程设置进程组ID号

    会话期:
  • 会话期(session)是一个或多个进程组的集合。

编写守护进程的一般步骤步骤:

1)在父进程中执行fork并exit退出;

2)在子进程中调用setsid函数创建新的会话;

3)在子进程中调用chdir函数,让根目录 ”/” 成为子进程的工作目录;

4)在子进程中调用umask函数,设置进程的umask为0;

5)在子进程中关闭任何不需要的文件描述符

注册退出函数

有三个函数可以正常退出程序:_exit_Exitexit

内核执行程序的唯一方式是exec函数被调用。程序自动终止的唯一方式是_exit或者_Exit被调用,或者程序被动的被信号终止。

#include<stdlib.h>
void _exit(int status);
void _Exit(int status); // The function _Exit() is equivalent to _exit(). #include <unistd.h>
void exit(int status);

exit:退出进程之前先去执行atexit或者是on_exit注册的函数,清理了IO缓冲,关闭该进程使用的所有fd之后才退出进程

在main里面return的时候默认会调用exit这个函数(即:main函数return(0)和exit(0)是一样的。)

_exit:直接退出进程,不清空IO缓冲区

我们重点来看 atexiton_exit

atexit

atexit:注册一个退出函数 (在结束时函数被注册多少次就会被调用多少次,后入先出。)

在一个程序中最多可以用atexit() 注册 ATEXIT_MAX (32)个处理函数, 后注册的函数先执行。

通过fork创建子进程时,它将继承其父进程的注册副本。成功调用exec函数之一后,将删除所有注册。

#include <stdlib.h>

int atexit(void (*function)(void));

参数解析:

  • function:exit后执行的处理函数名,必须是void function(void);的格式

    返回值:成功返回0;失败返回非零。

例程

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h> void bye(void)
{
printf("That was all, folks\n");
} int main(void)
{
long a;
int i; a = sysconf(_SC_ATEXIT_MAX);
printf("ATEXIT_MAX = %ld\n", a); i = atexit(bye);
if (i != 0) {
fprintf(stderr, "cannot set exit function\n");
exit(EXIT_FAILURE);
} exit(EXIT_SUCCESS);
}

on_exit

on_exit:复杂的退出处理函数,用来设置程序正常结束(调用exit() 或从main 中返回)前调用的函数。

函数的作用是:通过exit或从程序的main返回,注册在正常进程终止时要调用的给定函数。函数被传递给上一次调用exit的status参数和on_exit的arg参数。

可以注册多次:每次注册调用一次。后注册的函数先执行。

通过fork创建子进程时,它将继承其父进程的注册副本。成功调用exec函数之一后,将删除所有注册。

#include <stdlib.h>

int on_exit(void (*function)(int , void *), void *arg);

参数解析:

function:exit后执行的处理函数名字

  • 其中里面的int代表 退出的返回值
  • void * 注册时传递的arg,这份数据不会被拷贝,依赖于调用时的情况。

    arg:参数arg指针会传给参数function函数
#include <stdio.h>
#include<stdlib.h>
#include <sys/types.h>
#include <unistd.h> void my_exit(int status, void *myarg)
{
printf("my_exit before exit()!\n");
printf("exit (%d)\n", status);
printf("arg = %s\n", (char*)myarg);
} void my_exit2(int status, void *myarg)
{
printf("my_exit2 before exit()!\n");
printf("exit (%d)\n", status);
printf("arg = %s\n", (char*)myarg);
} int main(int argc, char *argv[])
{
char str[10]="test";
printf("Father's PID is %d\n", getpid());
on_exit(my_exit, (void *)str);
on_exit(my_exit, (void *)str);
if (fork() == 0)
{
// son
printf("Son's PID is %d\n", getpid());
str[0] = 'b';
on_exit(my_exit, (void *)str);
on_exit(my_exit2, (void *)str);
exit(getpid());
}
exit(getpid());
}

Linux 系统编程 学习:01-进程的有关概念 与 创建、回收的更多相关文章

  1. Linux 系统编程 学习:09-线程:线程的创建、回收与取消

    Linux 系统编程 学习:09-线程:线程的创建.回收与取消 背景 我们在此之前完成了 有关进程的学习.从这一讲开始我们学习线程. 完全的开发可以参考:<多线程编程指南> 在Linux ...

  2. Linux 系统编程 学习 总结

    背景 整理了Liunx 关于 进程间通信的 很常见的知识. 目录 与 说明 Linux 系统编程 学习:000-有关概念 介绍了有关的基础概念,为以后的学习打下基础. Linux 系统编程 学习:00 ...

  3. Linux 系统编程 学习:00-有关概念

    Linux 系统编程 学习:00-有关概念 背景 系统编程其实就是利用系统中被支持的调度API进行开发的一个过程. 从这一讲开始,我们来介绍有关Linux 系统编程的学习. 知识 在进行Linux系统 ...

  4. Linux 系统编程 学习:02-进程间通信1:Unix IPC(1)管道

    Linux 系统编程 学习:02-进程间通信1:Unix IPC(1)管道 背景 上一讲我们介绍了创建子进程的方式.我们都知道,创建子进程是为了与父进程协作(或者是为了执行新的程序,参考 Linux ...

  5. Linux 系统编程 学习:03-进程间通信1:Unix IPC(2)信号

    Linux 系统编程 学习:03-进程间通信1:Unix IPC(2)信号 背景 上一讲我们介绍了Unix IPC中的2种管道. 回顾一下上一讲的介绍,IPC的方式通常有: Unix IPC包括:管道 ...

  6. Linux 系统编程 学习:04-进程间通信2:System V IPC(1)

    Linux 系统编程 学习:04-进程间通信2:System V IPC(1) 背景 上一讲 进程间通信:Unix IPC-信号中,我们介绍了Unix IPC中有关信号的概念,以及如何使用. IPC的 ...

  7. Linux 系统编程 学习:05-进程间通信2:System V IPC(2)

    Linux 系统编程 学习:05-进程间通信2:System V IPC(2) 背景 上一讲 进程间通信:System V IPC(1)中,我们介绍了System IPC中有关消息队列.共享内存的概念 ...

  8. Linux 系统编程 学习:06-基于socket的网络编程1:有关概念

    Linux 系统编程 学习:006-基于socket的网络编程1:有关概念 背景 上一讲 进程间通信:System V IPC(2)中,我们介绍了System IPC中关于信号量的概念,以及如何使用. ...

  9. Linux 系统编程 学习:07-基于socket的网络编程2:基于 UDP 的通信

    Linux 系统编程 学习:07-基于socket的网络编程2:基于 UDP 的通信 背景 上一讲我们介绍了网络编程的一些概念.socket的网络编程的有关概念 这一讲我们来看UDP 通信. 知识 U ...

随机推荐

  1. Java 多线程并发编程

    导读 创作不易,禁止转载! 并发编程简介 发展历程 早起计算机,从头到尾执行一个程序,这样就严重造成资源的浪费.然后操作系统就出现了,计算机能运行多个程序,不同的程序在不同的单独的进程中运行,一个进程 ...

  2. Word rings

    Word rings 题目描述 这道题就是想求出所有的环,然后在所有环中比较出环串的平均长度最长的那一个,然后就输出平均长度最长的,如果在一个点当中的样例中没有环的话我们就应该输出"No S ...

  3. 03 Comments in C Programming C编程中的注释

    Comments 注释简介 Let's take a quick break from programming and talk about comments. Comments help progr ...

  4. Noip 2013 真题练习

    Day1 T1 转圈游戏 Link 一句话题意: 让你求 \({x + m \times 10^k} \bmod n\) 的结果. 直接套上快速幂的板子. code #include<iostr ...

  5. SpringBoot 完整学习笔记免费分享

    从0到进阶,完全系统性的学习笔记 每次我都会反复拿来观看,因为我们总会有遗漏忘记的地方,但是笔记不会. 希望大家能好好利用它,以下是笔记截图! 以上只是其中的一项部分,这份笔记可以说含金量超高,绝对会 ...

  6. shell-添加条件测试的多种方法语法介绍与简单实战

    1. 条件测试  1) 测试语句 1) 条件测试语法 在bash的各种流程控制结构中通常要进行各种测试,然后根据测试结果执行不同的操作,有时也会通过与if等条件语句相结合,使我们可以方便的完成判断. ...

  7. 一文看懂Vue3.0的优化

    1.源码优化: a.使用monorepo来管理源码 Vue.js 2.x 的源码托管在 src 目录,然后依据功能拆分出了 compiler(模板编译的相关代码).core(与平台无关的通用运行时代码 ...

  8. MeteoInfoLab脚本示例:中尺度气旋散点图

    全球长时间序列中尺度气旋数据(http://cioss.coas.oregonstate.edu/eddies/)有netCDF格式,散点数据类型,只有一个很大的维Nobs = 2590938.尝试读 ...

  9. iproute2工具

    iproute2工具介绍 iproute2是linux下管理控制TCP/IP网络和流量控制的新一代工具包,出现目的是替代老工具链net-tools.net-tools是通过procfs(/proc)和 ...

  10. 编程福利:50本C语言电子书,你还怕没书看吗!

    推荐适合编程新手入门的几本经典的C语言书籍. 1.<C程序设计语言> C语言之父的著作,被称为C语言的的圣经.全球最经典的C语言教程.这本书最大的特点是精炼.读起来不会觉得"啰嗦 ...