linux学习笔记-读《Linux编程技术详解》-进程与进程环境

进程与进程环境

进程是运行中的程序,每个进程都运行在各自的虚拟地址空间中,某个进程的崩溃不会影响其它进程的运行。进程间的通信(IPC)要通过系统内核(系统调用)来实现。

进程分类

Linux系统中,根据进程的特点,进程可分为3大类:交互进程、批处理进程和守护进程。

l  交互进程:由Shell启动的进程,可在前后台运行,在执行过程中要求与用户进行交互操作;

l  批处理进程:类似于Windows中原来的批处理,是一个进程序列,该进程负责按顺序启动其他进程;

l  守护进程:执行特定功能或者执行系统相关任务的后台进程。只是一个特殊的进程,不是内核组成部分。许多守护进程在系统启动时启动,只到系统关闭时停止运行。而某些只在需要的时候才启动,如FTPApache服务,可在需要时启动。(好像都是废话)

虚拟内存

安装Linux时建立swap分区被用作内存的扩展,系统将暂时不会用到的数据写到交换分区中,在需要时在都会内存,这种内存扩展机制被称为虚拟内存。

Linux中每个进程都运行在虚拟内存空间中。虚拟内存技术用于解决多进程同时运行时内存空间不足的问题,还提供了如下功能:

l  巨大寻址空间:Linux操作系统采用了虚拟内存管理技术,使得在32位系统上支持寻址高达4GB的线性地址空间。用户所看到的进程运行地址为虚拟地址,不是物理地址。4GB内存空间被分为用户空间和内核空间。用户程序在通常情况下只能访问用户空间,只有通过系统调用才能进入内核空间。与用户空间的内容在进程切换会发生变化不同,内核空间是由内核进行映射的,因此不会随着进程切换而发生变化。

l  共享虚拟内存:虚拟内存机制保证了每个进程都运行在自己的虚拟地址空间中,同时虚拟内存还提供了共享内存功能,即进程间通信(IPC)的一种方法,可实现进程间的数据交换。

l  进程保护:系统中的进程运行在自己的虚拟地址空间,保证了进程在运行时的稳定性。

vmstat命令可显示虚拟内存使用情况。

进程内存

所有的进程运行在自己的虚拟地址空间中。而每个进程都有自己的内存地址。

每个进程运行在自己私有的内存空间中(即虚拟地址空间)。在32位系统中,4GB被分为用户空间(0~3GB16进制表示为0xC0000000),内核空间为3GB~4GB

对一个进程而言,都会涉及3种不同的数据段,分别是代码段、数据段和堆栈段。

l  代码段:保存可执行文件操作指令和程序定义的常量。为防运行时代码被修改,只允许读,不能进行修改。多进程能够共享相同的代码段,即当程序被多次执行时,运行的相同程序将共享代码段。

l  数据段:位置紧接代码段,分为初始化代码段和未初始化代码段(也被称为BSS段)。初始化数据段用于存放已经初始化的全局变量和程序的静态变量,而未初始化数据段用于保存未初始化的全局变量。

l  堆栈段:堆栈段中的堆用于存放进程中动态分配的内存地址。如C中的malloc函数,C++中的new函数分配的内存空间将在堆中分配。当使用free(如果内存是通过malloc函数分配)或delete函数释放内存时,分配的内存将从堆中删除。

栈用于保存程序中创建的临时变量,进行数据交换的内存区域。栈大小受操作系统限制,空间有限,而堆大小只是受限于虚拟内存空间,堆位置与数据段相邻。

进程标识

Linux系统使用进程ID(PID)来标识进程。出了init进程(PID1)外,每个进程都有一个父进程。当父进程早于子进程结束时,子进程变为了“孤儿进程”,将被系统进程(init进程)收养。这时,init进程变为子进程的父进程。

每个进程在创建时候,系统会分配一个进程ID给该进程。当进程ID达到系统最大值时(<linux/threads.h>中有PID_MAX的定义,该常量表示系统能使用的最大PID),系统将重新使用最小且当前未使用的PID号。

getpid函数返回当前进程的进程IDgetppid函数返回当前进程的父进程ID

        pid_t getpid(void);

        pid_t getppid(void);

root用户创建新用户时,会给每个用户分配一个用户ID(UID)。可在/etc/passwd文件中查找到用户的UID信息。Linux系统每个进程都有两个IDUIDEUID。当用户登录入系统时,系统会将UIDEUID设置成/etc/passwd文件中的UIDUID用于表示进程的创建者,只有进程的创建者和root用户才有权限对进程进行操作。EUID用于确定进程在任何给定的时刻,对资源和文件具有访问权限。通过使UIDEUID不同,可使某个程序具有超越程序执行者的权限。当设置了EUID,无论哪个用户执行该进程,程序将拥有文件拥有者的权限。

修改EUIDchmod u+s filename;

修改EGIDchmod g+s filename;

 

Linux进程相关系统调用

Linux中创建新进程包括如下步骤:

1)        使用fork函数创建出新的进程;

2)        调用exec函数族修改创建的进程。使用fork创建的进程是当前运行的完全复制。很多时候创建新进程是为了运行新的程序,因此还需使用exec函数族对创建出的进程进行修改,让子进程执行新的程序。

原型:

pid_t fork(void);

返回两次,如果是在父进程中,返回子进程的进程号,如果在子进程中,返回0

vfork函数用于创建新的进程,在创建子进程时,不会复制父进程的相关资源,父子进程共享虚拟内存空间,子进程对虚拟内存空间的修改实际上是在修改父进程虚拟内存空间的内容,在使用vfork函数创建子进程后,父进程会被阻塞,直到子进程调用了exec或者_exit函数退出,子进程不能使用return返回或调用exit函数,但可以调用_exit函数通过共享父进程的地址空间,vfork避免了fork带来的资源复制的消耗。

           pid_t fork(void);

         vfork的出现促进了fork机制的改变。目前,Linux系统实现fork系统调用采用了“写操作时复制”(copy-on-write, COW)的方法。COW是一种延迟资源复制的方法,子进程在创建的时候并不复制父进程的相关资源,父子进程通过访问相同的物理内存来伪装已经实现了对资源的复制。这种共享是只读方式,这一点与vfork是不同的。当子进程对内存数据存在写入操作的时候,才会进行资源的复制。COW机制使得vfork几乎没存在必要,处于兼容,系统提供了改接口,但vfork对内存的修改会影响父进程。

         Linux提供了exec函数族用于对创建新进程中的数据段、代码段和堆栈段进行替换。进程调用exec函数族中的函数后,其代码段会被替换成新的代码段。同时,获得新程序的数据段和堆栈段,进程的进程号保持不变,exec函数族中的函数定义包括:

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 execve(const char *filename, char *const argv[], char *const envp[]);

         int execvp(const char *file, char *const argv[]);

 

exit_exit函数与return不同

exit函数和_exit函数可以用在子程序中。程序在执行到两个函数处,会自动结束并跳回到操作系统中。也可用在main函数中,但不考虑返回类型,也就是说main类型可以为void或任意类型。

调用exit函数结束进程,并将(statu & 0377)获得的结果返回给父进程。使用exit函数结束进程时,会首先执行使用atexit函数注册的函数,然后调用on_exit函数中清理动作。函数执行顺序与注册的顺序相反。

参数status为退出进程时的状态,父进程将获得该状态值。C语言标准指定了EXIT_SUCCESSEXIT_FAILURE作为程序正常结束和异常中止的两个宏。

atexit注册进程退出时的处理函数:

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

当程序中使用atexit注册了进程退出时的处理函数,当调用return exit退出时会自动调用atexit注册的处理函数。

exit函数将立即结束调用它的进程,与进程相关的任何打开的文件描述符都将关闭,进程的子进程都将变为init进程(进程号为1)的子进程。

_exit退出程序时,不会执行atexit中注册的处理函数。

exit_exit函数的区别:

l  exit函数是在ANSI C中说明的,而_exit函数是在POSIX标准中说明的

l  exit函数将终止调用进程,在退出程序之前所有文件关闭,标准输入输出的缓冲区被清空,并执行在atexit注册的回调函数;_exit终止调用进程,但不关闭文件,不清除标准输入输出的缓冲区,也不调用在atexit注册的回调函数。(fork创建的子进程中一般尽量不使用exit函数)

wait函数

为同步父子进程,父进程等待子进程结束,需要用到wait函数。如下为wait函数工作流程示意图:

 linux学习笔记-读《Linux编程技术详解》-进程与进程环境_第1张图片

 

           pid_t wait(int *status);

wait系统调用将挂起当前进程,直到子进程结束。wait(&status)等同于使用

waitpid(-1, &status, 0)。当所有的子进程结束时,wait函数返回最后结束的子进程的PID和结束状态。

使用KILL函数发送信号

“kill -l”命令可以查看这些信号的具体情况。使用kill命令可以给指定进程发送信息,命令格式:

kill -信号类型 进程号

发送的信号有可能被进程捕获,而不会起到默认作用。为解决这一问题,Linux提供了SIGKILL信号,这个信号是不会被进程捕获的。即如要确保杀死某个进程,可以使用kill -SIGKILL 进程号。也可在程序中使用kill函数实现相同功能:

           int kill(pid_t, int sig);

waitpid函数用于根据进程号等待指定的进程,功能强于wait

           pid_t waitpid(pid_t pid, int *status, int options);

 

僵尸进程

僵尸进程(Zombie Process)是由于子进程退出后父进程没有使用wait函数收集子进程状态而产生的。僵尸进程是一个已经结束了的进程,不占用系统的内存资源,对CPU时间几乎没有任何的影响,只是会占用进程表中的位置。由于进程表空间有限,Linux系统中产生的进程数是有限的,过多的僵尸进程会影响新进程的产生。

僵尸进程的存在是为了给程序员和系统管理人员提供一些重要的进程信息。这些信息包括进程如何结束,在运行过程中是否出现了错误等。如果没有僵尸进程,这新信息将随着进程的结束而消失。因此,僵尸进程携带的信息对于程序开发人员和系统管理员发现程序存在的问题有非常重要意义。

Linux系统中,每个进程都存在父进程。如果父进程被杀死,子进程将变为孤儿进程被init进程管理。如果子进程为僵尸进程的话,init进程将负责相应的清理工作。也就是说,只要杀死僵尸进程的父进程,将可以清除僵尸进程。

你可能感兴趣的:(编程,linux,kill,扩展,Path,程序开发)