高性能并发服务器
[ubuntu安装vmtools教程以及显示unable to execute](ubuntu安装wmwaretools教程以及显示 unable to execute “usr/bin/vmware-uninstall-tools.pl”解决办法 - 傻逼离我远点 - 博客园 (cnblogs.com))
使用 <font>
的标签的修改文字前景色
红色
绿色
蓝色
使用 rgb 颜色值
使用十六进制颜色值
1. Gcc
2. 静态库制作和使用
cp calc library ../lession06 -r
将当前文件夹lession05
下的calc
和library
文件复制到上一级目录下的lession05
中nowcoder@nowcoder:~/Linux/lession06$ tree
.
├── calc
│ ├── add.c
│ ├── add.o
│ ├── div.c
│ ├── div.o
│ ├── head.h
│ ├── libcalc.a
│ ├── main.c
│ ├── mult.c
│ ├── mult.o
│ ├── sub.c
│ └── sub.o
└── library
├── app
├── include
│ └── head.h
├── lib
│ └── libcalc.a
├── main.c
└── src
├── add.c
├── div.c
├── mult.c
└── sub.c
5 directories, 19 files
nowcoder@nowcoder:~/Linux/lession06$ cd calc
nowcoder@nowcoder:~/Linux/lession06/calc$ rm *.o libcalc.a 删除calc下的.o文件和库文件
3. 制作动态库
1.制作动态库
gcc -c -fpic add.c div.c sub.c mult.c
gcc -shared *.o -o libcalc.so
ldd +可执行文件(查看动态库地址加载情况)
env查看环境变量
nowcoder@nowcoder:~/Linux/lession06/library$ ll
总用量 36
drwxrwxr-x 5 nowcoder nowcoder 4096 4月 13 10:52 ./
drwxrwxr-x 4 nowcoder nowcoder 4096 4月 13 10:16 ../
drwxrwxr-x 2 nowcoder nowcoder 4096 4月 13 10:16 include/
drwxrwxr-x 2 nowcoder nowcoder 4096 4月 13 10:43 lib/
-rwxrwxr-x 1 nowcoder nowcoder 8424 4月 13 10:52 main*
-rw-rw-r-- 1 nowcoder nowcoder 306 4月 13 10:16 main.c
drwxrwxr-x 2 nowcoder nowcoder 4096 4月 13 10:16 src/
nowcoder@nowcoder:~/Linux/lession06/library$ ldd main
linux-vdso.so.1 (0x00007fffa132c000)
libcalc.so => not found //<-看这里
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f986a129000)
/lib64/ld-linux-x86-64.so.2 (0x00007f986a71c000)
3.1export添加环境变量(临时)
pwd
查看当前文件夹所在路径
nowcoder@nowcoder:~/Linux/lession06/library$ cd lib |
echo命令用于输出变量的值
echo $LD_LIBRARY_PATH:/home/nowcoder/Linux/lession06/library/lib
3.2添加换变量(长期)
3.3动静态库的优缺点
4. Makefile
4.1 什么是Makefile
- 一个工程中的源文件不计其数,其按类型、功能、模块分别放在若干个目录中Makefile 文件定义了一系列的规则来指定哪些文件需要先编译,哪些文件需要后编译,哪些文件需要重新编译,甚至于进行更复杂的功能操作,因为Makefile 文件就像一个 shel1 脚本一样,也可以执行操作系统的命令。Makefile 带来的好处就是“自动化编译”,一旦写好,只需要一个 make 命令,整个工程完全自动编译,极大的提高了软件开发的效率。make 是一个命令工具,是一个解释 Makefile 文件中指令的命令工具,一般来说,大多数的 IDE 都有这个命令比如Delphi的make,VisualC++的nmake,Linux下GNU 的 make。
4.2 Makefile 文件命名和规则文件命名
- 文件命名
makefile 或者Makefile - Makefile 规则
- 一个Makefile 文件中可以有一个或者多个规则
- 目标… : 依赖…
命令(shell 命令)
目标:最终要生成的文件(伪目标除外)
依赖:生成目标所需要的文件或是目标
命令:通过执行命令对依赖操作生成目标(命令前必须Tab 缩进)口 - Makefile 中的其它规则一般都是为第一条规则服务的。
- 目标… : 依赖…
- 一个Makefile 文件中可以有一个或者多个规则
4.3 工作原理
命令在执行之前,需要先检查规则中的依赖是否存在
- 如果存在,执行命令
- 如果不存在,向不检查其它的规则,检查有没有一个规则是用来生成这个依赖的如果找到了,则执行该规则中的命令
检测更新,在执行规则中的命令时,会比较目标和依赖文件的时间
如果依赖的时间比目标的时间晚,需要重新生成目标
如果依赖的时间比目标的时间早,目标不需要更新,对应规则中的命令不需要被执行
4.4 自定义变量
4.5 模式匹配
add.o:add.c |
4.6 函数
$(wildcard PATTERN…)
功能: 获取指定目录下指定类型的文件列表
参数: PATTERN 指的是某个或多个目录下的对应的某种类型的文件,如果有多个目录,一般使用空格间隔
返回:得到的若干个文件的文件列表,文件名之间使用空格间隔
示例:
$(wildcard .c ./sub/.c)
返回值格式:
a.c b.c c.c d.c e.c f.c
- ```C
#定义变量
#获取指定目录下的.o文件 sub.o add.o mult.o div.o main.o
src=$(wildcard./*.c)
target=app
$(target):$(src)
$(CC) $(src) -o $(target)
%.o:%.c
$(CC) -c $< -o $@
* $(patsubst<pattern>,<replacement>,<text>
* 功能: 查找<text>中的单词(单词以“空格”、“Tab"或“回车”“换行”分隔)是否符合模式<pattern>,如果匹配的话,则以<replacement>替换。
* <pattern>可以包括通配符`%`,表示任意长度的字串。如果<replacement>中也包含`%`,那么,<replacement>中的这个`%`将是<pattern>中的那个`%`所代表的字串。(可以用`\`来转义,以`\%`来表示真实含义的`%`字符)
* 返回: 函数返回被替换过后的字符串
* 示例:
* `$(patsubst %.c,%.o,x.c bar.c)`
* 返回值格式:`x.o bar.o`
* ```
#获取指定目录下的.o文件 sub.o add.o mult.o div.o main.o
src=$(wildcard./*.c)
objs=$(patsubst %.c,%.o,$(src))
target=app
$(target):$(src)
$(CC) $(src) -o $(target)
%.o:%.c
$(CC) -c $< -o $@
- ```C
touch 文件名——创建文件
.PHONY: 文件名O——生成伪目标文件O
5. GBD调试
5.1 什么是 GDB
- GDB 是由 GNU 软件系统社区提供的调试工具,同GCC配套组成了一套完整的开发环境,GDB是Linux和许多类Unix系统中的标准开发环境。
- 一般来说,GDB 主要帮助你完成下面四个方面的功能:
- 1.启动程序,可以按照自定义的要求随心所欲的运行程序
2.可让被调试的程序在所指定的调置的断点处停住(断点可以是条件表达式)
3.当程序被停住时,可以检查此时程序中所发生的事
可以改变程序,将一个 BUG 产生的影响修正从而测试其他BUG4.
- 1.启动程序,可以按照自定义的要求随心所欲的运行程序
5.2 准备工作
通常,在为调试而编译时,我们会()关掉编译器的优化选项(-o),并打开调试选项(-g)。另外,
-wa11
在尽量不影响程序行为的情况下选项打开所有warning
,也可以发现许多问题,避免一些不必要的BUG。gcc -g-Wall program.c o program
-g
选项的作用是在可执行文件中加入源代码的信息,比如可执行文件中第几条机器指令对应源代码的第几行,但并不是把整个源文件嵌入到可执行文件中,所以在调试时必须保证 gdb 能找到源文件。nowcoder@nowcoder:~/Linux/lession08$ gcc test.c -o test -g
nowcoder@nowcoder:~/Linux/lession08$ gcc test.c -o test1
nowcoder@nowcoder:~/Linux/lession08$ ll -h
总用量 52K
-rwxrwxr-x 1 nowcoder nowcoder 11K 4月 14 20:26 test*
-rwxrwxr-x 1 nowcoder nowcoder 8.3K 4月 14 20:26 test1*
5.3 GDB命令-启动、推出、查看代码
启动和退出
gdb 可执行程序quit
给程序设置参数/获取设置参数
set args 1020
show argsGDB 使用帮助
help查看当前文件代码
list/1 (从默认位置显示)
list/1行号 (从指定的行显示)
list/1 函数名(从指定的函数显示)查看非当前文件代码
list/l 文件名:行号
list/l 文件名:函数名
设置显示的行数
show list/listsize
set list/listsize 行数
(gdb) list |
5.4 GBD命令-断点操作
(gdb) break 9 |
5.5 GBD命令-调试命令
6. 文件I/O(针对内存而言)
6.1 标准C库IO函数
1、标准C库I/O函数与Linux系统I/O函数的区别
(1)标准C库I/O函数在读写的时候,中间有一个缓冲区,而Linux系统I/O函数没有缓冲区;如果中间有缓冲区的话在进行读写操作的时候会先存到缓冲区,再刷新到磁盘,它比直接逐条读写到磁盘效率要高。
(2)根据应用场景选择合适的I/O函数,如:再进行网络通信时就应该使用Linux系统I/O函数,因为通信更要求实时性;而在对磁盘进行读写时则选择标准C库I/O函数。
标准C库I/O函数与Linux系统I/O函数对比(通俗易懂)_标准c库io函数和linux系统io函数对比-CSDN博客
6.2 标准C库IO和Linux系统和IO关系
[68-文件I/O:标准C库IO函数和Linux系统IO函数对比-CSDN博客](https://blog.csdn.net/Edward_LF/article/details/124398047#:~:text=标准c库函数和linux系统函数区别: 标准c库可以跨平台;(调用了不同平台的系统API),在linux平台中,调用c库函数,底层是调用的是linux中的系统函数 linux系统I%2FO函数是没有缓冲区的,调用一次就会访问一次)
6.3 虚拟地址空间
- 一个进程对应一个虚拟地址空间,由CPU中的MMU内存管理映射到真实的物理地址,程序(.c、.exe)并不占用内存空间,只占用磁盘空间。进程占用内存。文件描述符在内核区。
6.4 文件描述符
6.5 open打开文件
/*open 打开文件 |
6.6 open创建新文件
/*open创建新文件 |
6.5 Linux系统i/o函数
read函数读取数据是指从文件中读取数据到内存中
write函数写数据是指把内存中数据写到文件中
/* |
6.6lseek函数
——移动文件指针到文件头、获取当前文件指针的位置、获取文件长度、拓展文件的长度
/* |
6.6 lstat函数和stat函数
——获取一个文件相关的一些信息
//stat 结构体 |
/* |
- 判断权限,应该和相应的宏与&操作;判断文件类型,将mode与掩码与&操作,再和宏进行比较
6.7模拟实现ls-l命令
——ls-l命令能够获取当前目录下文件的信息
ls-l xx.txt查看当前xx文件的信息
2024/04/21
6.8 文件属性操作函数
int access(const char *pathname, int mode); |
- *int access(const char pathname, int mode);
——作用:判断某个文件是否具有某个权限,或者判断文件是否存在
——参数:
- pathname: 判断的文件路径
- mode:
R_OK: 判断是否有读权限
W_OK: 判断是否有写权限
X_OK: 判断是否有执行权限
F_OK: 判断文件是否存在
返回值:成功返回0, 失败返回-1
- *int chmod(const char filename, int mode_t);
—— 作用:修改文件的权限
参数:
- pathname: 需要修改的文件的路径
- mode:需要修改的权限值,八进制的数
返回值:成功返回0,失败返回-1
int *chown(const char path, uid_t owner, gid_t group);
—— 作用:修改文件的所有者或所在组
vim /etc/passwd
——显示所有的用户和id、组idvim /etc/group
——查看当前系统所有组和iduseradd xx
——创建xx用户id xx
——查看xxidint truncate(const char path, off_t length);
作用:缩减或者扩展文件的尺寸至指定的大小
参数:
- path: 需要修改的文件的路径
- length: 需要最终文件变成的大小
返回值:
成功返回0, 失败返回-1
——
touch xx.xx
创建xx.x文件——
vim xx.x
进入xx.x文件
6.9 目录操作函数
int mkdir(const char *pathname, mode_t mode); |
- *int mkdir(const char pathname, mode_t mode);
man 2 xxx
——查看Linux系统函数xx
—— 作用:创建一个目录
参数:
pathname: 创建的目录的路径
mode: 权限,八进制的数
返回值:
成功返回0, 失败返回-1
|
结果:创建了aaa目录
nowcoder@nowcoder:~/Linux/lesson14$ gcc mkdir.c -o mkdir |
*int rmdir(const char pathname);
——作用:删除空目录
**int rename(const char oldpath, const char newpath);
——作用:重命名
*int chdir(const char path);
——作用:修改进程的工作目录
比如在/home/nowcoder 启动了一个可执行程序a.out, 进程的工作目录 /home/nowcoder
——参数:
path : 需要修改的工作目录
**char getcwd(char buf, size_t size);
——作用:获取当前的工作路径
参数:
- buf : 存储的路径,指向的是一个数组(传出参数)
- size: 数组的大小
返回值:
返回的指向的一块内存,这个数据就是第一个参数
|
6.10 目录遍历函数
DIR *opendir(const char *name); //打开目录 |
shell终端输入man 3 xx
——查看标准C库函数
- dirent结构体和d_type
/* |
6.11 dup、dup2函数
int dup(int oldfd); |
#include <unistd.h>
int dup(int oldfd);
作用:复制一个新的文件描述符,指向同一个文件
fd=3, int fd1 = dup(fd),
fd指向的是a.txt, fd1也是指向a.txt
从空闲的文件描述符表中找一个最小的,作为新的拷贝的文件描述符
|
#include <unistd.h>
int dup2(int oldfd, int newfd);
作用:重定向文件描述符
oldfd 指向 a.txt, newfd 指向 b.txt
调用函数成功后:newfd 和 b.txt 做close, newfd 指向了 a.txt
oldfd 必须是一个有效的文件描述符
oldfd和newfd值相同,相当于什么都没有做
返回值是:return the new file descriptor
*/
int main() {
int fd = open("1.txt", O_RDWR | O_CREAT, 0664);
if(fd == -1) {
perror("open");
return -1;
}
int fd1 = open("2.txt", O_RDWR | O_CREAT, 0664);
if(fd1 == -1) {
perror("open");
return -1;
}
printf("fd : %d, fd1 : %d\n", fd, fd1);
int fd2 = dup2(fd, fd1);//fd2==fd1
if(fd2 == -1) {
perror("dup2");
return -1;
}
// 通过fd1去写数据,实际操作的是1.txt,而不是2.txt
char * str = "hello, dup2";
int len = write(fd1, str, strlen(str));
if(len == -1) {
perror("write");
return -1;
}
printf("fd : %d, fd1 : %d, fd2 : %d\n", fd, fd1, fd2);
close(fd);
close(fd1);
return 0;
}
6.12 fcntl函数
- **int fcntl ( int fd, int cmd, …/arg / );
复制文件描述符
设置/获取文件的状态标志
/* |
7. 进程
7.1进程概述
7.1.1 程序与进程
程序占用磁盘资源,但不占用CPU和内存,进程占用CPU和内存,但不占用磁盘。程序是包含一系列信息的文件,这些信息描述了如何在运行时创建一个进程:
- 二进制格式标识:每个程序文件都包含用于描述可执行文件格式的元信息。内核利用此信息来解释文件中的其他信息。(ELF可执行连接格式)
- **机器语言指令:**对程序算法进行编码。
- **程序入口地址:**标识程序开始执行时的起始指令位置。
- **数据:**程序文件包含的变量初始值和程序使用的字面量值(比如字符串)
- **符号表及重定位表:**描述程序中函数和变量的位置及名称。这些表格有多重用途,其中包括调试和运行时的符号解析(动态链接)
- **共享库和动态链接信息:**程序文件所包含的一些字段,列出了程序运行时需要使用的共享库,以及加载共享库的动态连接器的路径名
- **其他信息:**程序文件还包含许多其他信息,用以描述如何创建进程
进程是正在运行的程序的实例。是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。它是操作系统动态执行的基本单元,在传统的操作系统中,进程既是基本的分配单元,也是基本的执行单元。
可以用一个程序来创建多个进程,进程是由内核定义的抽象实体,并为该实体分配用以执行程序的各项系统资源。从内核的角度看,进程由用户内存空间和一系列内核数据结构组成,其中用户内存空间包含了程序代码及代码所使用的变量,而内核数据结构则用于维护进程状态信息。记录在内核数据结构中的信息包括许多与进程相关的标识号(IDs)、虚拟内存表、打开文件的描述符表、信号传递及处理的有关信息、进程资源使用及限制、当前工作目录和大量的其他信息。
7.1.2 单道、多道程序设计
- 单道程序,即在计算机内存中只允许一个的程序运行。
- 多道程序设计技术是在计算机内存中同时存放几道相互独立的程序,使它们在管理程序控制下,相互穿插运行,两个或两个以上程序在计算机系统中同处于开始到结束之间的状态,这些程序共享计算机系统资源。**引入多道程序设计技术的根本目的是为了提高 CPU 的利用率。**
- 对于一个单CPU 系统来说,程序同时处于运行状态只是一种宏观上的概念,他们虽、然都已经开始运行,但就微观而言,任意时刻,CPU上运行的程序只有一个。
- 在多道程序设计模型中,多个进程轮流使用CPU。而当下常见CPU为纳秒级,1秒可以执行大约 10 亿条指令。由于人眼的反应速度是毫秒级,所以看似同时在运行
7.1.3 时间片
时间片(timeslice)又称为“量子(quantum)”或“处理器片(processor slice)是操作系统分配给每个正在运行的进程微观上的一段CPU时间。事实上,虽然一台计算机通常可能有多个 CPU,但是同一个CPU 永远不可能真正地同时运行多个任务。在只考虑一个 CPU 的情况下,这些进程“看起来像”同时运行的,实则是轮番穿插地运行由于时间片通常很短(在inux上为5ms-800ms),用户不会感觉到。
时间片由操作系统内核的调度程序分配给每个进程。首先,内核会给每个进程分配相等的初始时间片,然后每个进程轮番地执行相应的时间,当所有进程都处于时间片耗尽的状态时,内核会重新为每个进程计算并分配时间片,如此往复。
7.1.4 并行和并发
- **并行(paralle1):**指在同一时刻,有多条指令在多个处理器上同时执行。
- **并发(concurrency):**指在同一时刻只能有一条指令执行,但多个进程指令被快速的轮换执行,使得在宏观上具有多个进程同时执行的效果,但在微观上并不是同时执行的只是把时间分成若干段,使多个进程快速交替的执行。
并发是两个队列交替使用一台咖啡机。
并行是两个队列同时使用两台咖啡机。
7.1.5 进程控制块PCB/进程描述符表
- 为了管理进程,内核必须对每个进程所做的事情进行清楚的描述。内核为每个进程分配一个 PCB(Processing Control Block)进程控制块,维护进程相关的信息,Linux内核的进程控制块是 task struct 结构体
- 在/usr/src/linux-headers-xxx/include/linux/sched.h 文件中可以看 struct task struct 结构体定义。其内部成员有很多,我们只需要掌握以部分即可:
- 进程id:系统中每个进程有唯一的id,用pidt类型表示,其实就是一个非负整数
- 进程的状态:有就绪、运行、挂起、停止等状态
- 进程切换时需要保存和恢复的一些CPU寄存器
- 描述虚拟地址空间的信息
- 描述控制终端的信息
- 当前工作目录(Current Working Directory)
- umask 掩码
- 文件描述符表,包含很多指向 file 结构体的指针
- 和信号相关的信息
- 用户 id 和组 id
- 会话(Session)和进程组
- 进程可以使用的资源上限(Resource Limit)
7.2 进程状态转换
Linux上的进程调度策略和算法
Linux的进程调度策略和算法是操作系统管理多任务执行的核心部分,确保CPU资源能够有效分配给多个进程。Linux采用了多种调度算法和策略,旨在平衡系统性能和响应性。下面是Linux中的主要进程调度策略和算法概述: |
7.2.1 进程的状态
- 进程状态反映进程执行过程的变化。这些状态随着进程的执行和外界条件的变化而转换在三态模型中,进程状态分为三个基本状态,即就绪态,运行态,阻塞态。在五态模型中,进程分为新建态、就绪态,运行态,阻塞态,终止态。
- 运行态:进程占有处理器正在运行
- 就绪态:进程具备运行条件,等待系统分配处理器以便运行。当进程已分配到除CPU以外的所有必要资源后,只要再获得CPU,便可立即执行。在一个系统中处于就绪状态的进程可能有多个,通常将它们排成一个队列,称为就绪队列
- 阻塞态:又称为等待(wait)态或睡眠(sleep)态,指进程不具备运行条件,正在等待某个事件的完成
7.2.2 进程相关命令
- 查看所有进程
ps aux/ajx
a: 显示终端上的所有进程,包括其他用户的进程
u: 显示进程的详细信息
x: 显示没有控制终端的进程
j: 列出与作业控制相关的信息
tty
——查看当前进程对应的终端
*STAT参数意义 |
- 实时显示进程动态
top
可以在使用 top 命令时加上 -d 来指定显示信息更新的时间 间隔,在 top 命令执行后,可以按以下按键对显示的结果 进行排序:
M 根据内存使用量排序
P 根据CPU 占有率排序
T 根据进程运行时间长短排序
U 根据用户名来筛选进程
K 输入指定的 PID 杀死进程- 杀死进程
- kill [-signal]pid
- kill -l 列出所有信号
- kill-SIGKILL 进程ID 强制杀死进程
- kill -9 进程ID 强制杀死进程
- killall name 根据进程名杀死进程
- 杀死进程
./xx.xxx(可执行程序) &
——在后台运行程序
7.2.3进程号和相关函数
- 每个进程都由进程号来标识,其类型为pidt(整型),进程号的范围:0~32767进程号总是唯一的,但可以重用。当一个进程终止后,其进程号就可以再次使用。
- 任何进程(除 init 进程)都是由另一个进程创建,该进程称为被创建进程的父进程对应的进程号称为父进程号(PPID)
- 进程组是一个或多个进程的集合。他们之间相互关联,进程组可以接收同一终端的各种信号,关联的进程有一个进程组号(PGID)。默认情况下,当前的进程号会当做当前的进程组号
- 进程号和进程组相关函数:
- pid t getpid(void);
- pid t getppid(void);
- pid t getpgid(pid t pid);
7.3 进程创建
系统允许一个进程创建新进程,新进程即为子进程,子进程还可以创建新的子进程,形成G进程树结构模型。
|
返回值:
成功:子进程中返回 0,父进程中返回子进程ID
失败:返回 -1
失败的两个主要原因:
- 当前系统的进程数已经达到了系统规定的上限,这时errno的值被设置为 EAGAIN
- 系统内存不足,这时errno 的值被设置为NOMEM
/* |
7.4 父子进程虚拟地址空间情况
//程序运行后的结果,父子进程中的num变量互不影响 |
7.5 父子进程关系及GBD多进程调试
7.5.1 GBD多进程调试
使用GDB 调试的时候,GDB 默认只能跟踪一个进程,可以在fork 函数调用之前,通过指令设置 GDB 调试工具跟踪父进程或者是跟踪子进程,默认跟踪父进程。
设置调试父进程或者子进程:set follow-fork-mode [parent(默认)Ichild]
设置调试模式:set detach-on-fork[on | off]`
查看调试进程:show follow-fork-mode
(gdb) show follow-fork-mode |
默认为 on,表示调试当前进程的时候,其它的进程继续运行,如果为f,调试当前进程的时候,其它进程被 GDB 挂起。
查看调试的进程: info inferiors
切换当前调试的进程:inferior id
使进程脱离 GDB调试:detach inferiors id
gdb 文件名
进入gdb调试
gdb l
查看代码
b 行数
在某行打断点
i b
查看断点信息
r
运行程序
n
是单步调试
c
执行完剩下的代码
安装ubuntu16
7.6 exec函数族
——像C++中的函数重载,是一系列功能相同或相似的函数
7.6.1exec函数族介绍
- 函数族的作用是根据指定的文件名找到可执行文件,并用它来取代调用进程的exeC内容,换句话说,就是在调用进程内部执行一个可执行文件。
- exec 函数族的函数执行成功后不会返回,因为调用进程的实体,包括代码段,数据段和堆栈等都已经被新的内容取代,只留下进程ID等一些表面上的信息仍保持原样颇有些神似“三十六计”中的“金蝉脱壳”。看上去还是旧的躯壳,却已经注入了新的灵魂。只有调用失败了,它们才会返回-1,从原程序的调用点接着往下执行。
7.6.2 函数族图解
内核区维护着当前进程的一些信息,比如id、状态、
7.6.4 exec函数族
int execl(const char *path,const char *arg,.../* (char *) NULL */); |
which 程序名
——查看程序所在目录
/* execl函数 |
//结果 |
/* |
7.7 进程控制
7.7.1进程退出exit
status
是进程退出时的一个状态信息。父进程回收子进程资源的时候可以获取到。
/* |
7.7.2孤儿进程
- 父进程运行结束,但子进程还在运行(未运行结束),这样的子进程就称为孤儿进程(Orphan Process)
- 每当出现一个孤儿进程的时候,内核就把孤儿进程的父进程设置为init,而init进程会循环地 wait()它的已经退出的子进程。这样,当一个孤儿进程凄凉地结束了其生命周期的时候,init进程就会代表党和政府出面处理它的一切善后工作。
- 因此孤儿进程并不会有什么危害。
7.7.3僵尸进程
- 每个进程结束之后,都会释放自己地址空间中的用户区数据,内核区的PCB没有办法自己释放掉,需要父进程去释放。
- 进程终止时,父进程尚未回收,子进程残留资源(PCB)存放于内核中,变成僵尸(Zombie)进程。
- 僵尸进程不能被 kill -9 杀死。
- 这样就会导致一个问题,如果父进程不调用wait()或 waitpid()的话,那么保留的那段信息就不会释放,其进程号就会一直被占用,但是系统所能使用的进程号是有限的,如果大量的产生僵尸进程,将因为没有可用的进程号而导致系统不能产生新的进程,此即为僵尸进程的危害,应当避免。
治理僵尸进程:
杀死父进程 kill -9 进程号
7.8 wait函数
7.8.1进程回收
- 在每个进程退出的时候,内核释放该进程所有的资源、包括打开的文件、占用的内存等。但是仍然为其保留一定的信息,这些信息主要主要指进程控制块PCB的信息包括进程号、退出状态、运行时间等)
- 父进程可以通过调用wait或waitpid得到它的退出状态同时彻底清除掉这个进程。
- wait()和 waitpid()函数的功能一样,区别在于,wait()函数会阻塞waitpid()可以设置不阻塞,waitpid()还可以指定等待哪个子进程结束
- 注意:一次wait或waitpid调用只能清理一个子进程,清理多个子进程应使用循环。
7.8.2退出信息相关函数
WIFEXITED(status)非0,进程正常退出 |
/* |
7.9 waitpid函数
|
|
8 进程间通信IPC
8.1进程间通信IPC(进程与进程之间收发数据的过程)
- 进程间通信的方式有哪几种
- 进程间通信某个方式的具体原理
- 进程是一个独立的资源分配单元,不同进程(这里所说的进程通常指的是用户进程)之间的资源是独立的,没有关联,不能在一个进程中直接访问另一个进程的资源。
- 但是,进程不是孤立的,不同的进程需要进行信息的交互和状态的传递等,因此需要进程间通信(IPC:Inter Processes Communication)。
- 进程间通信的目的:
- 数据传输:一个进程需要将它的数据发送给另一个进程。
- 通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)
- 资源共享:多个进程之间共享同样的资源。为了做到这一点,需要内核提供互斥和同步机制。
- 异步可能会带来隐私问题
- 进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变
- GUI——图形用户接口,IDE——集成开发环境,API——应用程序接口
Linux进程间通信的方式
8.2 匿名管道概述
- 管道也叫无名(匿名)管道,它是是UNIX系统IPC(进程间通信)的最古老形式,所有的 UNIX 系统都支持这种通信机制。
- 统计一个目录中文件的数目命令:
ls | wc -l
,为了执行该命令,shell
创建了两个进程来分别执行ls
和wc
。
💥管道的特点
管道其实是一个在内核内存中维护的缓冲器,这个缓冲器的存储能力是有限的,不同的、操作系统大小不一定相同。
管道拥有文件的特质:读操作、写操作,匿名管道没有文件实体,有名管道有文件实体但不存储数据。可以按照操作文件的方式对管道进行操作。
一个管道是一个字节流,使用管道时不存在消息或者消息边界的概念,从管道读取数据的进程可以读取任意大小的数据块,而不管写入进程写入管道的数据块的大小是多少。
通过管道传递的数据是顺序的,从管道中读取出来的字节的顺序和它们被写入管道的顺序是完全一样的。
在管道中的数据的传递方向是单向的,一端用于写入,一端用于读取,管道是半双工的。
从管道读数据是一次性操作,数据一旦被读走,它就从管道中被抛弃,释放空间以便写更多的数据,在管道中无法使用 1seek()来随机的访问数据。
匿名管道只能在具有公共祖先的进程(父进程与子进程,或者两个兄弟进程,具有亲缘关系)之间使用。
管道的数据结构一般是循环队列,通过逻辑手段实现。
匿名管道的使用
//创建匿名管道 |
8.3 父子进程通过匿名管道通信
为什么匿名管道可以用于父子进程间通信
匿名管道(anonymous pipe)可以用于父子进程间通信,主要是因为它提供了一种简单的、无命名的内存缓冲区,可以在同一台计算机的进程之间传递数据。以下是一些关键原因:
继承文件描述符:当父进程创建匿名管道时,会生成两个文件描述符,一个用于读取数据(读端),一个用于写入数据(写端)。在进程创建时,子进程能够继承父进程的文件描述符,这意味着子进程可以直接使用父进程创建的管道进行读写操作。
单向数据流:管道是单向的,数据只能从写端写入,从读端读取。父进程可以写数据到管道中,子进程从管道的另一端读取数据,或者反之。这种模式非常适合在父子进程之间传递数据或消息。
进程间共享内存区域:管道在内核中维护一个缓冲区,父子进程通过管道实现了在内存中的共享访问。数据从一端写入,暂时存储在内核缓冲区中,另一端可以在需要时读取数据。这使得管道成为一种高效的进程间通信方式。
进程间同步:通过管道的阻塞特性,父子进程可以同步操作。如果一端试图从空管道中读取数据,它会阻塞,直到另一端写入数据;同样,如果一端试图向已满的管道写入数据,它会阻塞,直到有空间可用。这可以实现进程间的简单同步机制。
简单高效:匿名管道的创建和使用相对简单,操作系统直接提供了系统调用(如
pipe()
)来创建管道,并通过标准 I/O 函数(如read()
和write()
)进行数据读写,不需要显式的命名或配置。因此,匿名管道因其高效、简单和父子进程之间文件描述符继承的特性,成为进程间通信(IPC)的常用方式之一。
/* |
匿名管道通信案例
/* |
8.4 管道的读写特点和管道设置为非阻塞
管道的读写特点:
使用管道时,需要注意以下几种特殊的情况(假设都是阻塞I/O操作)
1.所有的指向管道写端的文件描述符都关闭了(管道写端引用计数为0),有进程从管道的读端
读数据,那么管道中剩余的数据被读取以后,再次read会返回0,就像读到文件末尾一样。
2.如果有指向管道写端的文件描述符没有关闭(管道的写端引用计数大于0),而持有管道写端的进程
也没有往管道中写数据,这个时候有进程从管道中读取数据,那么管道中剩余的数据被读取后,
再次read会阻塞,直到管道中有数据可以读了才读取数据并返回。
3.如果所有指向管道读端的文件描述符都关闭了(管道的读端引用计数为0),这个时候有进程
向管道中写数据,那么该进程会收到一个信号SIGPIPE, 通常会导致进程异常终止。
4.如果有指向管道读端的文件描述符没有关闭(管道的读端引用计数大于0),而持有管道读端的进程
也没有从管道中读数据,这时有进程向管道中写数据,那么在管道被写满的时 候再次write会阻塞,
直到管道中有空位置才能再次写入数据并返回。
总结:
读管道:
管道中有数据,read返回实际读到的字节数。
管道中无数据:
写端被全部关闭,read返回0(相当于读到文件的末尾)
写端没有完全关闭,read阻塞等待
写管道:
管道读端全部被关闭,进程异常终止(进程收到SIGPIPE信号)
管道读端没有全部关闭:
管道已满,write阻塞
管道没有满,write将数据写入,并返回实际写入的字节数
|
8.5 有名管道
匿名管道,由于没有名字,只能用于亲缘关系的进程间通信。为了克服这个缺点,提出了有名管道(FIFO),也叫命名管道、FIFO文件。
有名管道(FIFO)不同于匿名管道之处在于它提供了一个路径名与之关联,以 FIFO的文件形式存在于文件系统中,并且其打开方式与打开一个普通文件是一样的,这样即使与 FIFO 的创建进程不存在亲缘关系的进程,只要可以访问该路径,就能够彼此通过 FIFO 相互通信,因此,通过FIFO 不相关的进程也能交换数据。
一旦打开了 FIFO,就能在它上面使用与操作匿名管道和其他文件的系统调用一样的I/0系统调用了(如read()、write()和close())。与管道一样,FIFO 也有一个写入端和读取端,并且从管道中读取数据的顺序与写入的顺序是一样的。FIFO 的名称也由此而来:先入先出。
有名管道(FIFO)和匿名管道(pipe)有一些特点是相同的,不一样的地方在于
1. FIFO 在文件系统中作为一个特殊文件存在,但FIFO 中的内容却存放在内存中。(即内核的一个缓冲区)
2. 当使用 FIFO 的进程退出后,FIFO 文件将继续保存在文件系统中以便以后使用。
3. FIFO 有名字,不相关的进程可以通过打开有名管道进行通信。
有名管道的使用
- 通过命令创建有名管道
mkfifo 名字 - 通过函数创建有名管道
#include <sys/types.h>
#include <sys/stat.h>
*int mkfifo(const char pathname,mode_tmode); - 一旦使用 mkfifo 创建了一个 FIFO,就可以使用 open 打开它,常见的文件I/0 函数都可用于 fifo。如:close、read、write、unlink等
- FIFO 严格遵循先进先出(Firstin First out),对管道及FIFO 的读总是从开始处返回数据,对它们的写则把数据添加到末尾。它们不支持诸如lseek()等文件定位操作。
/*
创建fifo文件
1.通过命令: mkfifo 名字
2.通过函数:int mkfifo(const char *pathname, mode_t mode);
|
参数:
pathname: 管道名称的路径
mode: 文件的权限 和 open 的 mode 是一样,是一个八进制的数
返回值:成功返回0,失败返回-1,并设置错误号
*/
有名管道注意事项
1. 一个为只读而打开的一个管道的进程会阻塞,直到另外一个进程为只写打开管道
2. 一个为只写而打开的一个管道的进程会阻塞,直到另外一个进程为只读打开管道
读管道:
管道中有数据,read返回实际读到的字节数
管道中无数据:
管道写端被全部关闭,read返回0,(相当于读到文件末尾)
写端没有全部被关闭,read阻塞等待
写管道:
管道读端被全部关闭,进行异常终止(收到一个SIGPIPE信号)
管道读端没有全部关闭:
管道已经满了,write会阻塞
管道没有满,write将数据写入,并返回实际写入的字节数。
模拟发送对话
//chatA |
//chatB |
8.6 内存映射
//内存映射相关的系统调用 |
/*mmap-parent-child-ipc |
内存映射的注意事项
1.如果对mmap的返回值(ptr)做++操作(ptr++), munmap是否能够成功?
void * ptr = mmap(…);
ptr++; 可以对其进行++操作
munmap(ptr, len); // 错误,要保存地址
2.如果open时O_RDONLY, mmap时prot参数指定PROT_READ | PROT_WRITE会怎样?
错误,返回MAP_FAILED
open()函数中的权限建议和prot参数的权限保持一致。
3.如果文件偏移量为1000会怎样?
偏移量必须是4K的整数倍,返回MAP_FAILED
4.mmap什么情况下会调用失败?
- 第二个参数:length = 0
- 第三个参数:prot
- 只指定了写权限
- prot PROT_READ | PROT_WRITE
第5个参数fd 通过open函数时指定的 O_RDONLY / O_WRONLY
5.可以open的时候O_CREAT一个新文件来创建映射区吗?
- 可以的,但是创建的文件的大小如果为0的话,肯定不行
- 可以对新的文件进行扩展
- lseek()
- truncate()
6.mmap后关闭文件描述符,对mmap映射有没有影响?
int fd = open(“XXX”);
mmap(,,,,fd,0);
close(fd);
映射区还存在,创建映射区的fd被关闭,没有任何影响。
7.对ptr越界操作会怎样?
void * ptr = mmap(NULL, 100,,,,,);
4K
越界操作操作的是非法的内存 -> 段错误
// 使用内存映射实现文件拷贝的功能 |
/* |
9.信号概述
9.1信号的概念
信号是 inux 进程间通信的最古老的方式之一,是事件发生时对进程的通知机制,有时也称之为软件中断,它是在软件层次上对中断机制的一种模拟,是一种异步通信的方式。信号可以导致一个正在运行的进程被另一个正在运行的异步进程中断,转而处理某一个突发事件
发往进程的诸多信号,通常都是源于内核。引发内核为进程产生信号的各类事件如下:
- 对于前台进程,用户可以通过输入特殊的终端字符来给它发送信号。比如输入ctr1+C通常会给进程发送一个中断信号。
- 硬件发生异常,即硬件检测到一个错误条件并通知内核,随即再由内核发送相应信号给相关进程。比如执行一条异常的机器语言指令,诸如被0除,或者引用了无法访问的内存区域。
- 系统状态变化,比如 alarm定时器到期将引起SIGALRM 信号,进程执行的CPU时间超限,或者该进程的某个子进程退出。
- 运行 kill 命令或调用 kill 函数。
使用信号的两个主要目的是
- 让进程知道已经发生了一个特定的事情。
- 强迫进程执行它自己代码中的信号处理程序。
信号的特点
- 简单
- 不能携带大量信息
- 满足某个特定条件才发送
- 优先级比较高
- 查看系统定义的信号列表:
kill -l
- 前 31 个信号为常规信号,其余为实时信号
Linux信号一览表
信号的5种默认处理动作
查看信号的详细信息: man 7 signal
信号的 5 中默认处理动作
Termk 终止进程
Ign 当前进程忽略掉这个信号
Core 终止进程,并生成一个Core文件
Stop 暂停当前进程
Cont 继续执行当前被暂停的进程
信号的几种状态:产生、未决、递达
SIGKILL和 SIGSTOP 信号不能被捕捉、阻塞或者忽略,只能执行默认动作
进入gdb查看错误:core-file core
9.2kill、raise、abort函数
/* |
9.3alarm 函数
/* |
实际的时间 = 内核时间 + 用户时间 + 消耗的时间
进行文件IO操作的时候比较浪费时间
定时器,与进程的状态无关(自然定时法)。无论进程处于什么状态,alarm都会计时。
9.4signal信号捕捉函数
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
- 功能:设置某个信号的捕捉行为:
- 参数- signum: 要捕捉的信号
- handler: 捕捉到信号要如何处理
- SIG_IGN : 忽略信号
- SIG_DFL : 使用信号默认的行为
- 回调函数 : 这个函数是内核调用,程序员只负责写,捕捉到信号后如何去处理信号。
回调函数:
- 需要程序员实现,提前准备好的,函数的类型根据实际需求,看函数指针的定义
- 不是程序员调用,而是当信号产生,由内核调用
- 函数指针是实现回调的手段,函数实现之后,将函数名放到函数指针的位置就可以了。
- 返回值:
成功,返回上一次注册的信号处理函数的地址。第一次调用返回NULL
失败,返回SIG_ERR,设置错误号
SIGKILL SIGSTOP不能被捕捉,不能被忽略。
|
9.5信号集及其相关函数
信号集
- 许多信号相关的系统调用都需要能表示一组不同的信号,多个信号可使用一个称之为信号集的数据结构来表示,其系统数据类型为 sigset_t。
- 在 PCB 中有两个非常重要的信号集。一个称之为“阻塞信号集”,另一个称之为“未决信号集”。这两个信号集都是内核使用位图机制来实现的。但操作系统不允许我们直接对这两个信号集进行位操作。而需自定义另外一个集合,借助信号集操作函数来对 PCB 中的这两个信号集进行修改。
- 信号的“未决”是一种状态,指的是从信号的产生到信号被处理前的这一段时间
- 信号的“阻塞”是一个开关动作,指的是阻止信号被处理,但不是阻止信号产生
- 信号的阻塞就是让系统暂时保留信号留待以后发送。由于另外有办法让系统忽略信号所以一般情况下信号的阻塞只是暂时的,只是为了防止信号打断敏感的操作。
1.用户通过键盘 Ctrl + C, 产生2号信号SIGINT (信号被创建)
2.信号产生但是没有被处理 (未决)
- 在内核中将所有的没有被处理的信号存储在一个集合中 (未决信号集)
- SIGINT信号状态被存储在第二个标志位上
- 这个标志位的值为0, 说明信号不是未决状态
- 这个标志位的值为1, 说明信号处于未决状态
3.这个未决状态的信号,需要被处理,处理之前需要和另一个信号集(阻塞信号集),进行比较
- 阻塞信号集默认不阻塞任何的信号
- 如果想要阻塞某些信号需要用户调用系统的API
4.在处理的时候和阻塞信号集中的标志位进行查询,看是不是对该信号设置阻塞了
如果没有阻塞,这个信号就被处理
如果阻塞了,这个信号就继续处于未决状态,直到阻塞解除,这个信号就被处理
以下信号集相关的函数都是对自定义的信号集进行操作。
int sigemptyset(sigset_t * set);
- 功能:清空信号集中的数据,将信号集中的所有的标志位置为0:
- set,传出参数,需要操作的信号集
- 返回值:成功返回0, 失败返回-1
int sigfillset(sigset_t *set);
- 功能:将信号集中的所有的标志位置为1
- 参数
* set,传出参数,需要操作的信号集
- 返回值:成功返回0, 失败返回-1
int sigaddset(sigset_t *set, int signum);
- 功能:设置信号集中的某一个信号对应的标志位为1,表示阻塞这个信号:
- 参数- set:传出参数,需要操作的信号集
- signum:需要设置阻塞的那个信号
- 返回值:成功返回0, 失败返回-1
int sigdelset(sigset_t *set, int signum);
- 功能:设置信号集中的某一个信号对应的标志位为0,表示不阻塞这个信号:
- 参数- set:传出参数,需要操作的信号集
- signum:需要设置不阻塞的那个信号
- 返回值:成功返回0, 失败返回-1
int sigismember(const sigset_t *set, int signum);
- 功能:判断某个信号是否阻塞:
- 参数- set:需要操作的信号集
- signum:需要判断的那个信号
- 返回值:
1 : signum被阻塞
0 : signum不阻塞,不在信号集中
-1 : 失败
sigprocmask函数
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
- 功能:将自定义信号集中的数据设置到内核中(设置阻塞,解除阻塞,替换):
- 参数- how : 如何对内核阻塞信号集进行处理
SIG_BLOCK: 将用户设置的阻塞信号集添加到内核中,内核中原来的数据不变
假设内核中默认的阻塞信号集是mask, mask | set
SIG_UNBLOCK: 根据用户设置的数据,对内核中的数据进行解除阻塞
mask &= ~set
SIG_SETMASK:覆盖内核中原来的值
- set :已经初始化好的用户自定义的信号集
- oldset : 保存设置之前的内核中的阻塞信号集的状态,可以是 NULL
- 返回值:
成功:0
失败:-1
设置错误号:EFAULT、EINVAL
int sigpending(sigset_t *set);
- 功能:获取内核中的未决信号集:
- set,传出参数,保存的是内核中的未决信号集中的信息。
// 编写一个程序,把所有的常规信号(1-31)的未决状态打印到屏幕 |
后台运行—— ./可执行文件名 &
fg
指令切换到前台
sigaction
#include <signal.h>
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
- 功能:检查或者改变信号的处理。信号捕捉:
- 参数- signum : 需要捕捉的信号的编号或者宏值(信号的名称)
- act :捕捉到信号之后的处理动作
- oldact : 上一次对信号捕捉相关的设置,一般不使用,传递NULL
- 返回值:
成功 0
失败 -1
struct sigaction {
// 函数指针,指向的函数就是信号捕捉到之后的处理函数
void (*sa_handler)(int);
1️⃣
// 不常用
void (*sa_sigaction)(int, siginfo_t *, void *);
2️⃣
// 临时阻塞信号集,在信号捕捉函数执行过程中,临时阻塞某些信号。
sigset_t sa_mask;
// 使用哪一个信号处理对捕捉到的信号进行处理1️⃣或者2️⃣
// 这个值可以是0,表示使用sa_handler
1️⃣,也可以是SA_SIGINFO
表示使用sa_sigaction
2️⃣
int sa_flags;
// 被废弃掉了
void (*sa_restorer)(void);
};
|
SIGCHLD
SIGCHLD信号产生的3个条件:
1.子进程结束
2.子进程暂停了
3.子进程继续运行
都会给父进程发送该信号,父进程默认忽略该信号。
使用SIGCHLD信号解决僵尸进程的问题。
|
10共享内存
共享内存允许两个或者多个进程共享物理内存的同一块区域(通常被称为段)。由于一个共享内存段会称为一个进程用户空间的一部分,因此这种PC 机制无需内核介入。所有需要做的就是让一个进程将数据复制进共享内存中,并且这部分数据会对其他所有共享同一个段的进程可用。
与管道等要求发送进程将数据从用户空间的缓冲区复制进内核内存和接收进程将数据从内核内存复制进用户空间的缓冲区的做法相比,这种IPC技术的速度更快。
共享内存使用步骤
- Ø 调用 shmget()创建一个新共享内存段或取得一个既有共享内存段的标识符(即由其他进程创建的共享内存段)。这个调用将返回后续调用中需要用到的共享内存标识符
- Ø 使用 shmat()来附上共享内存段,即使该段成为调用进程的虚拟内存的一部分。
- Ø 此刻在程序中可以像对待其他可用内存那样对待这个共享内存段。为引用这块共享内存程序需要使用由 shmat()调用返回的 addr 值,它是一个指向进程的虚拟地址空间中该共享内存段的起点的指针。
- Ø 调用 shmat()来分离共享内存段。在这个调用之后,进程就无法再引用这块共享内存了。这一步是可选的,并且在进程终止时会自动完成这一步。
- Ø 调用 shmctl()来删除共享内存段。只有当当前所有附加内存段的进程都与之分离之后内存段才会销毁。只有一个进程需要执行这一步。
共享内存相关的函数 |
共享内存操作命令
ipcs 用法 |
共享内存的键变为0,表示标注删除
守护进程
终端
在UNIX系统中,用户通过终端登录系统后得到一个she1l进程,这个终端成为 shell 进程的控制终端(Controlling Terminal),进程中,控制终端是保存在 PCB 中的信息,而fork()会复制PCB 中的信息,因此由shell 进程启动的其它进程的控制终端也是这个终端。
默认情况下(没有重定向),每个进程的标准输入、标准输出和标准错误输出都指向控制终端,进程从标准输入读也就是读用户的键盘输入,进程往标准输出或标准错误输出写也就是输出到显示器上。
在控制终端输入一些特殊的控制键可以给前台进程发信号,例如Ctr1+C会产生SIGINT信号,Ctrl+\会产生SIGQUIT信号
echo$$ 查看当前终端的进程号;
Ps aux
tty 查看当前终端设备
进程组
进程组和会话在进程之间形成了一种两级层次关系:进程组是一组相关进程的集合会话是一组相关进程组的集合。进程组合会话是为支持she11作业控制而定义的抽象概念,用户通过 she11 能够交互式地在前台或后台运行命令。
进行组由一个或多个共享同一进程组标识符(PGID)的进程组成。一个进程组拥有一个进程组首进程,该进程是创建该组的进程,其进程ID为该进程组的ID,新进程会继承其父进程所属的进程组 ID。
进程组拥有一个生命周期,其开始时间为首进程创建组的时刻,结束时间为最后一个成员进程退出组的时刻。一个进程可能会因为终止而退出进程组,也可能会因为加入了另外一个进程组而退出进程组。进程组首进程无需是最后一个离开进程组的成员。
会话
会话是一组进程组的集合。会话首进程是创建该新会话的进程,其进程ID会成为会话 ID。新进程会继承其父进程的会话 ID。
一个会话中的所有进程共享单个控制终端。控制终端会在会话首进程首次打开一个终端设备时被建立。一个终端最多可能会成为一个会话的控制终端。
在任一时刻,会话中的其中一个进程组会成为终端的前台进程组,其他进程组会成为后台进程组。只有前台进程组中的进程才能从控制终端中读取输入。当用户在控制终端中输入终端字符生成信号后,该信号会被发送到前台进程组中的所有成员。
当控制终端的连接建立起来之后,会话首进程会成为该终端的控制进程.
进程组、会话、控制终端之间的关系
进程组、会话操作函数
守护进程
守护进程(Daemon Process),也就是通常说的Daemon进程(精灵进程),是Linux 中的后台服务进程。它是一个生存期较长的进程,通常独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。一般采用以 d结尾的名字。
守护进程具备下列特诊:
口 生命周期很长,守护进程会在系统启动的时候被创建并一直运行直至系统被关闭
口 它在后台运行并且不拥有控制终端。没有控制终端确保了内核永远不会为守护进程自动生成任何控制信号以及终端相关的信号(如SIGINT、SIGQUIT)。
Linux的大多数服务器就是用守护进程实现的。比如,Internet服务器inetd.web 服务器 httpd 等。
守护进程的创建步骤
- 执行一个
fork()
,之后父进程退出,子进程继续执行。 - 子进程调用
setsid()
开启一个新会话。 - 清除进程的 umask 以确保当守护进程创建文件和目录时拥有所需的权限修改进程的当前工作目录,通常会改为根目录(/)。
- 关闭守护进程从其父进程继承而来的所有打开着的文件描述符。
- 在关闭了文件描述符0、1、2之后,守护进程通常会打开/dev/nu11 并使用
dup2()
使所有这些描述符指向这个设备。 - 核心业务逻辑
/* |
线程
线程概述
与进程(process)类似,线程(thread)是允许应用程序并发执行多个任务的一种机制。一个进程可以包含多个线程。同一个程序中的所有线程均会独立执行相同程序,且共享同一份全局内存区域,其中包括初始化数据段、未初始化数据段,以及堆内存段。(传统意义上的UNIX 进程只是多线程程序的一个特例,该进程只包含一个线程)。
◼ 进程是 CPU 分配资源的最小单位,线程是操作系统调度执行的最小单位。
◼ 线程是轻量级的进程(LWP: Light Weight Process),在 Linux
环境下线程的本质仍是进程。
◼ 查看指定进程的 LWP 号: ps –Lf pid
线程和进程区别
◼ 进程间的信息难以共享。由于除去只读代码段外,父子进程并未共享内存,因此必须采用一些进程间通信方式,在进程间进行信息交换。
◼ 调用 fork() 来创建进程的代价相对较高,即便利用写时复制技术,仍然需要复制诸如内存页表和文件描述符表之类的多种进程属性,这意味着 fork()
调用在时间上的开销依然不菲。
◼ 线程之间能够方便、快速地共享信息。只需将数据复制到共享(全局或堆)变量中即可。
◼ 创建线程比创建进程通常要快 10 倍甚至更多。线程间是共享虚拟地址空间的,无需采用写时复制来复制内存,也无需复制页表。
线程和进程虚拟地址空间
线程之间共享和非共享资源
线程操作
◼ int pthread_create(pthread_t *thread, const pthread_attr_t *attr,void *(*start_routine) (void *), void *arg);
◼ pthread_t pthread_self(void);
◼ int pthread_equal(pthread_t t1, pthread_t t2);
◼ void pthread_exit(void *retval);
◼ int pthread_join(pthread_t thread, void **retval);
◼ int pthread_detach(pthread_t thread);
◼ int pthread_cancel(pthread_t thread);
线程属性
◼ 线程属性类型 pthread_attr_t
◼ int pthread_attr_init(pthread_attr_t *attr);
◼ int pthread_attr_destroy(pthread_attr_t *attr);
◼ int pthread_attr_getdetachstate(const pthread_attr_t *attr, int* detachstate);
◼ int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);
pthread_create
/* |
Pthread_exit
/* |
Pthread_join
/* |
pthread_detach
/* |
pthread_cancel
/* |
pthread_attr
/* |
线程同步
◼ 线程的主要优势在于,能够通过全局变量来共享信息。不过,这种便捷的共享是有代价的:必须确保多个线程不会同时修改同一变量,或者某一线程不会读取正在由其他线程修改的变量。
◼ 临界区是指访问某一共享资源的代码片段,并且这段代码的执行应为原子操作,也就是同时访问同一共享资源的其他线程不应终端该片段的执行。
◼ 线程同步:即当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作,其他线程才能对该内存地址进行操作,而其他线程则处于等待状态。
互斥量
◼ 为避免线程更新共享变量时出现问题,可以使用互斥量(mutex 是 mutual exclusion的缩写)来确保同时仅有一个线程可以访问某项共享资源。可以使用互斥量来保证对任意共享资源的原子访问。
◼ 互斥量有两种状态:已锁定(locked)和未锁定(unlocked)。任何时候,至多只有一个线程可以锁定该互斥量。试图对已经锁定的某一互斥量再次加锁,将可能阻塞线程或者报错失败,具体取决于加锁时使用的方法。
◼ 一旦线程锁定互斥量,随即成为该互斥量的所有者,只有所有者才能给互斥量解锁。一般情况下,对每一共享资源(可能由多个相关变量组成)会使用不同的互斥量,每一线程在访问
同一资源时将采用如下协议:
- 针对共享资源锁定互斥量
- 访问共享资源
- 对互斥量解锁
◼ 如果多个线程试图执行这一块代码(一个临界区),事实上只有一个线程能够持有该互斥量(其他线程将遭到阻塞),即同时只有一个线程能够进入这段代码区域,如下图所示:
/* |
互斥量相关操作函数
◼ 互斥量的类型 pthread_mutex_t
◼ int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr);
◼ int pthread_mutex_destroy(pthread_mutex_t *mutex);
◼ int pthread_mutex_lock(pthread_mutex_t *mutex);
◼ int pthread_mutex_trylock(pthread_mutex_t *mutex);
◼ int pthread_mutex_unlock(pthread_mutex_t *mutex);
/* |
死锁
◼ 有时,一个线程需要同时访问两个或更多不同的共享资源,而每个资源又都由不同的互斥量管理。当超过一个线程加锁同一组互斥量时,就有可能发生死锁。
◼ 两个或两个以上的进程在执行过程中,因争夺共享资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁。
死锁的几种场景:
忘记释放锁
重复加锁
多线程多锁,抢占锁资源
deadlock.c
/* |
deadlock1.c
|
读写锁
◼ 当有一个线程已经持有互斥锁时,互斥锁将所有试图进入临界区的线程都阻塞住。但是考虑一种情形,当前持有互斥锁的线程只是要读访问共享资源,而同时有其它几个线程也想读取这个共享资源,但是由于互斥锁的排它性,所有其它线程都无法获取锁,也就无法读访问共享资源了,但是实际上多个线程同时读访问共享资源并不会导致问题。
◼ 在对数据的读写操作中,更多的是读操作,写操作较少,例如对数据库数据的读写应用。为了满足当前能够允许多个读出,但只允许一个写入的需求,线程提供了读写锁来实现。
◼ 读写锁的特点:
如果有其它线程读数据,则允许其它线程执行读操作,但不允许写操作。
如果有其它线程写数据,则其它线程都不允许读、写操作。
写是独占的,写的优先级高。
读写锁相关操作函数
◼ 读写锁的类型 pthread_rwlock_t
◼ int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,const pthread_rwlockattr_t *restrict attr);
◼ int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
◼ int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
◼ int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
◼ int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
◼ int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
◼ int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
/* |
生产者消费者模型
Linux网络编程
网络结构模式
C/S结构
简介
服务器 - 客户机,即 Client - Server(C/S)结构。C/S 结构通常采取两层结构。服务器负责数据的管理,客户机负责完成与用户的交互任务。客户机是因特网上访问别人信息的机器,服务器则是提供信息供人访问的计算机。
客户机通过局域网与服务器相连,接受用户的请求,并通过网络向服务器提出请求,对数据库进行操作。服务器接受客户机的请求,将数据提交给客户机,客户机将数据进行计算并将结果呈现给用户。服务器还要提供完善安全保护及对数据完整性的处理等操作,并允许多个客户机同时访问服务器,这就对服务器的硬件处理数据能力提出了很高的要求。
在C/S结构中,应用程序分为两部分:服务器部分和客户机部分。服务器部分是多个用户共享的信息与功能,执行后台服务,如控制共享数据库的操作等;客户机部分为用户所专有,负责执行前台功能,在出错提示、在线帮助等方面都有强大的功能,并且可以在子程序间自由切换。
优点
- 能充分发挥客户端 PC 的处理能力,很多工作可以在客户端处理后再提交给服务器,所以 C/S 结构客户端响应速度快;
- 操作界面漂亮、形式多样,可以充分满足客户自身的个性化要求;
- C/S 结构的管理信息系统具有较强的事务处理能力,能实现复杂的业务流程;
- 安全性较高,C/S 一般面向相对固定的用户群,程序更加注重流程,它可以对权限进行多层次校验,提供了更安全的存取模式,对信息安全的控制能力很强,一般高度机密的信息系统采用 C/S 结构适宜。
缺点
- 客户端需要安装专用的客户端软件。首先涉及到安装的工作量,其次任何一台电脑出问题,如病毒、硬件损坏,都需要进行安装或维护。系统软件升级时,每一台客户机需要重新安装,其维护和升级成本非常高;
- 对客户端的操作系统一般也会有限制,不能够跨平台。
B/S结构
简介
B/S 结构(Browser/Server,浏览器/服务器模式),是 WEB 兴起后的一种网络结构模式,WEB浏览器是客户端最主要的应用软件。这种模式统一了客户端,将系统功能实现的核心部分集中到服务器上,简化了系统的开发、维护和使用。客户机上只要安装一个浏览器,如 Firefox 或 Internet Explorer,服务器安装 SQL Server、Oracle、MySQL 等数据库。浏览器通过 Web Server 同数据库进行数据交互。
优点
- B/S 架构最大的优点是总体拥有成本低、维护方便、 分布性强、开发简单,可以不用安装任何专门的软件就能实现在任何地方进行操作,客户端零维护,系统的扩展非常容易,只要有一台能上网的电脑就能使用。
缺点
- 通信开销大、系统和数据的安全性较难保障;
- 个性特点明显降低,无法实现具有个性化的功能要求;
- 协议一般是固定的:http/https;
- 客户端服务器端的交互是请求-响应模式,通常动态刷新页面,响应速度明显降低;