一、Linux进程概述
进程是一个程序一次执行的过程,是操作系统动态执行的基本单元。
进程的概念主要有两点:
第一,进程是一个实体。
每个进程都有自己的虚拟地址空间,包括文本区、数据区、和堆栈区。
文本区域存储处理器执行的代码;
数据区存储变量和动态分配的内存;
堆栈区存储着活动进程调用的指令和本地变量。
第二,进程是一个“执行中的程序”。
它和程序有本质区别。
程序是静态的,它是一些保存在磁盘上的指令的有序集合;
而进程是一个动态的概念,它是一个运行着的程序,包含了进程的动态创建、调度和消亡
的过程,是 Linux 的基本调度单位。
只有当处理器赋予程序生命时,它才能成为一个活动的实体,称之为进程。
内核的调度器负责在所有的进程间分配 CPU 执行时间,称为时间片(time slice),它轮流在每个进程分得的时间片用完后从进程那里抢回控制权。
1、进程标识
OS会为每个进程分配一个唯一的整型 ID,做为进程的标识号(PID)。查看进程命令为:ps -ef
进程 0 是调度进程,常被成为交换进程,它不执行任何程序,是内核的一部分,因此也被称为系统进程。
进程除了自身的ID外,还有父进程ID(PPID)。
也就是说每个进程都必须有它的父进程,操作系统不会无缘无故产生一个新进程。
所有进程的祖先进程是同一个进程,它叫做 init 进程,ID为1,init进程是内核自举后的第一个启动的进程。init 进程负责引导系统、启动守护(后台)进程并且运行必要的程序。它不是系统进程,但它以系统的超级用户特权运行。
2、进程的运行身份
进程在运行过程中,必须具有一类似于用户的身份,以便进行进程的权限控制。
缺省情况下,哪个登录用户运行程序,该程序进程就具有该用户的身份。
1)进程真实的用户ID和组ID
假设当前登录用户为gotter,他运行了ls程序,则ls在运行过程中就具有 gotter 的身份,该ls进程的用户ID和组ID分别为gotter和gotter所属的组。
这类型的ID叫做进程的真实用户ID和真实组ID。
真实用户ID和真实组 ID可以通过函数 getuid()和 getgid()获得。
2)进程有效用户ID和有效组ID
与真实ID对应,进程还具有有效用户ID和有效组ID的属性,内核对进程的访问权限检查时,它检查的是进程的有效用户ID和有效组ID,而不是真实用户ID和真实组ID。
缺省情况下,用户的(有效用户ID和有效组ID)与(真实用户 ID 和真实组 ID)是相同的。
有效用户id和有效组id通过函数 geteuid()和 getegid()获得。
实例:
1. #include <stdio.h> 2. #include <stdlib.h> 3. #include <unistd.h> 4. 5. int main() 6. { 7. printf("uid:%d gid:%d euid:%d egid:%d\n", 8. getuid(),getgid(),geteuid(),getegid()); 9. return 0; 10. }
测试:可以使用id命令查看用户gid,uid和组
[root@localhost process]# id
uid=0(root) gid=0(root) groups=0(root)……
[root@localhost process]#
编译生成可执行文件 main.out,程序文件的属性可能为:
[root@localhost process]# ls -l main.out
-rwxr-xr-x 1 root root 5133 11月 16 21:42 main.out
执行结果可能为:
[root@localhost process]# gcc -o main.out main.c
[root@localhost process]# ./main.out
uid:0 gid:0 euid:0 egid:0
[root@localhost process]#
现在将 main.out 的所有者可执行属性改为s 。
[root@localhost process]# chmod u+s main.out
[root@localhost process]# ls -l
-rwsr-xr-x 1 root root 5133 11月 16 21:42 a.out
可以看到,进程的有效用户身份变为了root,而不是cxx 了。
这是因为文件main.out的访问权限的所有者可执行为设置了s的属性,设置了该属性以后,用户运行main.out 时,main.out进程的有效用户身份将不再是运行main.out的用户,而是main.out文件的所有者。
问题:怎么查看正在执行的main函数进程?
需要在main.c中添加while(1)循环,然后重新打开一个终端(快捷键:ctrl+shift+t),然后输入ps -ef命令即可查看。
3、进程的状态
进程是程序的执行过程,根据它的生命周期可以划分成 3 种状态;
- 执行态:该进程正在运行,即进程正在占用 CPU。
- 就绪态:进程已经具备执行的一切条件,正在等待分配 CPU 的处理时间片。
- 等待态:进程不能使用 CPU,若等待事件发生(等待的资源分配到)则可将其唤醒。
4、Linux下进程的结果及管理
Linux 系统是一个多进程的系统,它的进程之间具有并行性、互不干扰等特点。也就是说,进程之间是分离的任务,拥有各自的权利和责任。
其中,每个进程都运行在各自独立的虚拟地址空间,因此,即使一个进程发生了异常,它也不会影响到系统的其他进程。
Linux中的进程包含以下几个部分:
1)“数据段”:
放全局变量、常数以及动态数据分配的数据空间。数据段分成普通数据段(包括可读可写/只读数据段,存放静态初始化的全局变量或常量)、 BSS 数据段(存放未初始化的全局变量)以及堆(存放动态分配的数据)。
- “正文段”:
存放的是 CPU 执行的机器指令部分。
- “堆栈段”:
存放的是子程序的返回地址、子程序的参数以及程序的局部变量等
5、一些进程相关信息&相关命令
进程process:
是OS的最小单元,地址空间大小为4G(0 ~ 4G-1),其中1G给OS,3G给进程(进程的可寻址空间){代码区 数据区 堆栈}。
进程相关命令:
二、Linux进程创建与控制
1、fork进程创建函数
原型:
1. #include <unistd.h> 2. pid_t fork(void); 3. pid_t vfork(void);
在 linux 中fork函数是非常重要的函数,它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。
返回值说明:
它执行一次返回两个值(一般的函数只有1个返回值)。
其中父进程的返回值是子进程的进程号。
而子进程的返回值为 0。
若出错则返回-1。
因此可以通过返回值来判断是父进程还是子进程。
fork函数创建子进程的过程说明:
1)使用 fork 函数得到的子进程是父进程的一个复制品。
它从父进程继承了进程的地址空间,包括进程上下文、进程堆栈、内存信息、打开的文件描述符、信号控制设定、进程优先级、进程组号、当前工作目录、根目录、资源限制、控制终端,而子进程所独有的只有它的进程号、资源使用和计时器等。
2)通过这种复制方式创建出子进程后,原有进程和子进程都从函数fork返回,各自继续往下运行。
也就是说, fork不仅仅复制了进程的资源,更复制了原有进程的运行状态。所以复制出的新的进程(子进程)虽然什么都和父进程一样,但它从 fork 函数后面开始运行。
3)但是原进程的 fork 返回值与子进程的 fork 返回值不同。
在原进程中,fork返回子进程的pid;
而在子进程中,fork返回 0;
如果fork返回负值,表示创建子进程失败。
- vfork 函数和fork函数:
相同点:它的作用和返回值与 fork 相同,二者都创建一个子进程。
不同点:
(1)vfork函数并不是将父进程的地址空间完全复制到子进程中,因为子进程会立即调用exec(或 exit),所以也就不会存放该地址空间。
(2)vfork 保证子进程先比父进程先运行,在它调用exec 或 exit之后父进程才可能被调度运行。
(3) fork:子进程拷贝父进程的数据段,父子进程执行次序不能确定;
vfork: 子进程与父进程共享数据段,子进程先运行,父进程后运行;
实例:
1. #include <stdio.h> 2. #include <unistd.h> 3. int main() 4. { 5. fork(); 6. fork(); 7. fork(); 8. printf("Hello World\n"); 9. return 0; 10. } 11. 自己运行上面的程序看看结果是什么,并思考为什么会这样。 12. 如果有多个 fork()呢?并输出其返回值。 13. 14. 示例 2: 15. #include <stdio.h> 16. #include <unistd.h> 17. //int m=100; //数据段 18. 19. int main() 20. { 21. int m = 100; //堆栈段 22. printf("aaaa\n"); 23. int n = fork();//把fork()改为vfork();后再次验证结果 24. if(n > 0){ 25. //wait(); 26. m++; 27. printf("bbbb n = %d m = %d\n", n, m); 28. } 29. else{ 30. printf("cccc n = %d m = %d\n", n, m); 31. } 32. return 0; 33. //注意看m n的值 34. }
用fork继承父进程打开的文件
1. #include <stdio.h> 2. #include <fcntl.h> 3. #include <unistd.h> 4. #include <sys/types.h> 5. #include <sys/stat.h> 6. 7. int main() 8. { 9. char szBuf[32] = {'\0'}; 10. int iFile = open("./a.txt", O_RDONLY); 11. //父进程 12. if(fork() > 0){ 13. close(iFile); 14. return 0; 15. } 16. //子进程 17. sleep(3); //wait for parent process closing fd 18. read(iFile, szBuf, sizeof(szBuf)-1); 19. printf("string:%s\n",szBuf); 20. close(iFile); 21. return 0; 22. }
2、进程的终止
进程的终止有 5 种方式:
1)main函数的自然返回(return 0)。
2)调用exit函数。
3)调用_exit函数。
4)接收到某个信号。如 ctrl+c SIGINT ctrl+\ SIGQUIT
5)调用 abort 函数,它产生SIGABRT信号。所以是上一种方式的特例。
前 3 种方式为正常的终止,后 2 种为非正常终止。
但是无论哪种方式,进程终止时都将执行相同的关闭打开的文件,释放占用的内存等。
只是后两种终止会导致程序有些代码不会正常的执行,比如对象的析构、atexit函数的执行等。
1)exit 和_exit 函数说明
exit 和_exit 函数都是用来终止进程的。
当程序执行到 exit 和_exit 时,进程会无条件的停止剩下的所有操作,清除包括PCB(进程控制块)在内的各种数据结构,并终止本程序的运行。
exit 函数和_exit 函数的最大区别在于exit函数在退出之前会检查文件的打开情况,把文件缓冲区中的内容写回文件,就是图中的“清理 I/O 缓冲”.
2)什么是缓冲I/O?
由于linux 的标准函数库中,有一种被称作“缓冲 I/O”操作,其特征就是对应每一个打开的文件,在内存中都有一片缓冲区。
每次读文件时,会连续读出若干条记录,这样在下次读文件时就可以直接从内存的缓冲区中读取;
同样,每次写文件的时候,也仅仅是写入内存中的缓冲区,等满足一定的条件(如达到一定数量或遇到特定字符等),再将缓冲区中的内容一次性写入文件。
这种技术大大增加了文件读写的速度,但也为编程带来了麻烦。
比如有一些数据,认为已经写入文件,实际上因为没有满足特定的条件,它们还只是保存在缓冲区内,这时用_exit 函数直接将进程关闭,缓冲区中的数据就会丢失。
因此,如想保证数据的完整性,建议使用 exit 函数。
3)exit 和_exit函数原型
1. #include <stdlib.h> //exit 的头文件 2. #include <unistd.h> //_exit 的头文件 3. void exit(int status); 4. void _exit(int status);
status 是一个整型的参数,可以利用这个参数传递进程结束时的状态。
一般来说, 0 表示正常结束;其他的数值表示出现了错误,进程非正常结束。
实例:
1. #include <stdio.h> 2. #include <stdlib.h> 3. int main() 4. { 5. printf("hello\n"); 6. printf("world"); 7. exit(0); 8. }
可以发现,调用exit 函数,缓冲区中的记录也能正常输出。
3、wait和waitpid函数
用fork函数启动一个子进程时,子进程就有了它自己的生命并将独立运行。
1)孤儿进程
如果父进程先于子进程退出,则子进程成为孤儿进程。此时将自动被PID为 1 的进程(即 init进程)接管。
孤儿进程退出后,它的清理工作由祖先进程 init 自动处理。但在init进程清理子进程之前,它一直消耗系统的资源,所以要尽量避免。
编写一个孤儿进程
1. #include <stdio.h> 2. #include <stdlib.h> 3. #include <unistd.h> 4. int main(void) 5. { 6. pid_t pid = fork(); 7. if( pid == 0){ 8. printf("子进程…\n"); 9. while(1) ; 10. } 11. else{ 12. printf("父进程 8 秒后退出…\n"); 13. sleep(8); 14. printf("父进程退出\n"); 15. exit(10); 16. } 17. }
2)僵尸进程 & wait和waitpid函数
如果子进程先退出,系统不会自动清理掉子进程的环境,而必须由父进程调用 wait 或 waitpid 函数来完成清理工作。
如果父进程不做清理工作,则已经退出的子进程将成为僵尸进程(defunct)。
在系统中如果存在的僵尸(zombie)进程过多,将会影响系统的性能,所以必须对僵尸进程进行处理。
编写一个僵尸进程
1. #include <stdio.h> 2. #include <stdlib.h> 3. #include <unistd.h> 4. 5. int main() 6. { 7. pid_t pid = fork(); 8. if( pid == 0 ){ 9. exit(10); 10. } 11. else{ 12. sleep(10); 13. //while(1); 14. } 15. }
通过用 ps –aux 快速查看发现Z的僵尸进程(要加while(1)的时候,用CTRL+Z退出)。
如何避免大量僵尸进程
- 方法一:改写父进程,调用waitpid()函数回收子进程资源。
- 方法二:杀死父进程,让子进程形成孤儿进程,由init进程接管。如在终端中输入命令
- kill -9 5166表示杀死父进程ID为5166的进程。
- 方法三:关闭终端
- 方法四:重启系统
4、exec函数族
exec*由一组函数组成:
1. extern char **environ; 2. 3. int execl(const char *path, const char *arg, ...); 4. int execlp(const char *file, const char *arg, ...); 5. 6. int execv(const char *path, char *const argv[]); 7. int execvp(const char *file, char *const argv[]); 8. 9. int execle(const char *path, const char *arg , ..., char * const envp[]); 10. int execve(const char * path, char *const argv[], char *const envp[]);
exec函数族的作用是运行第一个参数指定的可执行程序。
exec不会创建一个新的进程,只是把原有的代码段、数据段替换,进程ID没有变。
exec 函数族提供了一个在进程中启动另一个程序执行的方法。
exec 函数族的作用是根据指定的文件名找到可执行文件,并用它来取代调用进程的内容,换句话说,就是在调用进程内部执行一个可执行文件。
exec与fork的区别:
这些函数族的工作过程与fork完全不同
exec*参数说明:
exec函数族的参数传递有两种方式:
1)逐个列举的方式,
2)将所有参数整体构造指针数组传递。
函数名有标识的字母来区分参数传递方式:
注意:
【1】对于有参数 envp 的函数,它会使用程序员自定义的环境变量。
【2】如果自定义的环境变量中包含了将要执行的可执行程序的路径,那么第一个参数中是不是我们就可以不用写全路径了呢?不是的,必须写全路径。
因为我们自定义的环境变量不是用来寻找这个可执行程序的,而是在这个可执行程序运行起来之后给新进程用的。
【3】可以用 env 命令查看环境变量
execl实例:
1. #include <unistd.h> 2. #include <stdio.h> 3. 4. int main() 5. { 6. printf("aaaa\n"); 7. execl("/bin/ls", "ls", "-l", NULL); //执行 ls -l指令 8. printf("bbbb\n"); 9. printf("bbbb\n"); 10. return 0; 11. }
5、system函数
函数如下:
实例:
1. #include <stdio.h> 2. #include <stdlib.h> 3. 4. int main() 5. { 6. //system("ls -l"); //执行命令 7. system("clear"); //表示清屏 8. return 0; 9. }
三、守护进程
1、概念
Daemon 运行在后台也称作“后台服务进程”。它是没有控制终端与之相连的进程。它独立于控制终端,通常周期的执行某种任务。
那么为什么守护进程要脱离终端后台运行呢?
守护进程脱离终端是为了避免进程在执行过程中的信息在任何终端上显示并且进程也不会被任何终端所产生的任何终端信息所打断。
那么为什么要引入守护进程呢?
由于在linux中,每一个系统与用户进行交流的界面称为终端,每一个从此终端开始运行的进程都会依赖这个终端,这个终端就称为这些进程的控制终端。
当控制终端被关闭时,相应的进程都会自动关闭。但是守护进程却能突破这种限制,它被执行开始运转,直到整个系统关闭时才退出。
几乎所有的服务器程序都用 daemon进程的形式实现,如:Apache 和wu-FTP,。
很多Linux下常见的命令,如inetd和ftpd,末尾的字母 d通常就是指 daemon
2、进程守护的特性
1)守护进程最重要的特性是后台运行。
2)其次,守护进程必须与其运行前的环境隔离开来。这些环境包括未关闭的文件描述符、控制终端、会话和进程组、工作目录已经文件创建掩码等。
这些环境通常是守护进程从父进程那里继承下来的。
3)守护进程的启动方式。
3、daemon进程的编程规则
1)创建子进程,父进程退出
调用 fork产生一个子进程,同时父进程退出。
我们所有后续工作都在子进程中完成。这样做我们可以交出控制台的控制权,并为子进程作为进程组长作准备;由于父进程已经先于子进程退出,会造成子进程没有父进程,变成一个孤儿进程( orphan)。每当系统发现一个孤儿进程,就会自动由 1 号进程收养它,这样,原先的子进程就会变成1号进程的子进程。
2)在子进程中创建新会话
使用系统函数 setsid()。
由于创建守护进程的第一步调用了 fork 函数来创建子进程,再将父进程退出。
由于在调用 fork 函数的时候,子进程全盘拷贝了父进程的会话期、进程组、控制终端等,虽然父进程退出了,但会话期、进程组、控制终端并没有改变。
因此,还不是真正意义上的独立开来。
而调用 setsid 函数会创建一个新的会话并自任该会话的组长。
调用 setsid 函数有下面3个作用:
A.让进程摆脱原会话的控制,
B.让进程摆脱原进程组的控制,
C.让进程摆脱原控制终端的控制;
进程组:是一个或多个进程的集合。
进程组有进程组 ID 来唯一标识。
除了进程号(PID)之外,进程组 ID(GID)也是一个进程的必备属性。
每个进程都有一个组长进程,其组长进程的进程号等于进程组ID。且该进程组ID不会因为组长进程的退出而受影响。
会话周期:会话期是一个或多个进程组的集合。
通常,一个会话开始于用户登录,终止于用户退出,在此期间该用户运行的所有进程都属于这个会话期。
控制终端:由于在linux中,每一个系统与用户进行交流的界面称为终端,每一个从此终端开始运行的进程都会依赖这个控制终端。
3)改变当前目录为根目录
使用 fork 函数创建的子进程继承了父进程的当前工作目录。
由于在进程运行中,当前目录所在的文件是不能卸载的,这对以后的使用会造成很多的不便。可以利用 chdir("/");把当前工作目录切换到根目录。
4)重设文件权限掩码
umask(0);-->将文件权限掩码设为 0,Deamon 创建文件不会有太大麻烦。
5)关闭所有不需要的文件描述符
新进程会从父进程那里继承一些已经打开了的文件。这些被打开的文件可能永远不会被守护进程读写,而它们一直消耗系统资源。
另外守护进程已经与所属的终端失去联系,那么从终端输入的字符不可能到达守护进程,守护进程中常规方法(如 printf)输出的字符也不可能在终端上显示。所以通常关闭从 0到 MAXFILE 的所有文件描述符。
1. #include <stdio.h> 2. #include <stdlib.h> 3. #include <unistd.h> 4. #include <sys/stat.h> 5. 6. void Daemon() 7. { 8. const int MAXFD=64; 9. int i=0; 10. 11. if(fork()!=0) //父进程退出 12. exit(0); 13. setsid(); //成为新进程组组长和新会话领导,脱离控制终端 14. chdir("/"); //设置工作目录为根目录 15. umask(0); //重设文件访问权限掩码 16. for(;i<MAXFD;i++) //尽可能关闭所有从父进程继承来的文件 17. close(i); 18. } 19. int main() 20. { 21. Daemon(); //成为守护进程 22. while(1){ 23. sleep(1); 24. } 25. return 0; 26. }
4、daemon库函数
原型:
#include <unistd.h>
int daemon(int nochdir, int noclose);
功能:创建一个守护进程.
参数说明:
nochdir:=0将当前目录更改至“/”
noclose:=0将标准输入、标准输出、标准错误重定向至“/dev/null”
返回值说明:
成功:0
失败:-1