课程目标
(1) 掌握进程的基本概念,进程属性获取。
(2) 掌握进程的生命周期以及资源申请与释放的过程。
(3) 掌握创建新进程时父子进程资源的管理。
(4) 掌握守候进程等特殊进程的管理。
(5)多进程在编程中的应用。
主要知识点
(1) 程序,进程,进程资源。
(2) 进程的生命周期,进程状态。
(3) 进程创建与父子进程资源。
(4)进程属性管理与进程应用。
1. 程序、进程、进程属性与进程状态
- 进程是unix/Linux中基本的资源管理单元。
- 进程又是执行的代码片段(一个进程可能创建多个进程,一个进程可以执行多个程序的代码)。将一个代码片段加载到内存并让其执行也就是创建一个(或多个)进程。
- 进程与程序的关系:程序存储在磁盘上,是一个文件;而进程是一个加载到内存执行的程序段,且有生命周期,创建、执行、退出、等待的状态
- 一个进程不仅仅占用了加载代码的内存(用户空间),在Linux下,使用
task_struct
这个结构体来维护整个进程的资源(内核空间) - 在内核中
task_struct
完整的描述了一个进程的所有信息:
volatile long state; /* -1 unrunnable, 0 runnable, >0 stopped */ 描述了状态信息
struct mm_struct *mm, *active_mm; 与内存存储相关信息,描述了在用户空间中的代码段,数据段,堆, 栈, mmap等涉及的所有内存信息
/* open file information */
struct files_struct *files; 打开文件的信息列表,成员fd_array[NR_OPEN_DEFAULT]就是描述了这个进程打开的文件列表。
/* signal handlers */
struct signal_struct *signal; 信号的描述信息
struct sighand_struct *sighand; 信号的描述信息
- 应用编程时产看进程
ps aux
- 用户空间的进程属性:
PID:进程号,每个进程有唯一的编号
状态:如下描述
Linux系统是一个多任务多用户的系统。为什么会用多进程,意义何在?
现在CPU的速度非常快,而人的反映时间时微秒级,系统的外设的速度的速度也相对比较慢。如果使用单进程任务系统,则CPU有大量空闲。因此,我们让CPU在各个执行单元中不停的调转。当某个进程需要等待其他的资源时,CPU转而执行其它的进程,充分利用了CPU资源。在什么时候执行哪一个进程实质是由调度算法来决定的。
CPU只有一个,而进程有多个,因此某个时刻只能执行一个进程,其他的进程则处于相应的其他状态:
运行状态:占有资源,执行;
就绪状态:除了CPU资源外,其他资源都已获取,等待调度算法来执行;
等待状态:除了CPU资源外,还需要等待其他资源或时间,分为可中断等待(可以被信号打断)和不可中断等待
停止状态:正在被跟踪或者调试的进程
僵死状态:用户资源已经收回,PCB内存资源没有收回,已经不能执行
怎么来划分多个进程呢?
进程是资源管理的基本单元,创建进程时,一个比较独立的任务(事务)创建一个进程,这个事务尽量不与其他进程有太多的耦合性。
2. 进程管理应用及资源
(1)进程创建:进程创建于进程资源获取。fork/vfork
在进程创建过程中:
用户空间中:将程序的代码段,数据段,BBS段,从磁盘加载到内存,并且申请堆栈空间;
内核空间中:为这个进程分配唯一的PID标识,同时在内核中为这个进程申请进程控制块PCB,初始化相关信息
在运行的过程中,涉及打开的文件,关联的终端,安装信号,状态等系列信息
在创建进程时,由父进程来创建子进程
#include <unistd.h>
pid_t fork(void);
在父进程中,返回子进程的ID,在子进程中返回,返回0
(2)进程中执行新的代码
在进程执行过程中,要执行新代码,实际上是创建了一个新进程,更多的是期望在这个进程中执行新的代码,而不是原来的程序代码。使用exec
相关的函数可以在子进程中,替换原有进程的代码段和数据段(用户空间的信息),转而执行新代码。
#include <unistd.h>
extern char **environ;
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 execvp(const char *file, char *const argv[]);
参数说明:
path:可执行程序的路径
arg:参数列表(以NULL结束)
file:文件名(要求系统在$PATH环境变量所列路径下搜索)
argv[]:执行这个程序的参数列表字符串指针数组
用法参考:
execl("/bin/ls", "ls", "-l", NULL);
execlp("ls", "ls", "-l", NULL);
char *const argv[] = {"ls", "-l", NULL};
execv("/bin/ls", argv);
(3)进程退出
进程在以下几个情况会退出:
- 使用
kill
函数kill -9 pid
:强制退出 - 显示的执行
exit
系列函数
NAME
_exit, _Exit - terminate the calling process
SYNOPSIS
#include <unistd.h>
void _exit(int status);
#include <stdlib.h>
void _Exit(int status);
NAME
exit - cause normal process termination
SYNOPSIS
#include <stdlib.h>
void exit(int status);
exit()与_exit()的区别:
exit()在退出时会对进程资源做清理,例如刷新流流的缓冲区;
_exit()不做任何处理,直接退出。
- 进程遇到
main
函数的return
或者遇到}
没有代码可执行时退出
exit
与return
的区别:
exit
是一个系统函数,退出这个进程;
return
是C/C++关键字,退出这个函数。
- 在退出时,我们可以在退出前注册退出时执行的代码
NAME
atexit - register a function to be called at normal process termination
SYNOPSIS
#include <stdlib.h>
int atexit(void (*function)(void));
NAME
on_exit - register a function to be called at normal process termination
SYNOPSIS
#include <stdlib.h>
int on_exit(void (*function)(int , void *), void *arg);
注册回调函数,即执行exit()
或者正常退出时去执行相应的回调函数代码,实际上是提供一个功能,在退出进程时完成相应的进程资源清理工作,相当于C++中的析构。
(4)进程资源回收
进程退出有退出的状态,且进程资源的回收在exit
的时候仅仅释放了它的用户空间资源,而内核空间资源PCB没有回收,转而由它的父进程通过wait
相关的函数来回收。
子进程在退出时会给父亲进程发出一个信号(SIGCHLD),父进程可以显示的调用wait/waitpid
等待子进程结束并回收资源,回收资源时,也可以得到子进程退出的状态。
NAME
wait, waitpid, waitid - wait for process to change state
SYNOPSIS
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *status);
参数status用来存储子进程退出状态,返回值为退出的子进程PID,
这个函数以阻塞的方式等待某个进程退出,当进程退出后,此函数返回。
pid_t waitpid(pid_t pid, int *status, int options);
此函数为指定等待某个进程或者某些进程,其中第一个参数可以为以下值:
< -1 meaning wait for any child process whose process group ID is equal to the absolute value of pid.
-1 meaning wait for any child process.
0 meaning wait for any child process whose process group ID is equal to that of the calling process.
> 0 meaning wait for the child whose process ID is equal to the value of pid.
第二个参数status用来存储子进程退出状态
第三个参数options一般为0
使用示例:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
pid_t pid = fork();
if( pid < 0 )
{
perror("fork");
}
else if( pid == 0 )
{
sleep(1);
printf("child ID = %d\n", getpid());
exit(10);
}
else
{
int stat;
pid_t w_pid;
// w_pid = wait(&stat);
w_pid = waitpid(-1, &stat, 0);
printf("status = %d, wait_pid = %d\n", stat>>8, w_pid);
}
return 0;
}
总结资源申请与释放的问题:
创建时,申请进程的所有资源。内核空间中的PCB,用户空间中的加载了代码段,数据段,BSS段,申请了堆栈空间,打开的文件,安装的信号,关联的终端等;
执行时,替代代码段,数据段,BSS段,堆栈空间等用户空间信息,但内核PCB的信号没有修改;
退出时,释放了自己用户空间的资源
回收时,回收内核空间资源。
两个重要的概念:
- 僵死进程:进程已经退出,但是内核空间资源没有回收的进程
- 孤儿进程:父进程先于子进程退出,这样的子进程就是孤儿进程,其父进程会被转移到
init
(pid=1)进程
3. 进程创建详解与父子进程资源
(1)父子执行顺序问题
父子进程在创建完子进程后互不相关联,以独立身份抢占CPU资源,具体谁先执行由调度算法决定,用户空间没办法干预。子进程执行代码的位置是fork/vfork
函数返回的位置。
(2)子进程资源申请问题
子进程重新申请新的物理内存空间,复制父进程地址空间所有的信息(现在的操作系统实际采用写时复制等策略,真正的物理内存空间发生在需要写入时)
子进程在用户空间中复制父进程的代码段,数据段,BSS段,堆,栈所有的信息,在内核空间中操作系统为其重新申请一个PCB,并且使用父进程的PCB来初始化,除了pid特殊信息外,几乎所有的信息都是一样的。
- 父子进程中资源申请问题
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int glob = 100;
int main()
{
pid_t pid = fork();
int num = 20;
if( pid < 0 )
{
perror("fork");
exit(EXIT_FAILURE);
}
else if( pid == 0 )
{
num = 1000;
glob = 2000;
printf("child process: num = %d, glob =%d\n", num, glob); // child process: num = 1000, glob =2000
printf("child process: &num = %p, &glob =%p\n", &num, &glob); // child process: &num = 0xbfe94ff8, &glob =0x804a028
}
else
{
sleep(1);
printf("parent process: num = %d, glob =%d\n", num, glob); // parent process: num = 20, glob =100
printf("parent process: &num = %p, &glob =%p\n", &num, &glob); // parent process: &num = 0xbfe94ff8, &glob =0x804a028
}
return 0;
}
观察输出结果:
在子进程中先修改变量的值,并不影响父进程,明数据段,栈(当然也包括其它用户空间内存),子进程是申请新的物理空间;
但从打印的地址来看,父子进程中的变量地址为同一个地址,这是为什么?
这里打印的是虚拟地址,而不是物理地址编号;两个进程的虚拟地址空间是没有任何联系的。
- 父子进程中文件流的缓冲区状态
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
printf("hello\nworld");
fork();
printf("bye\n");
return 0;
}
===============
输出结果:
hello
worldbye
worldbye
流的缓冲区会缓存没有刷新的信息,且缓冲区在用户空间中,虽然子进程创建后从fork
返回处执行,但缓冲区被子进程复制了一份,这里存储在缓冲区中的world也被复制了一份,因此,输出了两份world。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
for(int i=0; i<2; i++)
{
fork();
printf("*");
}
return 0;
}
输出结果:
********
4.子进程创建或执行execX后,对打开的文件的操作方式
根据前面所学的内容,在同一个进程中,两次打开(使用open
)同一个文件(只要没有对文件上锁),分别写入文件会存在覆盖的情况。
而使用fcntl/dup
复制文件描述,分别使用这两个文件描述符写文件并不会出现覆盖,而是交叉写入。
原因:两次open
打开实际上在内核中创建了两个互不相关的文件表项(struct file),也就记录了两个读写位置。而复制文件描述符则在内核中使用同一个文件表项,因此,共用以一个读写位置。
- 在子进程中是如何操作父进程中打开的文件?
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main()
{
int fd;
int fd2;
fd = open("test.txt", O_CREAT|O_RDWR);
if( fd < 0 )
{
perror("open");
}
pid_t pid = fork();
if( pid == -1 )
{
perror("fork");
exit(EXIT_FAILURE);
}
else if(pid == 0)
{
write(fd, "helloworld", 10);
}
else
{
sleep(1);
write(fd, "abc", 3);
}
close(fd);
return 0;
}
输出结果:
helloworldabc
通过以上代码测试,父子进程共享一个文件表项(file struct),也就是共用一个读写位置。
操作文件时,父子进程共用读写位置。
- 在execX系列函数替换代码后,对打开的文件能够再处理吗?
默认情况下,execX执行的代码可以访问在原来代码中打开的文件,操作是同一个文件描述符,即用一个文件对象。
fcntl(fd,F_SETFD,FD_CLOEXEC);
语句用来使在execX之前打开的文件描述符在新的代码中不可用。
5. 进程属性获取与修改
进程的属性包括进程组属性和用户属性
(1)进程组属性决定了进程中运行过程中控制权限以及相关控制信息
- PID:进程号。当前进程在当前系统下唯一的编号,针对这个进程执行的操作多以进程号为标识:例如,等待某个子进程结束
waitpid
;向某个进程发送信号。用户可以获取getpid()
,但不能通过函数修改进程号的值。 - PPID:父进程号。一般为创建这个进程的那个进程的ID,一般也不会修改,当这些进程的父进程退出后,当前进程变成孤儿进程,它的PPID会被修改成init进程,即PID=1的这个进程。
- PGID:进程组号。将完成协同工作的多个进程默认为一个进程组。例如,在终端运行一个新的程序,新的程序创建的子进程以及自身在一个进程组下,而第一个进程默认为进程组长,进程组号也是进程组长的ID。PGID可以被获取和修改,
getpgid(pid_t pid)
:参数为某个进程的ID,返回该进程的进程组长编号;setpgid(pid_t pid, pid_t pgid)
:修改某个进程的进程组长。
修改一个进程的进程组号的意义:
kill
可以向一个进程组发起信号,要影响整个这个进程组的所有进程。
- SID:会话ID(session ID)。会话:进行交互。一般,在某个终端下执行的程序所创建的进程/进程组,它们的SID就是这个终端的编号。在一个会话下的所有进程都受到这个会话终端的影响。
getsid(pid)
:获取某个进程的会话ID。
setsid()
:设置某个进程为会话组长,要求这个进程不能说进程组长。一般在创建守候进程时会修改SID,避免原来关联进程的终端信号影响子进程。 - 终端
一个进程可以与某个终端关联,建立与控制终端关联的这个会话首进程为控制进程。
一个会话中的多个进程组可以分为一个前台和多个后台。
在终端下执行键盘命令ctrl+c
等,会将信号发送给前台进程组所有进程。
NAME
tcgetpgrp, tcsetpgrp - get and set terminal foreground process group
SYNOPSIS
#include <unistd.h>
pid_t tcgetpgrp(int fd);
int tcsetpgrp(int fd, pid_t pgrp);
(2) 用户属性决定了进程在运行时对其他资源的访问权限,如对文件的读写权限
- uid/ruid:创建这个进程的用户的ID。例如用户ID为500,uid就是为500
- gid/rgid:创建这个进程的用户所在组的id。
- EUID:有效用户ID,一般同uid。
- EGID:有效用户组ID,一般同gid。
一般对文件真正的访问权限由EUID和EGID决定,当EUID和EGID仅仅是在这个可执行程序的setuid位和setgid位被设置时,相应的EUID和EGID将与执行这个进程的UID/GID不同,上升到了这个可执行程序的setuid用户。
如:
delphi@delphi-vm:~/code/linux_coding$ ll /usr/bin/passwd
-rwsr-xr-x 1 root root 37100 2011-02-15 06:12 /usr/bin/passwd*
passwd的可执行程序setuid被置位,普通用户执行这个程序时,UID是普通用户ID,
但EUID上升到了这个文件的拥有者root,即这个进程对文件的访问权限为root用户的权限。
因此可以修改/etc/passwd这个文件。