- 1. 进程
- 进程与程序的区别
- 2. 进程空间问题
- 2.1 虚拟内存的划分
- 2.2 虚拟内存和物理内存之间由MMU(内存管理单元)映射管理
- 2.3 虚拟内存划分
- 2.4 进程进行IO操作示意图
- 3. 进程相关概念
- 3.1 进程号 PID
- 3.2 Linux 下进程分类
- 3.3 进程控制块(PCB)
- 3.4 进程的调度机制
- 多个进程如何运行
- 3.5 进程的运行状态
- 4. 进程相关命令
- 查看进程,杀掉进程
- 前后台切换
- 5. 进程相关函数
- 5.1 fork() 创建子进程
- 父子进程调度机制
- 父子进程用户空间问题
- 父子进程共享资源问题 / 父子进程同时访问同一文件
- dup() 复制文件描述符
- 5.2 getpid() / getppid() 获取当前进程号 / 父进程号
- 5.3 exit() / _exit() 退出当前进程
- `return`与`exit()`
- `exit()`与`_exit()`区别
- 5.4 atexit() 进程结束后,可以执行的代码。
- 5.5 wait() / waitpid() 等待子进程退出,回收资源
- 6. Linux下的特殊进程
- 6.1 处理僵尸进程
- 6.2 创建孤儿进程
- 6.3 创建守护进程
- 守护进程创建步骤
进程就是一段程序的执行过程。
进程与程序的区别
- 程序是静态的,它是保存在磁盘上的指令的有序集合,没有任何执行的概念
- 进程是一个动态的概念,它是程序执行的过程,包括进程的创建、调度和消亡
- 程序是保存在磁盘上的,而进程是在内存中运行的
2. 进程空间问题
当一个进程运行或者创建时,操作系统会自动为其分配4G的虚拟内存空间(32位操作系统),虚拟内存的使用主要解决了进程间通信问题,保证每一个进程的空间一样,这样在通信的时候,数据交换更加方便。
既然说是分配4G的虚拟内存,那么实际分配时一定不会直接给它4个G的物理内存,而是实际使用多少,就分配多少的物理内存。
2.1 虚拟内存的划分
- 4G 的虚拟内存分为 1G 内核空间 和 3G 用户空间
- 1G 的内核空间是 操作系统中所有进程所公有的
- 3G 的用户空间是进程私有的
进程间通信就是学习如何在内核空间开辟区域。
2.2 虚拟内存和物理内存之间由MMU(内存管理单元)映射管理
MMU(Memory Management Unit)主要用来管理虚拟存储器、物理存储器的控制线路,同时也负责虚拟地址映射为物理地址,以及提供硬件机制的内存访问授权、多任务多进程操作系统。
2.3 虚拟内存划分
2.4 进程进行IO操作示意图
3. 进程相关概念
3.1 进程号 PID
在操作系统中,每一个进程都有一个编号,该编号就是进程号,进程号是进程的唯一标识
进程号是操作系统给当前进程随机分配的,非负整数。
特殊的进程号:
- 0 → 内核进程号,系统运行起来就是内核进程
- 1 → init进程,它是所有进程的祖先
3.2 Linux 下进程分类
| 类型 | 解释 |
|---|---|
| 交互进程 | 由shell控制和运行,可以在前台运行,也可以在后台运行 |
| 批处理进程 | 不属于某一个终端,它被提交到一个队列中,以便顺序执行 |
| 守护执行 | 在后台运行,一般在Linux启动时就开始执行,系统关闭时才结束 |
3.3 进程控制块(PCB)
进程在创建和运行时,会有专门的一个结构体来保存它的信息,这个结构体为task_struct,又将其称之为进程表项或进程控制块,简称PCB
在/usr/src/linux-headers-3.2.0-29-generic-pae/include/linux下的sched.h头文件里面定义了task_struct结构体
3.4 进程的调度机制 多个进程如何运行
时间片轮转,上下文切换
3.5 进程的运行状态
| 符号 | 状态 | 解释 |
|---|---|---|
| D | 等待 | 不可中断的静止 |
| R | 运行态 | 正在执行中 |
| S | 睡眠态 | 阻塞状态 |
| T | 停止态 | 暂停执行,程序运行时,按下ctrl+Z,进程可进入该状态 |
| Z | 僵尸态 | 不存在但无法消除 |
| W | 没有足够页可分配 | |
| + | 前台进程 | 前台运行 |
| < | 高优先级进程 | 优先运行 |
| N | 低优先级进程 | |
| L | 有内存分页分配并锁在内存中,多线程中出现 | |
| s | 会话组组长 |
4. 进程相关命令
查看进程,杀掉进程
ps #查看当前用户运行的进程 ps aux #查看当前系统中所有的进程,并且可以查看所占内存百分百等信息 ps ajx #查看当前系统中所有的进程,并且可以查看当前进程的父进程的id top #动态显示系统中所有的进程 htop #更加好看的动态显示系统中所有的进程 sudo apt-get install htop #下载htop nice #按用户指定的优先级运行进程 renice #改变正在运行进程的优先级 kill #向一个进程发送一个信号 kill -9 pid #将进程号为pid的进程杀死 pstree #以树形结构显示系统中所有的进程的关系
前后台切换
#将一个进程在后台运行,查看运行状态后面没有+,如果是前台运行,会有+ ./a.out & #将挂起的进程(停止态T)在后台执行 bg #把后台运行的进程放到前台运行(+),也可以直接把停止态(T)的进程直接唤醒 fg #将进程号为pid的进程变为停止态 kill -19 pid #将进程号为pid的进程从停止态变为后台进程 kill -18 pid
5. 进程相关函数
5.1 fork() 创建子进程
#include#include pid_t fork(void); 功能: 父进程创建一个子进程 参数: 无 返回值: 成功: >0 子进程的进程号,标识父进程的代码区 0 标识子进程的代码区 失败:-1
fork函数主要用于在一个进程中创建子进程,目的是为了能够在一个程序里面分别独立执行多个任务,相互之间还没有影响
只要执行一次fork(),就会在原有进程基础上再创建一个进程
如果不区分父子进程代码,fork()后所有代码,父子进程都会执行
一般使用fork()都是为了执行不同任务,所以一般都会区分父子进程代码区。
区分父子进程的方法就是判断fork()函数的返回值,在父进程中,fork()返回子进程PID,在子进程中fork()返回0。
父子进程调度机制父子进程调度机制也是时间片轮转,上下文切换。
所以,他们之间执行时,根本没有先后之分。
使用fork()函数创建的子进程,会将父进程 (虚拟内存中) 的用户空间完整复制一份,作为子进程 (虚拟内存中) 的用户空间。
父子进程之间的用户空间是完全独立的,他们对各自用户空间的操作,都不会对对方产生任何影响。
这里注意,如果在父进程中已经定义了一个变量a,a的地址为0x123,当子进程创建完后,子进程中也会有变量a,且地址也是0x123,但是他们并不是同一个a,因为他们分别属于不同的用户空间,互不影响。
父子进程共享资源问题 / 父子进程同时访问同一文件
先看一段代码
#include#include #include #include #include int main(int argc, char const *argv[]) { int fd; if((fd = open(argv[1], O_RDWR)) == -1) { perror("open error"); return -1; } pid_t pid = fork(); if(pid == -1) { perror("fork error"); return -1; } else if(pid > 0) //父进程的代码区 { //父进程向文件写入数据 write(fd, "hello world", 12); printf("父: [%d]的offset = %ldn", fd,lseek(fd, 0, SEEK_CUR)); } else //子进程的代码区 { sleep(1); //子进程从文件中读取数据 printf("子: [%d]的offset = %ldn", fd,lseek(fd, 0, SEEK_CUR)); lseek(fd, 0, SEEK_SET); printf("子: 设置[%d]的offset = %ldn", fd,lseek(fd, 0, SEEK_CUR)); char buf[32] = {0}; read(fd, buf, 32); printf("子:buf = [%s]n", buf); } while(1){} return 0; }
执行结果
为什么子进程拿到文件描述符的时候,他的偏移量已经大于0了?
简单来看,子进程只拷贝了父进程的用户空间的所有东西,而关于文件的信息的部分,都在内核空间,内核空间的东西都是公用的,所以,子进程拿着复制来的文件描述符进行操作,必然受父进程的影响。
上面的解释也容易让人产生疑虑——既然内核空间的东西都是公用的,那么我对一个文件操作后,文件偏移量增加,如果此时,另一个进程也在操作这个文件,那么他们之间会不会产生影响?答案是不会。因为这两个进程在open()时,虽然都获得了一个文件描述符来表示同一个文件,但是内核为这两个进程创建了两个不同文件结构体struct file(就是上图的文件表项),这个两个文件结构体都指向同一个struct inode,inode节点用来唯一标识一个文件。由此也可断定父子进程间,如果在各自代码块内产生文件描述符,是不会对对方产生影响的(实际也是这样)。
既然如此,那fork()前创建的文件描述符,在父子进程间为什么会互相影响呢?
进程在创建的时候,内核会为其创建一个进程结构体struct task_struct,也就是进程控制块(PCB),保存了进程的各种信息(内核空间是公用的,内核中的进程结构体(进程控制块PCB)只负责各自进程的信息)。这个结构体中有一个struct files_struct *来指向保存该进程所有打开的文件信息的结构体,在files_struct中又有struct file * [32]来保存所有文件描述符对应的文件信息,每创建一个文件描述符,就会创建一个struct file(文件表项)并把地址存在struct file * [32]数组中。在父进程创建子进程时候,内核会为子进程拷贝父进程的所有信息,并作出必要的修改,因为这两个进程都是独立存在的,所以在拷贝结构体时应当都是深拷贝。
至少从现象来看,内核在拷贝struct files_struct *(指针)和struct file *(指针)时,为子进程开辟空间复制了一份struct files_struct(结构体变量)和struct file(结构体变量)并更新struct files_struct *(指针)和struct file *(指针)。在拷贝struct file * [32](结构体指针数组变量)时,只拷贝了数组中的指针值,而没有重新为每个指针指向内容重新开辟空间拷贝。文件描述符就是这个结构体指针数组的下标。当一个新的文件描述符指向一个已存在struct file文件表项时,文件表项中的打开引用计数 f_count就会加一,然后每关一个文件描述符,该f_count 打开引用计数就会减一,如果想要关掉这个文件表项,就需要满足打开引用计数 f_count为0,所以,子进程与父进程使用fork()前创建的文件描述符时,会互相影响偏移量、文件状态标志等,因为他们共用同一文件表项中的内容。如果其中一个进程把文件描述符关掉的话,这个进程的文件描述符表会把对应文件描述符去除,文件表项中的打开引用计数 f_count减一,这个进程将不能用该文件描述符访问原文件。而另外一个进程仍然可以正常使用该文件描述符,因为其对应的文件表项还存在。
在同一进程内,open()同一个文件多次,产生多个文件描述符符,这些文件描述符都对应独立的文件表项,不会互相影响偏移量、文件状态标志等。
总结:
先open()再fork(),子进程的会复制父进程的文件描述符表,但并不会创建新的文件表项,文件描述符对应的文件表项中打开引用计数会加一,此时,父子进程共享同一文件表项,意味着共享文件偏移量、文件状态标志等。关闭文件描述符时,文件表项中打开引用计数会减一,当打开引用计数==0时,文件表项才会关闭。所以父子进程间其中一个关闭文件描述符对另外一个并不影响。
先fork()再open(),父子进程有独立的文件描述符表,各自创建文件描述符时,内核会为他们各自再创建一个文件表项,意味着他们此时,就算打开的是同一个文件,也不会互相影响到对方的偏移量、文件状态等。但是向文件中写内容可会出现被覆盖等异常。
借用大佬的"高清图"
如果对文件描述符创建这个过程不太了解可以看这个文件描述符。
这里参考了Linux文件共享(四)——父进程与子进程之间的文件共享 和 关于文件描述符(file_struct)
扩展:
dup() 复制文件描述符子进程复制父进程文件描述符的过程,与函数dup()比较类似
#includeint dup(int oldfd); int dup2(int oldfd, int newfd); #define _GNU_SOURCE #include #include int dup3(int oldfd, int newfd, int flags); #include int dup(int oldfd); 功能: 用一个新的文件描述符,复制已存在的文件描述符 执行成功后,新的文件描述符和旧的文件描述符可以同时使用, 他们都引用同一个文件表项,共享文件偏移量和文件状态标志。 (they refer to the same open file description (see open(2)) and thus share file offset and file status flags) 这两个文件描述符不共享文件描述符标志位中的(close-on-exec flag) 该标志是指 在执行 exec 函数时,会关闭 所有有close_on_exec flag的文件描述符。 (The two file descriptors do not share file descriptor flags (the close-on-exec flag)) 参数: oldfd 传要复制的文件描述符 返回值 成功 新的文件描述符 失败 -1 errno 会保存错误码 dup2 类似dup,会使用指定的文件描述符newfd,来复制旧的文件描述符oldfd 如果newfd已经被使用,那么执行该函数后,在重新使用newfd之前, newfd会静默关闭。关闭文件描述符,和重新使用都是自动执行。 注意:1. 如果oldfd是无效的,那么会调用失败,newfd也会关闭 2. 如果oldfd是有效的,且newfd == oldfd,那么什么也不做返回newfd dup3 类似dup2,但是可以通过flags传参 O_CLOEXEC 来为文件描述符设置 close_on_exec flag, 该标志是指 在执行 exec 函数时,会关闭 所有有close_on_exec flag的文件描述符。 与dup2不同,dup3中,如果oldfd == newfd ,会报错,errno == EINVAL
我们也可以使用fcntl()来实现对已打开文件描述符,设置close-on-exec flag
int fd=open("foo.txt",O_RDONLY);
int flags = fcntl(fd, F_GETFD);
flags |= FD_CLOEXEC;
fcntl(fd, F_SETFD, flags);
在open()函数中也可以传参O_CLOEXEC[linux 2.6.23以后支持]
open("a", O_RDWR | O_CLOEXEC);
在创建socket也可以
socket(AF_INET, SOCK_DGRAM | SOCK_CLOEXEC, 0);
参考关于fd的close on exec
5.2 getpid() / getppid() 获取当前进程号 / 父进程号
#include#include 获取当前进程的进程号 pid_t getpid(void); 获取当前进程的父进程的进程号 pid_t getppid(void);
5.3 exit() / _exit() 退出当前进程
#includereturn与exit()void exit(int status); 功能: 退出当前进程 参数: status 当前进程退出的状态值 返回值: 无 #include void _exit(int status); 功能: 退出当前进程 参数: status 当前进程退出的状态值, 可以返回给父进程(父进程可以通 过wait函数获取这个值 一般不需要将这个值返回给父进程, 所以一般设置为0表示成功 退出,非0表示错误退出 返回值:无
return在主函数中执行,会退出主函数,并结束进程,但在子函数中执行,会退出子函数,但不会结束整个进程。
exit()不管在哪里执行都会退出整个进程。
exit()是库函数,会刷新缓冲区
_exit()是系统调用,不会刷新缓冲区,相当于直接kill进程
5.4 atexit() 进程结束后,可以执行的代码。
#includeint atexit(void (*function)(void)); 功能: 他在普通进程结束或者主函数return后 被执行。这个函数可能会被注册很多次, 他的执行顺序和注册顺序相反,每次注 册都会被调用一次。 fork()的子进程也会继承该函数的注册, 如果用exec()函数,这些注册信息就会 被清除。 参数: void (*function)(void) 函数指针 如果不传参的话,会被忽略 返回值: 成功 0 失败 非0
当使用_exit()(系统调用)退出进程时,atexit()也不会执行。
示例代码
#include#include #include int b = 100; void myexit() { printf("这是进程结束后执行的最后一个代码n"); } void myexit2(){ puts("程序结束后,我又调用了一次"); } void myexit3(){ printf("程序结束了,但是还想打印一个变量[%d]n",b); } void myfun() { printf("nihao beijing"); exit(0); //不刷新缓冲区,也不会执行atexit() //_exit(0); printf("hahahahahahahan"); } int main(int argc, char const *argv[]) { //当进程结束(非_exit())之后,还是可以执行代码的, //所执行的代码是atexit的回调函数 //先注册的后执行,后注册的先执行 atexit(myexit); atexit(myexit2); atexit(myexit3); printf("hello worldn"); myfun(); return 0; }
5.5 wait() / waitpid() 等待子进程退出,回收资源
#include#include pid_t wait(int *wstatus); pid_t waitpid(pid_t pid, int *wstatus, int options); 功能: 阻塞等待子进程的状态(子进程退出状态)改变 参数: pid:指定要等待的子进程 <-1 只阻塞等待进程组id等于这个值的绝对值的组中任意一个子进程 -1 阻塞等待所有子进程的退出状态 0 阻塞等待进程组id等于当前进程的进程号的组中任意一个子进程 >0 阻塞等待子进程的进程号等于这个值的子进程 wstatus:保存子进程状态改变值 options:选项 0 阻塞 WNOHANG 非阻塞 返回值: 成功:退出的子进程的进程号 失败:-1 wait(NULL) <==> waitpid(-1, NULL, 0);
6. Linux下的特殊进程
| 特殊进程分类 | 含义 |
|---|---|
| 僵尸进程 | 子进程结束,父进程没有结束,并且此时父进程没有将子进程的资源释放,此时子进程就是僵尸进程 |
| 孤儿进程 | 父进程结束,而子进程没有结束,此时子进程的父进程变为init进程,这个子进程称为孤儿进程 |
| 守护进程 | Daemon进程,Linux中的后台服务进程。它是一个生存期较长的进程,通常独立于控制终端,并且周期性的执行某种任务,或等待处理某些发生的事件 |
- 父进程结束,此时僵尸进程会变成孤儿进程,其父进程变为init进程,那么他的资源就会被init回收。
- 使用wait()函数,阻塞等待子进程退出,释放僵尸进程的资源。缺点,wait()是阻塞函数,父进程只有等待wait()处理完僵尸进程,才能执行下一步操作。
- 使用waitpid()设置非阻塞来处理僵尸进程,但是必须循环判断,如果没有循环,一样没用。
- 比较好的方式,使用信号来处理僵尸进程。
示例代码
#include#include #include #include #include int main(int argc, char const *argv[]) { pid_t pid; if((pid = fork()) == -1) { perror("fork error"); exit(1); } else if(pid > 0) //父进程 { printf("父进程正在执行...n"); //wait(NULL); for(;;) { waitpid(-1, NULL, WNOHANG); printf("hello worldn"); sleep(1); } } else //子进程 { printf("子进程正在执行...n"); sleep(10); printf("子进程退出了n"); exit(0); } return 0; }
6.2 创建孤儿进程
#include#include #include #include #include int main(int argc, char const *argv[]) { pid_t pid; if((pid = fork()) == -1) { perror("fork error"); exit(1); } else if(pid > 0) //父进程 { int ret; printf("父进程正在执行...n"); sleep(10); printf("父进程退出了n"); exit(0); } else //子进程 { printf("子进程正在执行...n"); for(;;) { printf("pid = %d, ppid = %dn", getpid(), getppid()); printf("hello worldn"); sleep(1); } } return 0; }
6.3 创建守护进程
守护进程常常在系统启动时开始运行,在系统关闭时终止
Linux系统有很多守护进程,大多数服务都是用守护进程实现的
在Linux中,每一个系统与用户进行交流的界面称为终端。从该终端开始运行的进程都会依附于这个终端,这个终端称为这些进程的控制终端。当控制终端被关闭时,相应的进程都会被自动关闭。
守护进程能够突破这种限制,它从开始运行,直到整个系统关闭才会退出。如果想让某个进程不会因为用户或终端的变化而受到影响,就必须把这个进程变成一个守护进程。
守护进程创建步骤
第一步:将子进程变成孤儿进程,目的是为了不受终端影响
第二步:将子进程设置为会话组组长,目的是为了不受其他进程影响
第三步:将子进程的工作目录改为根目录,目的是为了不受工作目录影响
第四步:将子进程对文件操作的权限修改为最高,目的是为了不受文件权限的影响
第五步:将子进程中所有打开的文件描述符关闭
示例代码
#include#include #include #include #include #include int main(int argc, char const *argv[]) { //第一步:将子进程变成孤儿进程,目的是为了不受终端影响 pid_t pid; if((pid = fork()) == -1) { perror("fork error"); exit(1); } else if(pid > 0) { exit(0); } printf("pid = %dn", getpid()); //第二步:将子进程设置为会话组组长,目的是为了不受其他进程影响 setsid(); //第三步:将子进程的工作目录改为根目录,目的是为了不受工作目录影响 chdir("/"); //第四步:将子进程对文件操作的权限修改为最高,目的是为了不受文件权限的影响 umask(0); //第五步:将子进程中所有打开的文件描述符关闭 int maxfd = getdtablesize(); int i; for(i = 0; i <= maxfd; i++) { close(i); } //守护进程执行服务 int fd = open("file.txt", O_WRonLY | O_CREAT | O_TRUNC, 0664); if(fd == -1) { perror("fail to open"); exit(1); } while(1) { write(fd, "hello worldn", 12); sleep(1); } return 0; }



