Linux基础
第一章 概述
1 前言
本章讨论系统的概念,从硬件、操作系统角度更加深刻的理解计算机系统,并快速浏览Linux系统提供的服务。
2 系统组成
3 操作系统和应用程序
操作系统这个词语有二意性,有时候指内核,有时候指内核和系统工具软件的组合。
操作系统是管理系统硬件的软件。操作系统是直接运行在裸机之上。其他应用软件运行在操作系统之上。
操作系统本身提供操作接口,支持用户通过该接口来操作 系统,但是系统本身提供的功能,不足于完成用户需求时,则需要开发应用程序来拓展系统功能。
发行版:
不同的公司使用Linux内核,加上自己开发的系统工具软件,一起发布的Linux操作系统版本。
4 启动和登陆
配置文件:
/etc/profile:系统启动时被执行
~/.bashrc:用户登陆时会调用
5 文件
文件是一个重要的概念,一般定义为信息的集合。计算机做为信息处理的机器,文件是计算机处理的对象。
在Unix和Linux系统中,泛化了文件的概念,设备也被抽象成文件对象来进行操作。
数据的集合叫做文件。
IT行业处理信息:转换,传输,存储
6 程序、进程
7 错误处理
系统调用在一般情况下返回整数,并且0表示成功,小于0表示失败。当系统调用返回失败时,可以通过errno获得错误嘛,通过strerror获取错误解释,或者直接通过perror在标准错误文件中,输出错误信息。
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
// 通过open返回的整数,在linux中有个特别的名字
// 叫文件描述符 file description 简称fd
int fd = open("a.txt", O_WRONLY|O_CREAT|O_EXCL, 0777);
if(fd < 0)//表示文件打开失败
{
perror("open");
return 0;
}
// 把指针放到文件开头
lseek(fd, 0, SEEK_SET);
// 对文件进行操作
write(fd, "hello", 5);
// 关闭文件,如果不关闭,内存会泄漏
// 当进程退出时,未关闭的文件会自动关闭
close(fd);
}
8 用户、组、文件权限
Linux是多用户系统,支持多个用户同时登陆系统。
为了安全起见,需要对系统的权限加于规范。
9 信号
信号是进程通信的一种手段,某个进程收到信号,该信号可能来自内核、来自其它进程或者来自用户操作。例如:当用户按下ctrl+c时,其实是给前台进程发送了一个信号。
10 系统调用和库函数
学习Linux系统开发接口时,程序员也需要学习一般常用的第三方库,来拓展程序员的编程能力。
User Space和Kernel Space是操作系统编程中常用的概念,表示当前的代码在用户空间还是内核空间运行,对于不同的运行空间,CPU对内存的处理方式稍有不同,在讲进程虚拟地址空间时再涉及该概念。
系统调用指操作系统内核提供的功能,它提供了接口给用户空间代码调用。比如open/read/write/close等,都是属于Linux系统操作接口,而fopen/fread/fwrite/fclose是属于C标准提供的接口,在Linux下,fopen其实底层调用了open。
配置文件:
/etc/profile:系统启动时被执行
~/.bashrc:用户登陆时会调用
11 文件操作
头文件:sys/types.h sys/stat.h fcntl.h 例:int fd=open(“文件路径”,mode); mode决定了对文件的操作方式 第三个参数可有可无,对文件权限进行处理, 因umask存在,创建文件权限要与上000 000 010的反,导致用户权限开始不能有写的权限
mode选项 |
解释 |
O_RDONLY |
读方式打开(与后面俩个互斥) |
O_WRONLY |
写方式打开 |
O_RDWR |
读写方式打开 |
O_CREAT |
创建文件,如果文件存在,直接打开 |
O_TRUNC |
截断 |
O_APPEND |
追加 |
O_EXCL |
和O_CREAT一起用,如果文件存在则失败 |
函数:
perror:对某种错误信息进行打印
open/creat:打开文件/创建文件
read:读文件
write:写文件
close:关闭文件
lseek:定位文件读写位置
fcntl:修改文件属性
sysconf:读取系统配置
dup/dup2:复制文件描述符
sync/fsync/fsyncdata:同步文件数据
mmap/munmap:文件映射
mkstemp:得到临时文件路径
命令
touch:修改文件的访问时间,创建文件
cat:访问文件内容
vim:编辑
ulimit:显示一些限制信息(文件描述符最大值、栈的空间尺寸)
umask:文件创建的权限掩码
getconf:对应sysconf
dd:可以拷贝块设备,但是要sudo权限 例 dd if=位置 of=文件名 bs=一次多少k cout=拷贝次数
Wc:计算文件的行数 单词个数 字节数
unlink:删除软链接
12 信号
是控制进程通信的一种方式,效率高,成本低
信号处理方式:掩盖、忽略、默认处理
掩码:延迟信号的处理 运用信号集合
掩盖不可靠信号,多次发送,只处理一次 掩盖:可靠信号 处理多次
进程
fork()创建
13 线程
鼠标键盘都是只读的字符文件夹设备,所以可以运用函数进行监控 一般在/dev/input/mic 文件下面 注意权限问题 鼠标键盘读取数据,是俩个进程,注意进程的阻塞问题 可以运用字进程和父进程进行处理
13.1 线程的创建
pthread_created(1,2,3,4) //1:线程的id 2:线程的的属性 3:新线程的函数名字, 4:新线程的属性 要链接 -lpthread 库
注意子线程是依附主线程的,主线程结束,子线程无法运行 这个 pthread_exit(0)主线程结束,子线程没有退出例外
运用pthread_equal 判断线程是否相等,先等返回0 不相等返回非零值
pthread_jion(1,&ret) 阻塞调用 1:线程id ret:线程返回值
pthread_t tid = pthread_self() 得到当前运行进程的id
13.2 进程和线程的区别:
进程:分配资源的单位 线程:调度的单位 多线程可以共享全局变量
13 锁
避免俩个线程同时操作全局变量,第一个线程运用了锁,后面的线程在外面等,等待解锁后,后面的线程在进来
13.1死锁
连续俩次加锁,加锁后,没有解锁,又继续加锁,会导致死锁。 运用循环锁,可以重复加锁 通过定义锁的属性,变为循环锁 例:pthread_mutexattr_t attr; pthread_mutexattr_init(&attr); pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);pthread_mutex_init(&mutex,&attr)
加锁后,忘记解锁,也会出现死锁 C++中运用析构函数,可以避免忘记解锁,定义一个类
13.2读写锁
pthread_rwlock_t mutex;
pthread_rwlock_init(&mutex, NULL);
读/写锁定pthread_rwlock_rd/wrlock(&mutex);
解锁:pthread_rwlock_unlock(&mutex);
14 守护进程
守护进程不和终端关联,注意此进程只能有一个,创建文件记录,判断此程序是否开启
编程规则:
设umask=0;
调用fork,让父进程退出。 让父进程变为init, 如果父进程不退出,用俩次fork()
调用setuid创建新会话 setsid
重设当前目录/根目录 chdir
关闭不需要的文件描述符 运用循环关闭所有文件描述符
15 高级IO
一个进程就是一段指令
15.1 IO复用技术
15.1.1 select的运用
运用文件描述符集合 运用fd_set创建文件描述符集合 文件接口相对较小,跨平台运用
FD_SET(1,2) 将文件描述符放入文件描述符集合 1:文件描述符 2:集合名字
15.1.2epoll的运用
epollfd 创建文件描述符集合
epol_ctl将文件描述加入集合中
15.2 非阻塞IO
16 管道
一边读,一边写
16.1 匿名管道 pipe()创建管道
mmap 可以实现有亲子关系进程的文件共享 效率低,数据写入内存,在从内存中读取数据 运用shm_open实现文件共享也可以
文件内存共享,无法进行通信
通过锁,让进程共享内存进行通信 pthread_mutex_init 需要将锁放在共享内存中
fork + exec 让进程有不同的功能
第二章 文件IO
1 前言
本章讨论普通文件的读写、读写效率、简单介绍文件描述符、IO效率、文件共享和原子操作、dup、文件映射、临时文件。
2 文件描述符
在Linux系统中,打开的文件是用一个整数来表示的,表示打开文件的整数,称之为文件描述符。当需要往写数据/读数据时,读写函数都需要文件描述符作为参数,以便系统知道用户操作的时哪个文件。
3 文件基本操作
3.1 open/creat
mode选项 | 解释 |
---|---|
O_RDONLY | 读方式打开 |
O_WRONLY | 写方式打开 |
O_RDWR | 读写方式打开 |
O_CREAT | 创建文件,如果文件存在,会被截断 |
O_TRUNC | 截断 |
O_APPEND | 追加 |
O_EXCL | 和O_CREAT一起用,如果文件存在则失败 |
open函数的flag值得是mode选项(注意互斥问题)。第三个参数指示新建的文件的属性。文件真实的权限,受umask的影响。影响方法
3.2 close
关闭文件。
在dup时,有更多讨论。
3.3 read/write
读写文件,会导致文件指针移动。
3.4 文件指针和lseek
文件指针是一个整数,描述当前读写位置,可以使用lseek移动文件指针。
4 文件读写效率
当读写文件时,缓冲区设置为1024到4096是一个比较合适的尺寸。
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
int main1()
{
int fdr = open("a.out", O_RDONLY);
if(fdr < 0)
{
perror("open read");
return -1;
}
int fdw = open("b.out", O_WRONLY|O_CREAT, 0777);
if(fdw < 0)
{
perror("open write");
return -1;
}
// 1. 如果文件很大怎么办
int filelen = lseek(fdr, 0, SEEK_END);//读取文件的长度
lseek(fdr, 0, SEEK_SET);//将文件读的指针。制回开始位置
char* buf = malloc(filelen);
read(fdr, buf, filelen);
write(fdw, buf, filelen);
close(fdr);
close(fdw);
}
int main2()
{
int fdr = open("a.out", O_RDONLY);
if(fdr < 0)
{
perror("open read");
return -1;
}
int fdw = open("b.out", O_WRONLY|O_CREAT, 0777);
if(fdw < 0)
{
perror("open write");
return -1;
}
// 一个一个字节拷贝,效率很低下
char buf;
while(1)
{
if(read(fdr, &buf, 1) <= 0)
{
break;
}
write(fdw, &buf, 1);
}
close(fdr);
close(fdw);
}
int main()
{
int fdr = open("a.out", O_RDONLY);
if(fdr < 0)
{
perror("open read");
return -1;
}
int fdw = open("b.out", O_WRONLY|O_CREAT, 0777);
if(fdw < 0)
{
perror("open write");
return -1;
}
// buf尺寸多少最合适?
char buf[1024];
int ret;
while(1)
{
ret = read(fdr, &buf, sizeof(buf));
if(ret <= 0)
{
break;
}
write(fdw, &buf, ret);//避免最后一次读取数据,没有1024个字节
}
close(fdr);
close(fdw);
}
5 文件共享
两个进程可以打开同一个文件进行操作,实现数据的共享。但是当两个进程打开同一个文件进行写操作时,会相互覆盖。当文件被打开两次时,两个文件描述符有各自的文件指针。
内核保存一个全局的文件描述结构体,而一个文件打开两次之后,两个结构体各自有各自的文件指针。
6 dup
dup函数可以复制文件描述符,让两个文件描述符指向同一个文件结构,通过dup复制文件描述符和两次打开文件描述符不同,所以两个文件描述符共享一个文件指针。
当一个文件描述符被关闭时,关闭的是内核的文件描述结构,但是如果文件描述结构体中,引用计数器不为1,那么close函数就只是减少了引用计数器而已。
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
// 0, 1, 2
// 0 标准输入
// 1 标准输出
// 2 标准错误
int main()
{
printf("this is output to terminate\n");
// 保存1号文件描述符
int fd_save1 = dup(1);
// 打开了一个新文件
int fd_file = open("new_output.txt", O_CREAT|O_RDWR, 0777);
// 将新文件拷贝到1的位置
dup2(fd_file, 1);
// 打印调试信息到文件
printf("this is output to file\n");
// 将保存的1号文件恢复到1号位置上
dup2(fd_save1, 1);
// 此时再输出,则又输出到终端
printf("output to terminate again\n");
return 0;
}
7 文件原子操作
原子操作是指一个操作一旦启动,则无法被能破坏它的其它操作打断。
-
写文件
无论是两次打开还是dup,同时操作一个文件都可能引起混乱,解决这个问题的方法是,可以通过O_APPEND解决这个问题。O_APPEND选项可以使得当一个写操作正在进行时,另外一个对该文件的写操作会阻塞等待。这意味着有O_APPEND选项的文件描述符,写操作无法被打断。
应用场景,多进程写Log文件
-
创建文件
除了写操作有原子性问题,创建文件也有,如果两个进程同时调用creat或者带O_CREAT的open,创建同一个文件时,可能会出现这种情况,第一个操作创建成功之后,写入数据,而第二个操作的O_CREAT把数据抹去了。
但是如果在O_CREAT之后,加上O_EXCL,那么可以避免这种情况。
8 fcntl和ioctl
fcntl可以用来设置文件描述符属性、文件状态属性、文件描述符复制、设置文件锁、设置文件通知等功能,这里只表示学习通过fcntl修改文件描述符属性。
如果一个文件描述符没有O_APPEND属性,但是后来又需要这个属性,那么可以通过fcntl来设置。
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
// uint32_t status; // 位操作
int main()
{
int fd = open("t11.txt", O_CREAT|O_RDWR, 0777);
if(fd < 0)
{
perror("open");
return -1;
}
// 通过fcntl修改文件属性,增加O_APPEND属性
int flags = fcntl(fd, F_GETFL);
flags |= O_APPEND;
fcntl(fd, F_SETFL, flags);
lseek(fd, 0, SEEK_SET);
write(fd, "hello", 5);
lseek(fd, 0, SEEK_SET);
write(fd, "world", 5);
close(fd);
return 0;
}
ioctl是一个杂项函数,一般用于文件底层属性设置。
9 文件映射
文件映射能将硬盘映射到进程的地址,这样可以像操作内存一样操作文件,而且效率很高,但是有一定限制:
-
文件长度必须大于等于映射长度
-
映射的offset必须是页的整数倍
页的尺寸获取方式:
命令行getconf -a | grep PAGE_SIZE
函数sysconf(_SC_PAGE_SIZE)
//运用mmap实现文件的映射
#include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <string.h> #include <stdio.h> #include <stdlib.h> #include <sys/mman.h> int main() { int fd = open("a.map", O_RDWR); if(fd < 0) { perror("open"); return -1; } void* ptr = mmap(NULL, /* 指定映射的地址,如果为空,那么内核自动选择一个地址 */ 4096, /* 映射长度 */ PROT_READ|PROT_WRITE, /* 访问方式,要和打开文件使的flag一致 */ MAP_SHARED, /* 修改映射地址的数据,反应到硬盘,如果是MAP_PRIVITED,那么修改数据,不会刷新到硬盘 */ fd, /* 文件描述符 */ 0 /*从什么地方开始映射*/); if(ptr == MAP_FAILED) { perror("mmap"); return -1; } // 像访问内存一样的访问硬盘,虚拟内存 // 通过这种方式访问大文件效率更高 // 进程之间共享数据 strcpy((char*)ptr, "hello"); munmap(ptr, 4096); close(fd); return 0; }
10 临时文件
可以通过mktemp(3)来获取一个临时文件路径,但是该文件不一定在/tmp目录下,在哪个目录下需要程序员指定。
Linux还有更多的创建临时文件的函数,学有余力的同学可以通过
man 3 mktemp
,查看相关函数。
#include <stdio.h>
#include <stdlib.h>
int main()
{
char buf[] = "./hello-XXXXXX";
char* p = mktemp(buf);
printf("p=%s\n", p);
printf("buf=%s\n", buf);
}
11 缓存
为了提高IO效率,系统为应用程序提供了缓存服务。当应用程序写数据到硬盘时,内核只是将数据写入内核缓存,然后返回成功。
缓存的存在隐藏风险,如果缓存数据未写入硬盘时,发生断电故障,那么会导致数据的不完整性。
关键数据的不完整,可能会导致系统崩溃。
使用O_SYNC选项打开文件时,那么写入操作将保证数据写入到硬盘再返回,当然这个选项导致IO效率降低。
也可以使用sync
,fsync
,fsyncdata
之类的函数,将数据写入硬盘。
fwrite和write都有缓存,不过fwrite在用户空间和内核空间都有缓存,而write只有在内核空间有缓存。
12 补充
12.1 标准输入/输出/错误
每一个进程都默认打开三个文件,三个文件描述符分别是0,1,2。printf其实是调用write(1)实现的。
一般不直接使用0,1,2来表示三个文件,而是用宏STDIN_FILENO,STDOUT_FILENO, STDERR_FILENO来表示输入、输出、错误。
12.2 open返回值
返回可用的最小的文件描述符。
小于0,表示对文件操作失败
12.3 文件描述符
进程范围内唯一。
12.4 dup2
也是用来赋值文件描述符,第二个参数指示复制的位置。
13 使用的命令和函数总结
13.1 函数
open/creat:打开文件/创建文件
read:读文件
write:写文件
close:关闭文件
lseek:定位文件读写位置
fcntl:修改文件属性
sysconf:读取系统配置
dup/dup2:复制文件描述符
sync/fsync/fsyncdata:同步文件数据
mmap/munmap:文件映射
mkstemp:得到临时文件路径
13.2 命令
touch:修改文件的访问时间,创建文件
cat:访问文件内容
vim:编辑
ulimit:显示一些限制信息(文件描述符最大值、栈的空间尺寸)
umask:文件创建的权限掩码
getconf:对应sysconf
第三章 文件和目录
1 前言
本章讨论文件属性和文件系统内容。除了上一章讨论的普通文件,Linux的文件概念还包括:目录、设备等。在Linux系统中,文件的种类包括:普通文件、目录、符号链接、块设备、字符设备、管道、套接字。
本章讨论的主要内容为普通文件、目录和符号链接。它们的公共特点是,真实的保存在了硬盘中,而其它类型的文件是内核产生的文件,在硬盘中并不存在。
2 文件属性
通过stat函数或者stat命令可以获得文件属性。
文件属性 | 解释 |
---|---|
dev_t st_dev | 设备号 |
ino_t st_ino | inode编号 |
mode_t st_mode | 访问权限相关 |
nlink_t st_nlink | 硬链接数量 |
uid_t st_uid | 拥有该文件的用户 |
gid_t st_gid | 拥有该文件的组 |
dev_t st_rdev | 设备号 |
off_t st_size | 文件尺寸 |
blksize_t st_blksize | 文件系统的IO尺寸 |
blkcnt_t st_blocks | 占用的block数量,一个block为512字节 |
time_t st_atime | 最后访问时间 |
time_t st_mtime | 最后修改时间 |
time_t st_ctime | 最后文件状态修改时间 |
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
struct stat buf;
int ret = stat(".", &buf);
if(ret < 0)
{
perror("stat");
return 0;
}
if(S_ISREG(buf.st_mode))
{
printf("this is regular file\n");
}
else if(S_ISDIR(buf.st_mode))
{
printf("this is dir\n");
}
// 测试拥有该文件的账户是否有读权限
if(S_IRUSR & buf.st_mode)
{
printf("user can read\n");
}
open("a.txt", O_CREAT|O_RDWR, 0777);
// access("./a.out", R_OK|W_OK|X_OK);
printf("file size is %d\n", (int)buf.st_size);
getchar();
return 0;
}
3 文件类型
在前言中,提到文件类型包括七种,在stat结构题中,保存了文件的文件类型属性,它的文件类型属性保存在st_mode中。但是七种类型,只需要3位即可,而st_mode是一个整数,因此它还保存其它内容,如果需要判断一个文件属于何种类型,需要一些宏的帮助。
文件类型属性是只读的属性,无法修改。
4 用户和组
Linux是一个多用户操作系统,因此每个文件都有属性,记录着这个文件属于哪个用户/组。
用户/组信息可以被修改,可以通过chown来修改文件所属的用户和组信息。
修改文件所属用户和组,需要root权限。
新文件所属用户和组,是创建该文件的进程所属用户和组。
-
实际账户和有效账户
账户 | 解释 |
---|---|
实际账户 | 登陆系统时的账户 |
有效账户 | 决定进程的访问资源的账户 |
5 文件访问权限
文件使用了9个位来表示访问权限,和文件类型一起,保存在st_mode中。此9位分成3组,每组3个位,分别表示读/写/执行权限,而三个组分别表示拥有该文件的账户,拥有该文件的组,其它用户组的权限。如果对应位是1,表示有权限,如果是0表示没有权限。
1 | 1 | 1 | 1 | 0 | 1 | 1 | 0 | 1 |
---|
文件访问权限经常用8进制来表示,比如上表的权限位可以表示为0755,意思是拥有它的账户对这个文件有读/写/执行权限,而拥有它的组有读/执行权限,其它账户对它有读/执行权限。
Linux提供一些宏,来测试文件的权限位
-
可以通过access函数来测试程序是否有访问某文件的权限。
-
创建文件时,可以指定文件的访问权限位,但是会收到umask位影响。
-
可以通过chmod来修改文件的权限位
6 其它权限位
6.1 SUID
只能对文件设置,如果文件设置了该位,那么该文件被运行时,对应的进程的权限,不是运行该程序账户的权限,而是拥有该用户的权限。
在对文件
未设置SUID的情况下:
如果对文件
设置了SUID,那么:
可以通过chmod u+s
或者chmod u-s
来设置获取去除SUID。
设置SUID可以让一个普通账户拥有它不该有的权限,容易产生安全漏洞。
6.2 SGID
可以对文件和目录设置,如果对文件设置,那么它的作用类似SUID,不过影响的是组。
如果对目录设置,那么拷贝到该目录下的文件都会被置位,除非拷贝是带
-p
参数。 在Ubuntu下测试并不如此。
在Ubuntu下设置了目录的SGID之后,在那个目录下创建的文件,拥有者是有效账户,而拥有组是该目录的拥有组。
命令:chmod g+s
chmod g-s
6.3 StickyBit
可以对文件或者目录设置,如果对文件设置,那么当这个文件运行并退出后,系统依旧保留该文件对应的映象,这样这个程序再次运行时,就不需要加载再加载了。这个属性的作用并不大,因为它占用了内存。
如果对目录设置,那么表示在该目录下,账户只能删除和修改它自己创建的文件,对于其它账户创建的文件,它不能修改和删除。这个位作用比较大,在一些公共目录,往往有这个属性,比如/tmp
命令:chmod o+t
chmod o-t
总结:
位 | 设置对象 | 设置方法 | 查看 | 效果 |
---|---|---|---|---|
SUID | 文件 | chmod u+s | 如果用户执行权限位为s或者S,则表示SUID有设置 | 当该文件被执行时,进程所拥有的权限是拥有该文件的账户权限 |
SUID | 目录 | 不可设置 | ||
SGID | 文件 | chmod g+s | 如果组执行权限位为s或者S,则表示GUID有设置 | 当执行该文件时,进程所属组是该拥有该文件的组 |
SGID | 目录 | chmod g+s | 同上 | 在该目录中创建文件时,该文件的所属组是目录的所属组 |
StickyBit | 文件 | chmod o+t | 如果其他执行权限位为t或者T,那么该文件有设置StickyBit | 执行该文件并退出后,系统保留该文件占用的一些内存,以便加快下一次的加载运行 |
StickyBit | 目录 | chmod o+t | 同上 | 账户只能修改和删除该目录下属于该账户的文件,不能修改该目录下其他账户创建的文件 |
7 文件长度
st_size保存文件的长度,write函数会修改该属性,也可以通过truncate修改文件大小,truncate可以扩大文件或者缩小文件,缩小文件时,文件内容会被删减。
文件大小可以通过ls
,wc -c
,stat
命令获取。
也可以通过fseek
和ftell
函数配合获取,或者直接通过stat
函数获取文件长度。
8 文件系统
8.1 文件管理
文件系统描述文件在硬盘中的组织,保存在硬盘中的文件只有普通文件、目录、软链接。
为了更加方便的管理持久性文件存储,操作系统一般对硬盘进行有规划的管理,规划包括:
-
分区
-
格式化
文件系统指一个分区内,文件存储的组织方式。
在Linux下,通过mount命令将分区挂载到虚拟文件系统。
8.2 inode
一个硬盘分区,被格式化之后,可以认为硬盘被划分成两部分:管理数据和数据。管理数据部分保存着这个分区的分区信息,以及inode表。
inode保存文件的属性信息,stat命令能看到的信息,大部分都是保存在inode里的,一个inode占用128或者256字节,这个依赖具体的文件系统,每当在硬盘上创建一个文件/目录时,系统为这个文件/目录分配一个inode。值得注意的是,文件名,不存在inode中,而是存在文件所在目录的文件内容部分。
8.3 数据块
数据部分被简单的、按照等大尺寸的划分成n块,一般每块数据块的尺寸为1024-4096,由具体文件系统决定。
8.4 文件
当创建一个文件时,系统为该文件分配一个inode。如果往该文件写数据,那么系统为该文件分配数据块,inode会记录这个数据块位置,当一个数据块不够用时,系统会继续为它分配数据块。
8.5 目录
当创建一个目录时,系统为该目录分配一个inode,同时分配一个数据块,并且在该数据块中,记录文件.
和..
对应的inode。
如果在该目录下创建文件newfile
,那么参考上一节内容,会为该文件创建inode,最后将newfile
文件名和它的inode,作为一条记录,保存在目录的数据块中。
如果一个inode被别人引用,那么它的引用计数器会加1。
8.6 路径和寻址
Linux系统采用以/划分的路径字符串来寻址文件。
比如命令mkdir testdir
,寻址和操作过程如下图:
思考:为什么mv命令很快,而cp命令很慢,rename如何实现的
补充:分区
查看磁盘信息
磁盘名字 sda sdb ..
分区名字 sda1 sda2 ...
分区
n 创建新分区
p 输出分区信息
w 保存分区信息并退出
分区和挂载
挂载成功之后,对xxyy目录的读写,其实是在/dev/sdb1文件系统中。
开机自动挂载
通过mount挂载的目录是临时的。如果希望开酒就挂载,那么可以将挂载命令写入到/etc/profile。或者修改/etc/fstab文件,/etc/fstab描述了开机需要挂载的文件系统信息。
去除挂载
通过手动umount去除挂载。
8.7 硬链接和软链接
硬链接不占用inode,只占用目录项。
软链接占用inode。
创建链接命令ln,硬链接只将对应的inode在目录总增加一个名字,并且将inode的引用计数器+1。
为了可以跨文件系统和对目录进行链接,创建了软链接这种方式。ln -s
思考:为什么硬链接不能跨文件系统,而且不能对目录进行硬链接
8.8 虚拟文件系统VFS
内存无法加载硬盘所有内容,因为一般内存比硬盘小,但是在Linux内核中,维护了一个虚拟文件系统,将硬盘的目录结构映射到内存中。这个映射一般只包含已经被打开的文件。
9 文件删除
使用unlink命令和函数可以删除一个文件。
如果此时文件已经打开,那么该文件也可以被unlink,但是删除操作不会立即执行,而会被保留到文件关闭时执行。
10 文件时间
对文件的访问,会导致文件时间发生变化。系统会自动记录用户对文件的操作的时间戳,以便将来可以查询文件修改时间。
如果需要故意修改,那么可以通过utime函数,修改文件的访问时间和修改时间。
touch
命令也可以将文件的时间修改为当前时间。touch
命令的副作用是,如果参数所指文件不存在,那么创建一个空文件。
当用户进行大规模拷贝时,cp
操作会修改文件的访问时间,如果想提高效率,可以使用-p
选项,避免文件属性的修改,同时加快速度。
#include <sys/types.h>
#include <utime.h>
int main()
{
struct utimbuf buf;
buf.actime = 0;
buf.modtime = 0;
utime("a.out", &buf);
}
利用utime来修改文件的访问时间和修改时间
11 目录操作
11.1 创建和删除目录
mkdir
和rmdir
11.2 遍历目录
opendir
,closedir
,readdir
,rewinddir
,telldir
,seekdir
遍历目录
seekdir和telldir
#include <dirent.h>
#include <sys/types.h>
#include <dirent.h>
#include <stdio.h>
int main(int argc, char* argv[])
{
DIR* dir = opendir(argv[1]);
struct dirent* entry;
while(1)
{
entry = readdir(dir);
if(entry == NULL)
break;
// linux下,.开头是隐藏文件
if(entry->d_name[0] == '.')
continue;
printf("%s\n", entry->d_name);
}
closedir(dir);
}
12 练习
12.1 实现文件拷贝,保留文件属性
12.2 实现目录打包到文件,将文件解包成目录
13 函数和命令
13.1 函数
stat/lstat:查看文件属性
chmod:修改文件权限
chown:修改文件的所有者
utime:修改文件时间
unlink:删除文件
link:创建硬链接
symlink:创建软链接
rmdir:删除空目录
mkdir:创建空目录
opendir:打开目录
closedir:关闭目录
readdir:读取一个目录项,并且将目录项指针移到下一项
seekdir:修改目录项指针
rewainddir:重置目录项指针
telldir:获得当前目录向指针
判断权限位宏 S_IRUSR(stat.st_mode)
判断文件类型宏S_ISDIR(stat.st_mode)
3.13.2 命令
stat:查看文件属性
chmod:修改文件权限
chown
unlink:删除文件(不会跟随)
ln:创建链接
mkdir
rmdir
rm
cp
dd:拷贝数据(可以拷贝文件,也拷贝块设备)
wc:计算文件内容的行数、单词数、字节数
which:查找非内置的命令位置
fdisk:查看磁盘信息、分区
mkfs:在分区中创建文件系统(ext2,ext3,ext4, fat32, ntfs, xfs,nfs)
mount:挂载
umount:取消挂载
第四章 进程
1 前言
本章讨论进程概念、资源、属性。
2 内核和进程的关系
当系统启动时,内核代码被加载到内存,初始化之后,启动第一个用户进程,然后内核的代码就等着用户进程来调度了。
3 进程是程序的实例
当程序员编写好一个程序,编译之后会生成这个可执行程序,这个程序可以被运行。
运行程序其实是用户进程(Shell进程)指示内核要启动另一个用户进程,内核便为这个新的进程分配资源,并加载该进程的代码和数据。
一个程序可以被运行多次。
3 进程资源
3.1 PCB
进程运行时,内核为进程每个进程分配一个PCB(进程控制块),描述进程的信息。
PCB在内核中对应的结构体是task_struct
。
3.2 虚拟地址空间
每个进程都会分配虚拟地址空间,在32位机器上,该地址空间为4G。
在进程里平时所说的指针变量,保存的就是虚拟地址。当应用程序使用虚拟地址访问内存时,处理器(CPU)会将其转化成物理地址。
这样做的好处在于:
-
进程隔离,更好的保护系统安全运行
-
屏蔽物理差异带来的麻烦,方便操作系统和编译器安排进程地址
思考:如果实现一个智能的myfree函数,该函数会自动判断指针是否在堆上还是在栈上,还是在全局变量中。
3.3 CPU
CPU的分配是动态的,不是进程一加载就直接分配的,一般来说每个系统都会有许多进程同时在运行,而CPU只有一个(多核CPU可以认为是多个,但是数量远少于进程数量)。那么,进程就需要排队等待,就好像有100个人,在4个卖饭的窗口买饭一样。
内核将进程PCB放入一个队列,总是让CPU服务队列中的第一个进程,服务时间可以是10毫秒,可以是25毫秒,具体多长时间跟具体系统有关系,这个时间有个名字叫做时间片。一旦这个进程服务时间到,这个进程会被丢到队列尾部,进行排队。进程调度。
内核中有一个常量HZ,一般是100,250, 1000
4 进程属性和状态
进程有许多的属性和状态,具体可以看task_struct,这里挑一些常见的进行讲解。
4.1 PID
进程编号,内核为每个进程分配一个进程编号,这个是进程的身份证,系统保证了不会重复分配。
通过函数getpid
或者命令ps
可以查看进程的PID。
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
4.2 PPID
PPID就是父进程ID,在Linux系统中,除了内核启动的第一个进程,其它进程都有父进程。
通过函数getppid
或者命令ps
可以查看进程的PPID。
4.3 账户ID/组ID
账户分实际账户和有效账户两种,如果你使用test账户登陆系统,但是使用sudo运行程序时,实际账户时test,有效账户时root。
通过函数getuid
和geteuid
获取真实账户id和有效账户id
通过函数getgid
和getegid
获得真实账户id和有效账户id
通过setuid
,setgid
,seteuid
,setegid
,setreuid
,setregid
等设置进程的有效和真实账户id。
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main()
{
uid_t uid = getuid();
uid_t euid = geteuid();
printf("uid =%d, euid=%d\n", (int)uid, (int)euid);
}
4.4 进程组ID/会话组ID/控制终端
进程组:getpgrp
和setpgid
会话组:getsid
和setsid
控制终端:
4.5 环境变量
保存该进程运行的环境信息。
进程的环境变量保存在全局变量environ
中,
也可以通过setenv
、getenv
、unsetenv
进行设置和获取。
4.6 进程状态
启动进程时,该进程在RUNNING状态,RUNNING状态的进程有可能时正在被执行,或者在队列中排队。但是如果进程调用阻塞函数,而运行条件不满足时,该进程会进入挂起状态。挂起状态的进程不再分配CPU,除非等到运行条件满足时。会阻塞进程运行的函数有许多,比如getchar是典型的阻塞调用。
阻塞函数列表可以在man 7 signal
中,找到关于阻塞函数的列表。
4.7 文件描述符
在进程控制块中,有一个数组保存着打开的文件描述符信息。
4.8 进程时间
进程有一些字段,用来记录进程的运行时间。
通过times
可以获取进程从运行开始时到执行times
函数时,所花费的时间。这个在系统性能优化时特别重要。
简单的程序可以从time
命令获取进程的运行时间。
Linux时间相关函数可以从man 7 time
获取。
4.10 当前工作目录和根目录
当前工作目录是相对地址的相对目录,通过getcwd
函数可以获取当前目录,也可以通过pwd
或者echo $PWD
获取。也可以通过chdir
来修改当前工作目录。
根目录是绝对地址的相对目录,可以通过chroot
来修改根目录。调用chroot
需要root权限。
目录相关资料在man 7 path_resolution
。
5 动态库和静态库
当使用动态库时,系统会检查该动态库是否已经加载,如果已经加载,则直接映射即可,如果没有加载,那么会加载之后再映射。
如果动态库中有全局变量,那么该全局变量对于不同的进程来说,是相互独立和隔离的。
链接静态库时,静态库被一起编译进可执行程序,运行时不再依赖静态库。
动态库编译:gcc -fpic -shared a.c b.c -o libtest.so
链接动态库gcc main.c -ltest -L. -o mybin
运行程序时
静态库打包:ar rcs libtest.a a.o b.o
链接库时,如果有同名的动态库和静态库,默认优先动态库,如果要链接静态库,那么使用-static
,比如
通过以下方式可以指定某些库使用静态链接,而某些库使用动态链接
6 内存管理
进程运行时,总是占用内存,无论是加载代码,还是在函数中定义局部变量,还是调用malloc申请内存。
无论是那种原因,进程需要使用内存时,它将向系统申请,并获得相对应的虚拟地址,而进程只能访问虚拟地址,真实的内存地址,进程无法访问。当进程访问虚拟地址时,系统会负责进行虚拟地址到物理地址的转换,系统发现进程尝试访问非法地址,那么进程将得到惩罚(段错误)。
这样做保护了系统的稳定性,不会因为个别新手程序员导致整个系统的崩溃。
另外还有一个好处是,使用虚拟内存之后,每个进程的导致空间是一致的,简化了进程的设计。
相关函数:malloc
,brk
,mmap
,alloca
7 进程总结
从用户的角度看,一个程序跑起来就是进程。而从操作系统的角度看,进程是一个控制块+代码+数据的组合。
8 函数和命令
8.1 函数
getpid:获取进程ID
getppid:获取父进程ID
getuid:获取实际用户ID
getgid:获取实际组ID
geteuid:获取有效账户ID
getegid:获取有效组ID
进程组描述了一项任务
getpgrp:获取进程组号
setpgid:设置进程组号
setsid:设置Session号
getsid:获得Session号
getcwd:获取当前工作目录
chdir:设置当前工作目录
chroot:修改当前根目录
getenv:环境中取字符串,获取环境变量的值
setenv:改变或增加环境变量
unsetenv
extern char** environ(全局变量)
malloc/free:堆区申请内存
mmap/munmap:在映射区申请内存
brk:全局区申请内存
alloca:在栈上申请内存
8.2 命令
ps axu:现行终端机下的所有程序,以用户为主的格式来显示程序状况,显示所有程序,不以终端机来区分
ps ajx
grep:搜索
kill:杀死进程(给进程发送信号)
第五章 进程控制
2 fork
fork函数实现进程复制,类似于动物界的单性繁殖,fork函数直接创建一个子进程。这是Linux创建进程最常用的方法。在这一小节中,子进程概念指fork产生的进程,父进程指主动调用fork的进程。
fork后,子进程继承了父进程很多属性,包括:
-
文件描述符:相当与dup,标准输入标准输出标准错误三个文件
-
账户/组ID:
-
进程组ID
-
会话ID
-
控制终端
-
set-user-ID和set-group-ID标记
-
当前工作目录
-
根目录
-
umask
-
信号掩码
-
文件描述符的close-on-exec标记
-
环境变量
-
共享内存
-
内存映射
-
资源限制
但是也有一些不同,包括:
-
fork返回值
-
进程ID
-
父进程
-
进程运行时间记录,在子进程中被清0
-
文件锁没有继承
-
闹钟
-
信号集合
#include <stdio.h> #include <unistd.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> int main() { printf("before fork\n"); // 在父进程中打开的文件描述符 // int fd = open("a.txt", O_RDWR|O_CREAT, 0777); // FILE* fp = fopen("a.txt", "r"); int fd = open("a.txt", O_RDWR); pid_t pid = fork(); // 创建一个新进程 if(pid == 0) { // 子进程可以使用父进程的描述符 // write(fd, "hello", 5); // char ch = fgetc(fp); char ch; read(fd, &ch, 1); printf("ch is %c\n", ch); printf("this is in child, ppid=%d\n", (int)getppid()); } else if(pid > 0) { // write(fd, "world", 5); char ch; read(fd, &ch, 1); printf("ch is %c\n", ch); // 当fork返回值大于0时,说明该进程是父进程 // 此时,返回值就是子进程的pid printf("this is in parent, pid=%d\n", (int)getpid()); } else { printf("error fork\n"); } printf("hello fork\n"); }
#include <stdio.h> #include <unistd.h> int global_var = 0;//fork()出来的子进程的值改变,不会影响父进程 因为开开辟了新的空间 int main() { int var = 0; int* p = (int*)malloc(sizeof(int)); *p = 0; pid_t pid = fork(); if(pid == 0) { global_var = 100; *p = 100; var = 100; printf("set var\n"); } else if(pid > 0) { sleep(1); // 确定的结果,就是0 printf("%d\n", global_var); printf("var is %d\n", var); // 0 printf("*p = %d\n", *p); } printf("hello world\n"); }
#include <stdio.h> #include <unistd.h> void forkn(int n) { int i; for(i=0; i<n; ++i) { pid_t pid = fork(); if(pid == 0) break; } } int main() { forkn(10); printf("hello world\n");
3 进程终止
进程有许多终止方法:
方法 | |
---|---|
main函数return | 正常退出 |
调用exit或者_Exit或者_exit | 正常退出 |
在多线程程序中,最后一个线程例程结束 | 正常退出 |
在多线程程序中,最后一个线程调用pthread_exit | 正常退出 |
调用abort | 异常退出 |
收到信号退出 | 异常退出 |
多线程程序中,最后一个线程响应pthread_cancel | 异常退出 |
当进程退出时,内核会为进程清除它申请的内存,这里的内存是指物理内存,比如栈空间、堆、代码段、数据段等,并且关闭所有文件描述符。
一般来说,进程退出时,需要告诉父亲进程退出的结果,如果是正常退出,那么这个结果保存在内核的PCB中。如果是异常退出,那么PCB中保存退出结果的字段,是一个不确定的值。因此程序员应该避免程序的异常退出。
进程退出时,除了它的PCB所占内存,其他资源都会清除。
4 wait和waitpid
一个进程终止后,其实这个进程的痕迹还没有完全被清除,因为还有一个PCB在内核中,如果不回收,那么会导致内存泄漏。父进程可以调用wait函数来回收子进程PCB,并得到子进程的结果。
wait
是一个阻塞调用,它的条件是一个子进程退出或者一个子进程有状态变化。wait
得到的status,包含了子进程的状态变化原因和退出码信息等等。
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
pid_t pid = fork();
if(pid == 0)
{
sleep(1);
printf("child process\n");
return 18;
}
else if(pid > 0)
{
printf("parent process\n");
// 等待子进程结束,并且回收子进程的PCB
int status;
wait(&status);
// 如何得到子进程的返回值
if(WIFEXITED(status))
{
printf("normal child process exit\n"); // 正常退出
int code =WEXITSTATUS(status);
printf("code is %d\n", code);
}
else if(WIFSIGNALED(status))
{
printf("signal\n");
}
else if(WIFSTOPPED(status))
{
printf("child stopped\n");
}
else if(WIFCONTINUED(status))
{
printf("child continue...\n");
}
printf("after wait\n");
}
return 0;
}
wait和waitpid可能会阻塞父进程,所以一般使用SIGCHLD信号来监控子进程
5 僵尸进程和孤儿进程
5.1 僵尸进程
是指已经退出的进程,但是父进程没有调用wait回收的子进程。僵尸进程没有任何作用,唯一的副作用就是内存泄漏。如果父进程退出,那么它的所有僵尸儿子会得到清理,因此僵尸进程一般指那些用不停歇的后台服务进程的僵尸儿子。
程序员应该避免僵尸进程的产生。
#include <stdio.h>
#include <unistd.h>
int main()
{
pid_t pid = fork();
if(pid == 0)
{
// 子进程什么事儿都不干,退出了,此时子进程是僵尸进程
}
else if(pid > 0)
{
getchar(); // 父进程不退出
}
return 0;
}
5.2 孤儿进程
父进程退出了,而子进程没有退出,那么子进程就成了没有父亲的孤儿进程。孤儿进程不会在系统中出现很长时间,因为系统一旦发现孤儿进程,就会将其父进程设置为init进程。那么将来该进程的回收,由init来负责。
6 exec
exec函数执行一个进程,当一个进程调用exec后,调用该函数的进程的虚拟地址空间的代码段、数据段、堆、栈被释放,替换成新进程的代码段、数据段、堆、栈,而PCB依旧使用之前进程的PCB。这个函数用中文来说就是鸠占鹊巢。
exec后使用的是同一个PCB,所以exec之后和之前,由很多进程属性是相同的,包括:
-
进程ID和父进程ID
-
账户相关
-
进程组相关
-
定时器
-
当前目录和根目录
-
umask
-
文件锁
-
信号mask
-
未决的信号
-
资源限制
-
进程优先级
-
进程时间
-
没有close-on-exec属性的文件描述符
使用fork和exec来执行一个新程序
#include <unistd.h>
#include <stdio.h>
// execle, e表示环境变量environ
//
int main(int argc, char* argv[])
{
char* args[] = {
"/bin/ls",
"-a",
"-l",
NULL
};
execv("/bin/ls", args);
}
int main2(int argc, char* argv[])
{
// p表示在PATH的环境变量中寻找这个程序
execlp("ls", "ls", NULL);
}
int main1(int argc, char* argv[])
{
// 执行一个程序
execl("/bin/ls", "/bin/ls", "-a", "-l", NULL);
// 该函数不会被执行
printf("hello world\n");
}
#include <stdio.h>
#include <fcntl.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
// fd is 3
int fd = open("exec.txt", O_RDWR|O_CREAT|O_CLOEXEC, 0777);
execl("./exec_test", "./exec_test", NULL);
}
函数后缀 | 解析 |
---|---|
l | list 用不定参数列表来表示命令参数,如果用不定参数列表,那么用NULL表示结束 |
v | vector 用数组来传递命令行参数 |
p | path 表示程序使用程序名即可,在$PATH中搜索该程序,不带p的需要提供全路径 |
e | environ 表示环境变量 |
补充:不定参数
不定参数函数定义:
int main()
{
int a = add(3, 12, 13, 14);
int b = add(2, 12, 13);
int c = add(4, 12, 13, 14, 15);
printf("%d, %d, %d\n", a, b, c);
char* p = concat("abc", "bcd", NULL);
printf("p is %s\n", p);
// 最后的NULL,被称之为哨兵
p = concat("aaaa", "bbbb", "cccc", NULL);
printf("p is %s\n", p);
}
#include <stdio.h>
#include <fcntl.h>
#include <sys/types.h>
// 如果没有__VA_ARGS__不带##,表示__VA_ARGS__至少要表示一个参数
// #define mylog(fmt, ...) printf("[%s:%d] "fmt, __FILE__, __LINE__, __VA_ARGS__)
// __VA_ARGS__如果有##,表示可以没有参数
#define mylog(fmt, ...) printf("[%s:%d] "fmt, __FILE__, __LINE__, ##__VA_ARGS__)
int main()
{
int fd = open("a.txt", O_RDWR);
if(fd < 0)
{
mylog("error open file\n");
}
}
#include <stdio.h>
// 转字符串 abc "abc"
#define STR(a) #a
// 拼接标识符
#define CC(a, b) a##b
int main()
{
int abcxyz = 100;
printf("%d\n", CC(abc, xyz));
}
8 账户和组控制
9 进程间关系
在Linux系统中,进程间除了有父子关系,还有组关系、Session关系、进程和终端进程关系。设计这些关系是为了更好的管理进程。
5.9.1 Session
一次登陆算一个session,exit命令可以退出session,session包括多个进程组,一旦session领导退出,那么一个session内所有进程退出(它的所有进程收到一个信号)。
#include <unistd.h>
int main()
{
pid_t pid = fork();
if(pid == 0)
{
// 独立一个session
setsid();
}
while(1)
{
sleep(1);
}
}
9.2 进程组
在终端执行进程,就会生成一个进程组。执行的进程fork之后,子进程和父进程在一个组中。
进程组长退出后,进程组的其他进程的组号依旧没有变化。
10 练习
10.1 fork任意个子进程。
10.2 使用多进程加速文件拷贝
使用-job
定义进程数量,加速文件拷贝。
10.3 实现自定义终端
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
// ls
// mkdir aaa
// cp ../aa bb
// cd
void handle_cmd(char* cmd)
{
char* args[1024];
char* p = strtok(cmd, " ");
int i = 0;
while(p)
{
args[i++] = p;
p = strtok(NULL, " ");
}
args[i] = NULL; // 表示参数结束位置
if(strcmp(args[0], "cd") == 0)
{
// 切换当前目录
chdir(args[1]);
return;
}
pid_t pid = fork();
if(pid == 0)
{
execvp(args[0], args);
// 如果命令执行失败,应该让子进程退出
printf("invalid command\n");
exit(0);
}
else
{
wait(NULL);
}
}
int main()
{
while(1)
{
printf("myshell> ");
// 等待用户输入
char buf[4096];
fgets(buf, sizeof(buf), stdin);
buf[strlen(buf)-1] = 0; // remove \n
if(strlen(buf) == 0)
{
continue;
}
handle_cmd(buf);
}
}
11 函数和命令
11.1 函数
fork:创建子进程
exec:执行新的程序
wait/waitpid:等待子进程结束,回收子进程PCB内存。
va_list:
va_start:定义指向不定参数的第一个参数的地址
va_arg:从参数列表中获取一个参数,并且让指针指向下一个参数
va_end:清除ap
第六章 信号
1 前言
本章简单描述信号。信号是Linux系统中,内核和进程通信的一种方式。如果内核希望打断进程的顺序执行,那么内核会在进程的PCB中记录信号。而当这个进程被分配到CPU,进入执行状态时,首先会检查是否有信号,如果有信号,那么进程会先尝试执行信号处理函数。
内核需要打断进程运行的时机:
-
进程无法继续了
-
按下ctrl+c
ctrl+c其实是bash向前台进程组发送SIGINT
运行该程序后,再按Ctrl+c,结果是四个进程全部退出
有了signal的处理之后,ctrl+c发送的SIGINT不会导致进程退出。
2 信号类型
通过kill -l
命令可以看到系统定义的信号类型,信号值小于32的是传统的信号,称之为非实时信号,而大于32的称之为实时信号。这里只讨论非实时信号。
3 信号的处理
可以通过signal函数,注册信号处理函数。如果没有注册信号处理函数,那么按照默认方式处理。
也可以通过signal设置忽略信号。
信号 | 默认处理动作 | 发出信号的原因 |
---|---|---|
SIGHUP | A | 进程session leader退出时,同session的其他进程会收到这个信号 |
SIGINT | A | Ctrl+C |
SIGQUIT | C | Ctrl+D |
SIGILL | C | 非法指令 |
SIGABRT | C | 调用abort函数产生的信号 |
SIGFPE | C | 浮点异常 |
SIGKILL | AEF | Kill信号 |
SIGSEGV | C | 无效的内存引用 |
SIGPIPE | A | 管道破裂: 写一个没有读端口的管道 |
SIGALRM | A | 由alarm(2)发出的信号 |
SIGTERM | A | 终止信号 |
SIGUSR1 | A | 用户自定义信号1 |
SIGUSR2 | A | 用户自定义信号2 |
SIGCHLD | B | 子进程状态变化会给父进程发送SIGCHLD信号 |
SIGCONT | 进程继续(曾被停止的进程) | |
SIGSTOP | DEF | 暂停进程 |
SIGTSTP | D | 控制终端(tty)上按下停止键 |
SIGTTIN | D | 后台进程企图从控制终端读 |
SIGTTOU | D | 后台进程企图从控制终端写 |
A 缺省的动作是终止进程
B 缺省的动作是忽略此信号
C 缺省的动作是终止进程并进行内核映像转储(dump core)
D 缺省的动作是停止进程
E 信号不能被捕获
F 信号不能被忽略
#include <stdio.h>
#include <signal.h>
#include <stdlib.h>
void signal_handle(int a)
{
if(a == SIGINT)
printf("signal_handle\n");
else if(a == SIGABRT)
printf("abrt\n");
else if(a == SIGALRM)
printf("alarm\n");
else if(a == SIGCHLD)
printf("child\n");
else if(a == SIGUSR1)
printf("usr1 signal\n");
}
int main()
{
// SIGINT 2
signal(SIGINT, signal_handle);
signal(SIGABRT, signal_handle);
signal(SIGALRM, signal_handle);
signal(SIGCHLD, signal_handle);
signal(SIGUSR1, signal_handle);
pid_t pid = fork();
if(pid == 0)
return 0;
// 给自己发送一个abrt信号
//abort();
alarm(1);
while(1)
{
sleep(1);
}
}
4 不可靠信号
信号值小于32的都是不可靠信号,假如进程收到一个信号来不及处理,这时候又收到一个同样的信号,那么这两个信号会合并成一个信号,这个原因是因为进程保存该信号的值只有1位。
5 中断系统调用(中断阻塞)
假如一个进程调用了某系统调用导致该进行处于挂起状态,而此时该进程接收到一个信号,那么该系统调用被唤醒。通常该系统调用会返回-1,错误码是EINTR
。
也有些系统调用,可以设置打断后重启,这样就不会被信号打断,具体参考man 7 signal
如果使用signal函数注册信号处理函数,默认被中断的系统调用是自动重启的。
#include <signal.h>
#include <stdio.h>
#include <errno.h>
void handle(int v){
printf("ctrl+c\n");
}
int main()
{
signal(SIGINT, handle);
char buf;
int ret = read(0, buf, sizeof(buf)); // read被中断打断了
printf("ret = %d, errno=%d, EINTR=%d\n", ret, errno, EINTR); // EINTR
}
6 可重入问题
信号会导致可重入问题,比如一个全局链表。
以上代码在一定情况下会崩溃,在main函数中不停调用push_back,如果在push_back执行一半时,被中断打断,然后去执行中断处理函数时,那么该中断处理函数的push_back会崩溃。
有些系统调用本身带有局部静态变量,因此那些函数不能在信号处理函数中调用,比如strtok
,readdir
等,对应的可重入的函数是strtok_r
,readdir_r
。
7 发送信号
可以通过kill函数发送信号。
kill也可以进程组发送信号
#include <sys/types.h> #include <signal.h> int main() { kill(27054, SIGUSR1); }
补充:掩盖信号
//掩盖不可靠信号
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
// 掩盖SIGINT
// 掩盖一个可靠信号
void handle(int v)
{
printf("sigint \n");
}
int main()
{
signal(SIGINT, handle);
sigset_t set;
// 将set集合设置为空
sigemptyset(&set);
// 将SIGINT信号加入集合
sigaddset(&set, SIGINT);
// 把这个集合代表的信号,加入信号掩码
sigprocmask(SIG_BLOCK, &set, NULL);
// 从此,该进程收到SIGINT,不会被处理
sleep(5);
printf("remove SIGINT from mask\n");
// 去掉SIGINT的掩码
sigprocmask(SIG_UNBLOCK, &set, NULL);
while(1)
{
sleep(1);
}
}
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
// 掩盖34
// 掩盖一个可靠信号
void handle(int v)
{
printf("hahahaha \n");
}
int main()
{
signal(34, handle);
sigset_t set;
// 将set集合设置为空
sigemptyset(&set);
// 将34信号加入集合
sigaddset(&set, 34);
// 把这个集合代表的信号,加入信号掩码
sigprocmask(SIG_BLOCK, &set, NULL);
// 从此,该进程收到34,不会被处理
kill(getpid(), 34);
kill(getpid(), 34);
kill(getpid(), 34);
kill(getpid(), 34);
kill(getpid(), 34);
sleep(5);
printf("remove 34 from mask\n");
// 去掉34的掩码
sigprocmask(SIG_UNBLOCK, &set, NULL);
while(1)
{
sleep(1);
}
}
8 忽略信号
#include <signal.h>
void handle(int v)
{
}
int main()
{
// 表示忽略SIGINT
// 忽略信号和掩盖信号是有区别:
//
// 未决的信号:已经发出但是没有被处理的信号叫未决的信号
signal(SIGINT, SIG_IGN);
// 从这里开始就不忽略
signal(SIGINT, handle); // 设置一个处理函数
signal(SIGINT, SIG_DFL); // 恢复成默认处理
while(1)
{
sleep(1);
}
}
以上例子,忽略SIGPIPE信号,那么进程收到SIGPIPE后,不会有任何反应。
9 屏蔽信号
屏蔽和忽略不同,忽略意味着在忽略期间,接收的信号就被忽略了。而屏蔽的意思,是暂时屏蔽,屏蔽期间收到的信号依旧在,如果某一时刻该信号不再忽略时,该信号的处理程序会被调用。
设置屏蔽集合,使用sigprocmask
10 SIGCHLD
SIGCHLD信号产生于子进程退出和状态变化,父进程通常在该信号的处理函数中,调用wait来回收子进程的PCB,这样可以避免阻塞。
#include <signal.h>
void chld_handle(int v)
{
// 有子进程退出了
// wait(NULL);
//
while(1) // 使用while(1)是避免有多个子进程同时退出,由于SIGCHLD是不可靠信号,函数只会调用一次
{
int ret = waitpid(-1, NULL, WNOHANG); // 每次回收都应该用非阻塞方式去回收
if(ret == -1) // 没有僵尸进程了,之前僵尸进程已经被回收了
break;
}
}
int main()
{
// 等待子进程的结束,问题是:ddddd
// 它阻塞主进程的执行,影响效率
// wait(NULL);
//
signal(SIGCHLD, chld_handle);
pid_t pid = fork();
if(pid == 0) return 0;
while(1)
{
sleep(1);
}
}
11 sigaction和sigqueue
sigaction和signal一样用来注册信号处理函数,siqqueue和kill一样,用来发送信号,但是sigaction比signal功能强大,signal比较简单。
强大:
-
可以传递参数
-
可以获得发送信号的进程信息
-
可以设置SA_RESTART
#include <signal.h> #include <stdio.h> #include <errno.h> //void handle(int v){} // // 新的信号处理函数 void handle(int v, siginfo_t* info, void* p) { printf("ctrl+c\n"); } int main() { // 默认的signal已经有SA_RESTART这个flag了 //signal(SIGINT, handle); struct sigaction sig; sig.sa_handler = NULL; sig.sa_sigaction = handle; sigemptyset(&sig.sa_mask); // sig.sa_flags = 0; sig.sa_flags = SA_RESTART; // 让阻塞的系统调用,被这个信号打断之后,要重启 sig.sa_restorer = NULL; // 在Linux下没用,直接填NULL就可以了 sigaction(SIGINT, &sig, NULL); char buf; int ret = read(0, buf, sizeof(buf)); // read被中断打断了 printf("ret = %d, errno=%d, EINTR=%d\n", ret, errno, EINTR); // EINTR }
#include <stdio.h> #include <stdlib.h> #include <signal.h> char buf[1024]; void handle_data() { printf("user input is %s\n", buf); } void handle_chld1(int v) { while(1) { int ret = waitpid(-1, NULL, WNOHANG); if(ret < 0) break; } } void handle_chld(int v, siginfo_t* info, void* p) { handle_chld1(v); } int main() { // signal(SIGCHLD, handle_chld); struct sigaction act; act.sa_handler = NULL; act.sa_sigaction = handle_chld; sigemptyset(&act.sa_mask); act.sa_flags = 0; act.sa_restorer = NULL; sigaction(SIGCHLD, &act, NULL); while(1) { // char* ret = fgets(buf, sizeof(buf), stdin); // if(ret == NULL) break; int ret = read(0, buf, sizeof(buf)); if(ret < 0) { // 说明read出错,不是真正的出错,而是被中断打扰了 // 那此时,应该重新调用read函数,去获取信息 if(errno == EINTR) { continue; } // 如果是其他错误原因,就break break; } pid_t pid = fork(); if(pid == 0) { handle_data(); // 创建一个子进程去处理数据 exit(0); } } }
#include <signal.h> #include <stdio.h> void handle(int v, siginfo_t* info, void* p) { printf("recv SIGINT, arg=%d\n", info->si_value.sival_int); } int main() { struct sigaction act; act.sa_handler = NULL; act.sa_sigaction = handle; sigemptyset(&act.sa_mask); act.sa_flags = SA_SIGINFO|SA_RESTART; act.sa_restorer = NULL; // 注册信号处理函数 sigaction(SIGINT, &act, NULL); union sigval v; v.sival_int = 99; // 相当于kill,但是它可以传递一个参数 sigqueue(getpid(), SIGINT, v); getchar(); }
#include <signal.h> #include <stdio.h> int main() { char* p = malloc(100); union sigval v; // v.sival_int = 98; v.sival_ptr = p; // 相当于kill,但是它可以传递一个参数 sigqueue(28360, SIGINT, v); getchar(); }
12 函数命令
signal:注册信号处理函数
kill:发送信号
sigprocmask:设置信号掩码
sigemptyset:清空信号集
sigfillset:设满信号集
sigaddset:往信号集增加一个信号
sigdelset:从信号集删除一个信号
sigismember:判断信号否则在信号集
sigaction:注册更加强大的处理函数
sigqueue:发送信号
abort
alarm
pause
第七章 线程
1、前言
之前讨论了进程,了解一个进程能做一件事情,如果想同时处理多件事情,那么需要多个进程,但是进程间很不方便的一点是,进程间的数据交换似乎没有那么方便。Linux提供线程功能,能在一个进程中,处理多任务,而且线程之间的数据是完全共享的。
线程也有PCB,它的PCB和进程的PCB结构完全一样,只是它里面保存的虚拟地址空间和创建它的进程的虚拟地址空间完全保持一致。
2、线程的创建
通过pthread_create
函数可以创建一个线程,被创建的线程的例程,就是一个新的执行指令序列了。
#include <pthread.h> void* thread_func(void* p ) { return NULL; } int main() { pthread_t tid; pthread_create(&tid, NULL, thread_func, NULL); printf("tid=%d\n", (int)tid); pthread_create(&tid, NULL, thread_func, NULL); printf("tid=%d\n", (int)tid); pthread_create(&tid, NULL, thread_func, NULL); printf("tid=%d\n", (int)tid); pthread_create(&tid, NULL, thread_func, NULL); printf("tid=%d\n", (int)tid); getchar(); }
补充
intptr_t是一种整型,它的长度依赖机器位长,也就意味着它的长度和指针的长度一样的。
3、线程标识
线程使用pthread_t来标识线程,它也是一个非负整数,由系统分配,保证在进程范围内唯一。pthread_t虽然在Linux下是非负整数,但是在其它平台下不一定是,所以比较线程号是否想等,应该用pthread_equal
。
任何一个函数都可以调用pthread_self
来获取目前代码运行的线程。
4、线程终止
终止方式 | |
---|---|
例程返回 | 正常退出 |
调用pthread_exit | 正常退出 |
响应pthread_cancel | 异常退出 |
注意:
-
在线程里调用
exit
是退出整个进程。 -
在多线程的进程中,主线程调用
pthread_exit
,进程并不会退出,它的其他线程依旧在执行,但是主线程已经退出了。 -
意味着:主线程和其他线程是几乎是平等的。
-
不平等的是,如果主线程的main函数return了,那么其他线程也结束了,如果其他线程的入口函数return了,主线程不会跟着结束。
5、线程的回收
线程退出之后,它的PCB依旧在内核中存在,等着其它线程来获取它的运行结果,可以通过pthread_join
来回收线程。从这个角度看,线程和进程差不多,但是跟进程不同的时,线程没有父线程的概念,同一个进程内的其它线程都可以来回收它的运行结果。
pthread_join
会阻塞调用它的线程,一直到被join的线程结束为止。
pthread_join
和wait/waitpid
一样,也是阻塞的调用,它除了有回收PCB的功能,也有等待线程结束的功能。
6、线程的使用场景
客户端使用场景
一般来说,线程用于比较复杂的多任务场景,比如:
这样主线程可以基础处理主线程的事情,不至于被复杂的任务阻塞。比如:
这样聊天界面不会卡死在那里,否则如果网络情况很差,有可能导致界面卡死。
服务器使用场景
服务器一般的流程如下:
在服务器上,一个线程来处理整个流程,会导致处理流程非常慢,导致主线程无法及时接收报文。一般会使用子线程来做具体的工作,而主线程只负责接收报文。
有时为了提高处理效率,会使用线程池
7 线程的同步
无论上述那种场景,都有一个报文队列或者消息队列,一般这个队列是一个链表,主线程需要往链表中添加数据,而子线程从链表获取数据。两个线程同时操作一个全局变量是不安全的,应该避免不安全的访问。无论这种全局变量是数组、链表、还是一个简单的变量。
线程A:i = i + 1;
线程B:i = i + 1;
7.1 不安全的案例
-
多线程操作一个全局变量
1 #include <stdio.h> 2 #include <signal.h> 3 #include <pthread.h> 4 5 int result=0; 6 7 void add() 8 { 9 int i; 10 for(i=0; i<100000; ++i) 11 { 12 result++; 13 } 14 } 15 16 void* thread_func(void* p) 17 { 18 add(); 19 return NULL; 20 } 21 22 int main() 23 { 24 pthread_t t1; 25 pthread_t t2; 26 27 pthread_create(&t1, NULL, thread_func, NULL); 28 pthread_create(&t2, NULL, thread_func, NULL); 29 30 pthread_join(t1, NULL); 31 pthread_join(t2, NULL); 32 33 printf("%d\n", result); 34 return 0; 35 } 36 不安全的生产者消费者模型 37 38 #include <list> 39 40 struct task_t 41 { 42 int task; 43 }; 44 45 list<task_t*> queue; 46 47 void* work_thread(void* arg) 48 { 49 while(1) 50 { 51 if(queue.size() == 0) continue; 52 53 task_t* task = *queue.begin(); 54 queue.pop_front(); 55 56 printf("task value is %d\n", task->task); 57 delete task; 58 } 59 } 60 61 void main(int argc, char* argv[]) 62 { 63 pthread_t tid; 64 pthread_create(&tid, NULL, work_thread, NULL); 65 66 while(1) 67 { 68 int i; 69 cin >> i; 70 task_t* task = new task_t; 71 task->task = i; 72 73 queue.push_back(task); 74 } 75 76 pthread_join(tid, NULL); 77 }
7.2 锁(临界量)
锁能避免两个线程同时访问一个全局变量。
锁会带来两个问题:
-
效率低
-
死锁
#include <stdio.h> #include <pthread.h> int result = 0; // 定义锁,锁一般也定义在全局 //pthread_mutex_t mutex; // 粗粒度的锁 pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; int result1 = 0; pthread_mutex_t mutex1; // 1.一个线程重复加锁两次,会死锁 void func() { pthread_mutex_lock(&mutex); pthread_mutex_unlock(&mutex); } void foo() { pthread_mutex_lock(&mutex); func(); pthread_mutex_unlock(&mutex); } // 2. 一个线程加锁之后,忘记了解锁 void foo1() { pthread_mutex_lock(&mutex); if(...) // 这种场合容易产生忘记解锁 return; // .... // 忘记了解锁 pthread_mutex_unlock(&mutex); } void foo2() { // 因为别的线程忘记解锁,所以本线程无法进行加锁 pthread_mutex_lock(&mutex); // 阻塞在这里 pthread_mutex_unlock(&mutex); } void* thread_func(void* ptr) { foo(); int i=0; for(i=0; i<100000; ++i) { pthread_mutex_lock(&mutex1); result1++;//它的值由什么决定 pthread_mutex_unlock(&mutex1); // 两个线程同时操作全局变量,结果不可靠 // // 将该操作变成原子操作,或者至少不应该被能影响它操作的人打断 pthread_mutex_lock(&mutex); result ++; // result++代码被锁保护了,不会被其他线程的result++影响 pthread_mutex_unlock(&mutex); } return NULL; } int main() { // 使用锁之前,要对它进行初始化 // pthread_mutex_init(&mutex, NULL); pthread_mutex_init(&mutex1, NULL); pthread_t t1, t2; pthread_create(&t1, NULL, thread_func, NULL); pthread_create(&t2, NULL, thread_func, NULL); pthread_join(t1, NULL); pthread_join(t2, NULL); printf("result is %d\n", result); }
#include <stdio.h> #include <list> #include <iostream> using namespace std; struct task_t { int task; }; // 全局的任务队列 list<task_t*> tasks; pthread_mutex_t mutex; pthread_cond_t cond; // pthred_cond_signal和pthread_cond_wait类似不可靠信号,signal不会累计 // 当一个线程发送signal时,如果另外一个线程此时没有调用wait函数,那么这个signal就会消失掉 void* work_thread(void* ptr) { while(1) { // 等待条件 pthread_mutex_lock(&mutex); pthread_cond_wait(&cond, &mutex); pthread_mutex_unlock(&mutex); // 一旦条件满足,就应该处理队列中所有的任务 while(1) { pthread_mutex_lock(&mutex); if(tasks.size() == 0) { pthread_mutex_unlock(&mutex); // 特别容易忘记解锁 break; } task_t* task = *tasks.begin(); tasks.pop_front(); pthread_mutex_unlock(&mutex); // 处理任务 printf("current task is %d\n", task->task); // new和delete(malloc和free)都是线程安全的 delete task; } } } int main() { pthread_mutex_init(&mutex, NULL); pthread_cond_init(&cond, NULL); pthread_t tid; pthread_create(&tid, NULL, work_thread, NULL); while(1) { int i; // 阻塞的,等待任务 cin >> i; // 构造任务结构体 task_t* task = new task_t; task->task = i; // 把任务丢到任务列表中 pthread_mutex_lock(&mutex); tasks.push_back(task); pthread_mutex_unlock(&mutex); // 唤醒条件变量 pthread_cond_signal(&cond); } }
//运用析构函数 #ifndef __AUTO_LOCK_H__ #define __AUTO_LOCK_H__ #include <pthread.h> class auto_lock { public: auto_lock(pthread_mutex_t& m); ~auto_lock(); private: pthread_mutex_t& mutex; }; #endif #include "auto_lock.h" auto_lock::auto_lock(pthread_mutex_t& m): mutex(m) { pthread_mutex_lock(&mutex); } auto_lock::~auto_lock() { pthread_mutex_unlock(&mutex); } #include <stdio.h> #include "auto_lock.h" pthread_mutex_t mutex; int result = 0; void* thread_func(void*ptr) { for(int i=0 ;i<100000; ++i) { auto_lock var1(mutex); // 重复加锁 auto_lock var(mutex); // 在构造里自动加锁 result++; } } int main() { // 变成递归锁 及循环锁 pthread_mutexattr_t attr;//设计循环锁属性 pthread_mutexattr_init(&attr); pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE); // 用递归属性去初始化这个锁 pthread_mutex_init(&mutex, &attr); pthread_t tid1, tid2; pthread_create(&tid1, NULL, thread_func, NULL); pthread_create(&tid2, NULL, thread_func, NULL); pthread_join(tid1, NULL); pthread_join(tid2, NULL); printf("result is %d\n", result); }
相对的解决方法:
-
读写锁
-
#include <pthread.h> pthread_rwlock_t mutex; int result; void* thread_func(void* ptr) { pthread_rwlock_rdlock(&mutex); // 只能对数据读 result ++; // 写数据的行为是会导致数据不正确 pthread_rwlock_unlock(&mutex); pthread_rwlock_wrlock(&mutex); // 可以对数据读写 pthread_rwlock_unlock(&mutex); } int main() { pthread_rwlock_init(&mutex, NULL); pthread_t tid; pthread_create(&tid, NULL, thread_func, NULL); }
-
循环锁
7.2.1 基本锁
类型:pthread_mutex_t
定义的变量一般在全局:pthread_mutex_t g_mutex;
在使用之前要初始化:pthread_mutex_init(&g_mutex, NULL);
访问敏感对象前加锁:pthread_mutex_lock(&g_mutex);
访问结束要解锁:pthread_mutex_unlock(&g_mutex);
一把所可以负责多个全局变量的安全问题,但是负责的范围越大,效率越低,代码相对容易写。负责全局变量的数量,被称之为锁的粒度。
死锁问题
-
忘了解锁会产生死锁
-
重复加锁会导致死锁
怎么解决死锁问题:
-
忘了解锁:程序员自己要注意
-
重复加锁:使用循环锁可以解决问题
7.2.2 循环锁
解决重复加锁导致死锁问题,循环锁的特点是,同一个线程进行多次加锁,不会阻塞。
pthread_mutex_lock(&mutex);
pthread_mutex_lock(&mutex); // 第二次加锁不会阻塞,但是它会给mutex增加一个计数。
pthread_mutex_unlock(&mutex) // 减少计数
pthread_mutex_unlock(&mutex);//减少到0的时候,真正解锁
怎么设置循环锁。
//头文件
#ifndef __AUTO_LOCK_H__
#define __AUTO_LOCK_H__
#include <pthread.h>
class auto_lock
{
public:
auto_lock(pthread_mutex_t& m);
~auto_lock();
private:
pthread_mutex_t& mutex;
};
#endif
//头文件的实现
#include "auto_lock.h"
auto_lock::auto_lock(pthread_mutex_t& m): mutex(m)
{
pthread_mutex_lock(&mutex);
}
auto_lock::~auto_lock()
{
pthread_mutex_unlock(&mutex);
}
//主函数
#include <stdio.h> #include "auto_lock.h" pthread_mutex_t mutex; int result = 0; void* thread_func(void*ptr) { for(int i=0 ;i<100000; ++i) { auto_lock var1(mutex); // 重复加锁 auto_lock var(mutex); // 在构造里自动加锁 result++; } } int main() { // 变成递归锁 pthread_mutexattr_t attr; pthread_mutexattr_init(&attr); pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE); // 用递归属性去初始化这个锁 pthread_mutex_init(&mutex, &attr); pthread_t tid1, tid2; pthread_create(&tid1, NULL, thread_func, NULL); pthread_create(&tid2, NULL, thread_func, NULL); pthread_join(tid1, NULL); pthread_join(tid2, NULL); printf("result is %d\n", result); }
7.2.3 读共享写排他锁(读写锁)
共享锁/排他锁
定义锁:pthread_rwlock_t mutex;
初始化:pthread_rwlock_init(&mutex, NULL);
读锁定:pthread_rwlock_rdlock(&mutex);
写锁定:pthread_rwlock_wrlock(&mutex);
解锁:pthread_rwlock_unlock(&mutex);
7.2.4 总结
-
无论是什么锁,都会导致性能下降,所以能不用就尽量不用
-
锁能不能用于进程间同步?可以
C++使用构造函数和析构函数自动加锁解锁
7.3 条件变量
条件变量是另外一种同步机制,它可以使线程在无竞争的等待条件发生。在之前讲到的线程场景里,子线程往往要等到队列有数据
才运行,否则它应该休眠,以避免浪费CPU。但是如果用锁来实现这种机制的话,会非常麻烦。
定义:pthread_cond_t g_cond;
初始化:pthread_cond_init(&g_cond);
等待:pthread_cond_wait(&g_cond, &g_mutex);
唤醒:pthread_cond_signal(&g_cond);
pthread_cond_broadcast(&g_cond);
惊群
#include <stdio.h> #include <pthread.h> pthread_mutex_t mutex; pthread_cond_t cond; void* thread_func(void* ptr) { sleep(1); pthread_mutex_lock(&mutex); pthread_cond_wait(&cond, &mutex); pthread_mutex_unlock(&mutex); printf("wait ok\n"); } int main() { pthread_mutex_init(&mutex, NULL); pthread_cond_init(&cond, NULL); pthread_t tid; pthread_create(&tid, NULL, thread_func, NULL); // 发信号时,线程不是正在调用pthread_cond_wait,而是在执行sleep(1),所以signal发送之后,就消失了,不会保留 // 按照刚才的说法,这个signal根本无效 // 所以当一个线程发送多次的signal时,那么最多只有一次是有作用的 pthread_cond_signal(&cond); pthread_join(tid, NULL); }
7.3.1 条件变量的等待和唤醒
如果没有线程在等待条件,此时唤醒函数pthread_cond_signal不会唤醒任何的线程,也不会记录。
如果有多个线程在执行pthread_cond_wait,而此时有一个线程调用pthread_cond_signal,那么只会唤醒其中一个线程。
如果想唤醒所有线程,那么调用pthread_cond_broadcast,该函数可以唤醒等待该条件的所有线程。
#include <stdio.h> #include <pthread.h> // 假如有三个线程同时调用pthread_cond_wait,一个线程调用pthread_cond_signal // pthread_mutex_t mutex; pthread_cond_t cond; void* thread_func(void* ptr) { pthread_mutex_lock(&mutex); pthread_cond_wait(&cond, &mutex); pthread_mutex_unlock(&mutex); printf("wait ok\n"); } int main() { pthread_mutex_init(&mutex, NULL); pthread_cond_init(&cond, NULL); pthread_t tid1, tid2, tid3; pthread_create(&tid1, NULL, thread_func, NULL); pthread_create(&tid2, NULL, thread_func, NULL); pthread_create(&tid3, NULL, thread_func, NULL); sleep(1); // 唤醒一个线程 // pthread_cond_signal(&cond); // 唤醒所有正在等待的线程 pthread_cond_broadcast(&cond); pthread_join(tid1, NULL); pthread_join(tid2, NULL); pthread_join(tid3, NULL); }
7.4 信号量
信号量类似条件变量,但是信号量可以保存信号数量。
-
定义: sem_t sem;
-
初始化:sem_init(&sem, 0, 0);
初始化的第二个参数,如果是0表示同一进程内的多线程之间的信号量,如果是非0,那么该信号量可以使用在进程之间。第三个参数表示信号量的初始值。 -
等待:sem_wait(&sem);
sem_wait函数会导致该线程休眠,唤醒的条件是sem的值大于0。并且sem_wait调用结束后,会自动将sem值减1。 -
唤醒:sem_post(&sem);
sem_post只是简单的将sem值+1#include <stdio.h> #include <semaphore.h> #include <pthread.h> sem_t sem; void* thread_func(void* ptr) { sleep(1); sem_wait(&sem); printf("wait ok\n"); } int main() { sem_init(&sem, 0, 0); pthread_t tid1, tid2, tid3; pthread_create(&tid1, NULL, thread_func, NULL); pthread_create(&tid2, NULL, thread_func, NULL); pthread_create(&tid3, NULL, thread_func, NULL); // 发送信号 sem_post(&sem); pthread_join(tid1, NULL); pthread_join(tid2, NULL); pthread_join(tid3, NULL); }
8 重入
如果函数操作了全局变量,这个函数就不是可重入的函数了。
#include <stdio.h> #include <pthread.h> #include <string.h> int result = 0; void foo() { // 因为这个函数操作了全局变量 result ++; } void* thread_func(void* ptr) { #if 0 int i; for(i=0; i<10000; ++i) { // 该函数是不可重入的函数 // 用锁来保护它 foo(); } #endif char p[] = "1 2 3 4 5 6 7 8 9 0"; char* saveptr; char* sub = strtok_r(p, " ", &saveptr); while(sub) { usleep(1000); // 1毫秒 printf("%s, tid=%d\n", sub, (int)pthread_self()); sub = strtok_r(NULL, " ", &saveptr); } } int main() { pthread_t tid1, tid2; pthread_create(&tid1, NULL, thread_func, NULL); pthread_create(&tid2, NULL, thread_func, NULL); pthread_join(tid1, NULL); pthread_join(tid2, NULL); printf("result=%d\n", result); }
9 分离的线程
分离的线程不用pthread_join
,也无法通过pthread_join
来获取结果。因为它运行结束之后,它的PCB同时被释放了。
#include <errno.h> #include <stdio.h> #include <pthread.h> #include <inttypes.h> // intptr_t 整数类型:char short int long (long long) // 整数:8 16 32 64 // 有些机器的int是32位,有的机器是64位 // void*指针类型都是按照机器的字长决定 // // intptr_t是一个整数,并且它总是和指针的字节数是一样的 void* thread_func(void* ptr) { // 用的是地址本身,而不是地址指向的值 printf("%d\n", (int)(intptr_t)ptr); sleep(1); } int foo() { char p[] = "hello world"; int a = 100; pthread_attr_t attr; pthread_attr_init(&attr); pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); pthread_t tid; pthread_create(&tid, &attr, thread_func, (void*)(intptr_t)a); // 该线程自生自灭 // pthread_detach(tid); int ret = pthread_join(tid, NULL); printf("join error, ret=%d, errno=%d, EINVAL=%d\n", ret, errno, EINVAL); } int main() { foo(); sleep(2); }
10 线程私有数据
线程可以定义私有数据,私有数据只供该线程使用。
线程私有数据可以在该线程调用函数中访问,其他线程调用的函数中,不可访问。
#include <string.h> #include <pthread.h> #include <stdio.h> #include <stdlib.h> pthread_key_t key; // 可能被线程A调用 // 也可能线程B调用 void foo() { char* p = (char*)pthread_getspecific(key); printf("%s\n", p); } void my_malloc() { // 去这个线程的内存池去申请内存 void* mempool = pthread_getspecific(key); // __my_malloc(mempool, ...); } void* thread_func(void* ptr) { // setspecific,需要在线程中调用,当然也可以在主线程中调用 // 为这个线程设置私有数据 pthread_setspecific(key, ptr); foo(); my_malloc(); return NULL; } void free_func(void* ptr) { printf("free call\n"); free(ptr); } int main() { pthread_key_create(&key, free_func); pthread_t tid1, tid2; pthread_create(&tid1, NULL, thread_func, strdup("thread1")); pthread_create(&tid2, NULL, thread_func, strdup("thread2")); pthread_join(tid1, NULL); pthread_join(tid2, NULL); }
#include <stdio.h> #include <stdlib.h> #include <pthread.h> #include <unistd.h> // 线程本地变量,每个线程都有一份拷贝 thread_local int result = 0; void foo() { // 全局变量 thread_local static int a = 0; a++; printf("%d\n", a); } void* thread_func1(void* ptr) { foo(); foo(); result = 100; } void* thread_func2(void* ptr) { foo(); foo(); sleep(1); // printf("%d\n", result); // 100 printf("%d\n", result); // thread_local时,这个值是0 } int main() { pthread_t tid1, tid2; pthread_create(&tid1, NULL, thread_func1, NULL); pthread_create(&tid2, NULL, thread_func2, NULL); pthread_join(tid1, NULL); pthread_join(tid2, NULL); }
11 线程取消
取消线程也结束线程,但是应该避免这种设计。
退出点函数:man pthreads
搜索cancel关键字,找到这些退出点函数。
pthread_cancel在线程外部(其他线程)来退出另外一个线程A,当线程A调用了cancelpoint函数时,会退出。
如果希望调用cancelpoint函数不退出,应该设置当前的线程状态为:不理会线程退出(cancelability disabled)
pthread_setcancelstate(...)
#include <stdio.h> #include <pthread.h> void* thread_func(void* ptr) { // 因为这个线程没有cancel point while(1) { // 关闭cancel检测 pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, NULL); sleep(10); // 打开cancel检测 pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL); // 检查cancel point pthread_testcancel(); } return NULL; } int main() { pthread_t tid; pthread_create(&tid, NULL, thread_func, NULL); // 让线程退出 pthread_cancel(tid); // 等待线程退出 pthread_join(tid, NULL); }
第九章 守护进程(Daemon)
1 前言
Linux常用于服务器,程序通常不运行在前台。运行于前台的进程和终端关联,一旦终端关闭,进程也随之退出。因为守护进程不和终端关联,因此它的标准输出和标准输入也无法工作,调试信息应该写入到普通文件中,以便将来进行错误定位和调试。而且守护进程通常以root权限运行。
2 编程规则
-
设置umask为0
-
调用fork,并让父进程退出
-
调用setuid创建新会话
-
重新设置但前目录
-
关闭不需要的文件描述符
-
重定向标准输入/标准输出/标准错误到/dev/null
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <fcntl.h> #include <sys/types.h> #include <syslog.h> int main() { pid_t pid = fork(); if(pid == 0) { pid = fork(); if(pid == 0) { // daemon process umask(0); // 设置掩码 setsid(); // 让自己变成session leader chdir("/"); // 修改当前目录 chroot("/"); // 获取最大的已经打开的文件描述符 int maxfd = 1024; // 演示 // 把所有文件关闭 int i; for(i=0; i<=maxfd; ++i) { close(i); } // 重定向0、1、2文件到/dev/null open("/dev/null", O_RDONLY); // 标准输入 open("/dev/null", O_WRONLY); // 标准输出 open("/dev/null", O_WRONLY); // 标准错误 // printf(""); // --> aaa.txt 效率低下 // syslog(LOG_ERR|LOG_KERN, "haha, this is syslog....\n"); // 后台进程不退出 while(1) sleep(1); } } }
3 出错处理
由于不能再使用标准输入和输出,因此需要调用以下函数来输出调试信息。
4 单例
守护程序往往只有一个实例,而不允许多个,可以用文件锁来实现单例。
5 惯例
惯例是指大家都这么做,不这么做显得不专业的事情。
-
单例文件路径在/var/run目录下,内容为该进程ID
-
配置文件应该在/etc目录下
-
守护的启动脚本通常放在/etc/init.d目录下
第十章 高级IO
1 前言
在文件IO中,学习了如何通过read和write来实现文件的读写。在这一章讨论一些高级的IO方式。
2 非阻塞IO
IO通常是阻塞的,比如读鼠标文件,如果鼠标未产生数据,那么读操作会阻塞,一直到鼠标移动,才能返回。这种阻塞的IO简化了程序设计,但是导致性能下降。
使用O_NONBLOCK标记打开文件,那么read行为就是非阻塞的了。如果read不到数据,read调用会返回-1,errno被标记为EAGAIN。
如果open时没有带上O_NONBLOCK,那么可以通过fcntl设置这个模式。
3 记录锁
如果多个进程/线程同时写文件,那么使用O_APPEND,可以保证写操作是原子操作,但是O_APPEND只写到文件末尾。
如果需要修改文件内容,则无法使用O_APPEND了,需要使用记录锁来锁定文件,保证写操作的原子性。
#include "../h.h" int main() { int fd = open("a.txt", O_RDWR); // lock it struct flock l; l.l_type = F_WRLCK; l.l_whence = SEEK_SET; l.l_start = 0; l.l_len = 128; int ret = fcntl(fd, F_SETLKW, &l); if(ret == 0) { printf("lock success\n"); } else { printf("lock failure\n"); } getchar(); l.l_type = F_UNLCK; fcntl(fd, F_SETLKW, &l); }
4 IO多路转接
如果一个进程,同时要响应多路IO数据,那么这个程序设计将会很麻烦。一般程序都是需要响应多路IO的,比如GUI程序都需要处理鼠标和键盘文件。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/select.h>
#include <sys/types.h>
#include <fcntl.h>
#include <errno.h>
//void FD_CLR(int fd, fd_set *set);
// 将fd从set中拿掉
//
//int FD_ISSET(int fd, fd_set *set);
//判断fd是否在集合中
//
//void FD_SET(int fd, fd_set *set);
//将fd加入到集合中
//
//void FD_ZERO(fd_set *set);
//将集合清空
// int select(int nfds, fd_set *readfds, fd_set *writefds,
// fd_set *exceptfds, struct timeval *timeout);
// int nfds: 要求是集合中最大的文件描述符+1
// fd_set* readfds: 想读取的文件描述符集合,这个参数既是输入,也是输出参数
// fd_set* writefds: 想写的文件描述符集合,一般为NULL
// fd_set* execptfds:出错,异常的文件描述符集合,一般为NULL
// struct timeval* timeout: 因为select是阻塞的调用,这个参数表示超过这个时间,无论文件描述符是否有消息,都继续往下执行
// 返回值:-1表示失败,0表示超时,而且没有任何的事件,大于0表示有事件的文件描述符的数量
int main()
{
int fd_key;
int fd_mice;
fd_key = open("/dev/input/event1", O_RDONLY);
fd_mice = open("/dev/input/mice", O_RDONLY);
if(fd_key < 0 || fd_mice < 0)
{
perror("open key mice");
return 0;
}
// fd_set 文件描述符集合类型
fd_set set;
FD_ZERO(&set);
FD_SET(fd_key, &set);
FD_SET(fd_mice, &set);
// 此时set中有两个文件描述符,分别是鼠标和键盘
int nfds = fd_key > fd_mice ? fd_key : fd_mice;
nfds ++;
struct timeval tv;
tv.tv_sec = 1; // 秒
tv.tv_usec = 0; // 微秒 1/1000000 秒
int ret;
RESELECT:
ret = select(nfds, &set, NULL, NULL, &tv); // 阻塞一秒
if(ret < 0)
{
if(errno == EINTR) // 被中断打断
{
// 补救
goto RESELECT;
}
return 0;
}
if(ret == 0)
{
}
if(ret > 0)
{
// 用户动了鼠标或者键盘,从而鼠标文件描述符或者键盘文件描述符可读
if(FD_ISSET(fd_key, &set))
{
printf("keyboard message\n");
// 键盘有消息
}
if(FD_ISSET(fd_mice, &set))
{
printf("mice message\n");
// 鼠标有消息
}
}
}
4.1 select
select的作用是,让内核监听一个fd集合,当集合中的fd有事件时,select会返回有消息的fd子集。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/select.h>
#include <sys/types.h>
#include <fcntl.h>
#include <errno.h>
// fd_set最多能容纳1024个文件
//
// unsigned int data[32]; 32x32 = 1024
int main()
{
int fd_key;
int fd_mice;
fd_key = open("/dev/input/event1", O_RDONLY);
fd_mice = open("/dev/input/mice", O_RDONLY);
int nfds = fd_key > fd_mice ? fd_key : fd_mice;
nfds ++;
// 文件描述符集合的拷贝
fd_set set1;
fd_set set2; // set1 --> set2
memcpy(&set2, &set1, sizeof(set1));
while(1)
{
fd_set set;
FD_ZERO(&set);
FD_SET(fd_key, &set);
FD_SET(fd_mice, &set);
struct timeval tv;
tv.tv_sec = 1; // 秒
tv.tv_usec = 0; // 微秒 1/1000000 秒
int ret = select(nfds, &set, NULL, NULL, &tv);
if(ret < 0)
{
if(errno == EINTR)
continue;
return 0;
}
if(ret > 0)
{
if(FD_ISSET(fd_key, &set))
{
// 既然鼠标有消息,就应该把数据都读出
char buf[1024];
read(fd_key, buf, sizeof(buf));
printf("key event\n");
}
if(FD_ISSET(fd_mice, &set))
{
char buf[1024];
read(fd_mice, buf, sizeof(buf));
printf("mice event\n");
}
}
}
}
4.2 epoll
epoll的作用和select差不多,但是操作接口完全不同。
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <sys/epoll.h>
#include <sys/types.h>
#include <fcntl.h>
// 通过epoll来实现多路io复用
int main()
{
int fd_key = open("/dev/input/event1", O_RDONLY);
int fd_mice = open("/dev/input/mice", O_RDONLY);
if(fd_key < 0 || fd_mice < 0)
{
perror("open mice and keyboard");
return -1;
}
// 创建epoll对象,创建epoll的参数已经废弃了,随便填
int epollfd = epoll_create(512);
if(epollfd < 0)
{
perror("epoll");
return -1;
}
// 把鼠标和键盘的文件描述符,加入到epoll集合中
struct epoll_event ev;
ev.data.fd = fd_key; // 联合体,这个联合体用来保存和这个文件描述符相关的一些数据,用于将来通知时,寻找文件描述符
ev.events = EPOLLIN | EPOLLONESHOT; // epoll要监听的事件,读或者写
epoll_ctl(epollfd, EPOLL_CTL_ADD, fd_key, &ev);
ev.data.fd = fd_mice;
epoll_ctl(epollfd, EPOLL_CTL_ADD, fd_mice, &ev);
// 调用epoll_ctl时,第四个参数被epoll_ctl拷贝走
struct epoll_event ev_out[2];
while(1)
{
int ret = epoll_wait(epollfd, ev_out, 2, 2000);
if(ret < 0)
{
if(errno == EINTR)
continue;
return -2;
}
if(ret > 0)
{
int i;
for(i=0; i<ret; ++i)
{
if(ev_out[i].data.fd == fd_mice)
{
// 鼠标有消息
// char buf[1024];
// read(fd_mice, buf, sizeof(buf));
printf("mice\n");
}
else if(ev_out[i].data.fd == fd_key)
{
// char buf[1024];
// read(fd_key, buf, sizeof(buf));
printf("key\n");
}
}
}
}
}
4.3 select和epoll的区别
select | epoll |
---|---|
出现早 | 晚 |
大规模文件描述符效率低 | 大规模文件描述符效率高 |
小规模是select效率高 | |
使用位域来表示描述符集合 | 使用红黑树来保存文件集合 |
5 存储映射IO
第十一章 进程间的通信
1 前言
进程间通信(IPC)方式有许多种。包括匿名管道、命名管道、socketpair、信号、信号量、锁、文件锁、共享内存等等。
由于进程之间的虚拟地址无法相互访问,但是在实际的系统中,经常要涉及进程间的通信,所以在Unix的发展中,人们创造了多种进程间通信的方式,而这些通信方式,都被Linux继承了过来。
进程间通信的原理,是在进程外的公共区域申请内存,然后双方通过某种方式去访问公共区域内存。
按照分类,进程间通信涉及三个方面:
-
小数据量通信(管道/socketpair)
-
大数据量通信(共享内存)
-
进程间同步(socketpair/管道/锁/文件锁/信号量)
2 匿名管道
用于有亲缘关系的进程间通信,匿名管道是单工通信方式。
内核的buffer究竟有多大?一个内存页尺寸。实际在Ubuntu下测试是64K。当缓冲区满的时候,write是阻塞的。
read管道时,如果管道中没有数据,那么阻塞等待。
read管道时,如果此时write端已经关闭,而此时管道有数据,就读数据,如果没有数据,那么返回0表示文件末尾。
write管道时,如果此时所有的read端已经关闭,那么内核会产生一个SIGPIPE给进程,SIGPIPE的默认会导致进程退出,如果此时进程处理了SIGPIPE信号,那么write会返回-1,错误码是EPIPE。
2.1 创建
2.2 读写
2.3 应用
单工:只能单方向通信
半双工:可以两个方向通信,但是同一时刻只能有一个方向通信
全双工:可以同时双方通信
3 命名管道
命名管道也是单工通信,但是比匿名相比,它可以用于非亲缘关系的进程。
3.1 创建
3.2 打开读端
3.3 打开写端
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/types.h>
int main()
{
// 打开文件时,添加非阻塞属性
//int fd = open("/dev/input/mice", O_RDONLY | O_NONBLOCK);
// 先打开文件,再通过fcntl设置O_NONBLOCK属性
int fd = open("/dev/input/mice", O_RDONLY);
int flags = fcntl(fd, F_GETFL);
flags |= O_NONBLOCK;
fcntl(fd, F_SETFL, flags);
while(1)
{
char buf[1024];
int ret = read(fd, buf, sizeof(buf));
if(ret == -1) // 错误发生
{
if(errno == EAGAIN || errno == EWOULDBLOCK) // EAGAIN错误码表示:底层没有数据,应该继续再尝试读 EWOULDBLOCK
{
//鼠标并没有移动,底层并没有数据可以读,这种不算真的错误
printf("mouse not move\n");
}
else // 真的有错误发生了
{
return -1;
}
}
}
}
4 socketpair
socketpair和匿名管道类似,但是它是全双工的。
4.1 创建
5 mmap实现共享内存
unix提供了一些内存共享机制,但是还是习惯使用mmap进行内存共享。
10.5.1 有亲缘关系的进程之间mmap共享
有亲缘的关系的父子进程,可以使用匿名映射,直接将虚拟地址映射到内存。
5.2 无亲缘关系的进程之间mmap共享
如果进程之间没有亲缘关系,那么就需要一个文件来进行内存共享。
但是如果使用了硬盘文件,那么效率相对底下。最好使用内存文件来映射,效率更加高。
5.3 使用shm_open打开共享内存文件
shm_open:创建内存文件,路径要求类似/somename
,以/
起头,然后文件名,中间不能带/
。
6 文件锁
#include <fcntl.h>
#include <sys/types.h>
#include <sys/file.h>
#include <stdio.h>
int main()
{
int fd = open("a.txt", O_RDWR);
// flock(fd, LOCK_SH); // 共享
flock(fd, LOCK_EX); // 排他锁
// 可以对文件进行读操作
sleep(10);
flock(fd, LOCK_UN); // 解锁
close(fd);
}
#include <fcntl.h>
#include <sys/types.h>
#include <sys/file.h>
#include <stdio.h>
int main()
{
int fd = open("a.txt", O_RDWR);
// flock(fd, LOCK_EX); // 排他锁
int ret = flock(fd, LOCK_SH|LOCK_NB); // 共享锁
if(ret == 0)
{
printf("get lock\n");
// flock(fd, LOCK_EX); // 排他锁
// 可以对文件进行读操作
sleep(1);
flock(fd, LOCK_UN); // 解锁
}
else
{
printf("can not get lock\n");
}
close(fd);
}
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/file.h>
int main()
{
int fd = open("a.txt", O_RDWR);
pid_t pid = getpid();
printf("process id is %d\n", (int)pid);
// 锁文件开始位置的4K内容
struct flock l;
l.l_type = F_WRLCK;
l.l_whence = SEEK_SET;
l.l_start = 0;
l.l_len = 4096;
fcntl(fd, F_SETLKW, &l); // F_SETLKW:锁文件,如果锁不上(原因:别人上锁了),就等
printf("get lock\n");
sleep(10);
// 解锁
l.l_type = F_UNLCK;
fcntl(fd, F_SETLKW, &l);
close(fd);
}
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/file.h>
int main()
{
int fd = open("a.txt", O_RDWR);
// 锁文件开始位置的4K内容
struct flock l;
l.l_type = F_WRLCK;
l.l_whence = SEEK_SET;
l.l_start = 1024;
l.l_len = 4096;
fcntl(fd, F_GETLK, &l);
printf("pid = %d\n", (int)l.l_pid);
#if 0
fcntl(fd, F_SETLKW, &l); // F_SETLKW:锁文件,如果锁不上(原因:别人上锁了),就等
printf("get lock\n");
sleep(10);
// 解锁
l.l_type = F_UNLCK;
fcntl(fd, F_SETLKW, &l);
#endif
close(fd);
}
7 锁
pthread_mutex_init的锁,可以用于进程间同步,但是要求锁变量在共享内存中。
8 信号量
信号量用于计数,而不用考虑进程竞争问题。
第十二章 UDP套接字
1 前言
上一章讲述了TCP通信方式,它是基于流的面向连接的网络通信。UDP是IP协议上的另一种传输协议。
TCP和UDP都是端到端的通信协议,都处于TCP/IP网络模型的第三层(传输层)。
它和TCP的区别是:
TCP | UDP | 解释 |
---|---|---|
基于流 | 基于报文 | |
有连接 | 无连接 | |
有保障 | 无保障 | |
效率低 | 效率高 | |
适用稳定传输场合 | 适合允许报文丢失的场合 |
2 创建UDP socket
2 绑定地址
绑定地址和TCP一样
3 发送和接收
调用的接口和TCP不一样,行为不同。TCP的发送会发生粘包情况,而UDP不会。TCP发送认为是可靠的,而UDP的发送可能会发生丢失和乱序。
UDP的发送大部分时候使用sendto,因为send函数没有提供目标地址。如果UDP socket调用了connect函数,也可以使用send函数。
4 广播和多播(组播)
由于UDP没有连接,所以可以支持广播和多播。
5 关闭socket
使用close
注意:
-
UDP也可以调用connect函数,但是connect函数只是让udp socket保存默认的发送地址,以便可以简单的调用send函数来发送数据。
-
UDP的数据是基于报文的,客户端调用一次send,产生一个UDP报文,接收一次只能接收一个报文。
-
如果recv时,程序提供的缓冲区小于UDP报文长度,那么会导致数据丢失。如何得到数据报文的长度???UDP报文不要超过MTU(1400)
-
广播时,发送端做额外设置,允许发送广播,接收端还是默认处理接口。允许发送广播socket,也可以接收数据。
-
组播,发送端不需要额外设置,只需要发送地址改成组播地址即可。接收端加入组。
第十三章 通信服务模型
1 前言
高性能服务器方面的讨论,主要面对TCP。
2 使用多进程
优点:好理解/简单
缺点:每个客户端对应一个进程,资源消耗大
适合:少量客户端
应用:早期的apache服务器
3 使用多线程
优点:好理解/简单
缺点:每个客户端对应一个线程,资源消耗大
适合:少量客户端
应用:优化过的apache服务器
4 使用多路IO复用技术
select(跨平台) epoll(linux) pselect(x) poll(unix) kqueue(freeBSD) iocp(windows平台)
4.1 select
优点:跨平台,在少量文件描述符集合中,效率高
缺点:只能监听最多1024文件描述符
4.2 epoll
优点:支持大规模网络服务
缺点:只支持linux
5 使用多路IO服用技术和线程池
特点:两个线程,一个线程负责epoll_wait和accept,另外一个线程负责接收和处理数据
优点:支持大规模网络服务,并且支持高并发。
6 使用多进程并发和多路IO复用技术
特点:多个进程同时监听一个端口,如果外部有连接,多个进程通过内核实现的竞争机制,会有一个进程被唤醒。
优点:效率非常高,nginx就是用这种方式实现的。
7 使用现有通信库xs
8 使用现有通信库libevent
libevent libev ace ASIO xs
了解:听过这个词,知道它是干嘛的。
熟悉:把它代码下载下来,编译一下,写一个C用libevent实现hello world
掌握:把libevent大部分的功能使用一遍
精通:使用libevent开发一个产品
大牛:你能找到libevent bug,甚至修改它的bug,并提交。
-
libevent代码下载
git clone https://github.com/libevent/libevent.git
-
编译
安装位置:
头文件:/usr/local/include
库文件:/usr/local/lib
openssl:加密安全
ffmpeg:音视频处理
iconv:文本转换
-
编写代码
-
编译
g++ t01_libevent_hello_world.c -levent_core
注意链接相应的库
-
运行
第十四章 HTTP协议
1 前言
在TCP/IP协议基础上,有很多应用层协议,支持各种网络应用,比如HTTP,SMTP,FTP等等。HTTP协议是最广泛的应用层协议
2 通信模型
-
支持客户/服务器模式。
-
简单快速:客户向服务器请求服务时,只需传送请求方法和路径。
-
灵活:HTTP允许传输任意类型的数据对象。
-
短连接:无连接的含义是限制每次连接只处理一个请求。
-
无状态:HTTP协议是无状态协议。
3 地址(URL)
URL全称为Unique Resource Location,用来表示网络资源,可以理解为网络文件路径。URL的格式如下:
例如:
URL的长度有限制,不同的服务器的限制值不太相同,但是不能无限长。以下博文有对RUL长度的一些叙述:
http://www.cnblogs.com/henryhappier/archive/2010/10/09/1846554.html
补充:HTTP协议的服务器和客户端
服务器
Http服务器/Web服务器:apache,nginx,iis(Windows平台)
客户端:浏览器
客户端
curl命令
curl是一个命令行数据传输工具,它支持很多协议,包括DICT, FILE, FTP, FTPS, GOPHER, HTTP, HTTPS, IMAP, IMAPS, LDAP, LDAPS, POP3, POP3S, RTMP, RTSP, SCP, SFTP, SMTP, SMTPS, TELNET and TFTP。这里使用curl来学习HTTP协议,因此重点说明HTTP相关的内容。
curl命令格式
例如
选项[option]
选项 | 长选项 | 解释 |
---|---|---|
-# | --progress-bar | 显示进度条 |
-d | --data, --data-ascii, --data-binary | post协议的数据 |
-F | --form <name=content> | 提交表单 |
4 HTTP请求
HTTP请求由三部分组成,分别是:请求行、消息报头、请求正文
4.1 请求方法
请求行由请求方法字段、URL字段和HTTP协议版本字段3个字段组成,它们用空格分隔。例如:GET /index.html HTTP/1.1
。
HTTP协议的请求方法有GET、POST、HEAD、PUT、DELETE、OPTIONS、TRACE、CONNECT。
而常见的有如下几种:
-
GET
GET方法是HTTP协议中最常见的请求方式,当客户端需要从服务器中读取文档时,往往使用GET方法。GET方法要求服务器URL指定的文档内容。而内容部分,按照HTTP协议规定,放在响应报文的正文部分。
GET方法请求,没有请求正文内容,请求时的参数在URL中携带,由于URL被限制了长度,因此GET方法不适合用于上传数据。同时在浏览器中,通过GET方法来获取网页时,参数会显示在浏览器地址栏上,因此保密性很差。
-
POST
POST方法运行客户端给服务器提供比较多的信息。POST方法将请求参数封装在HTTP请求数据中,而且长度没有限制,因为POST携带的数据,在HTTP的请求正文中。
-
HEAD
HEAD只返回信息头,不要求服务器返回URL所指资源内容。
4.2 消息报头
消息报头由关键字/值对组成,每行一对,关键字和值用:分割。
比如
4.3 空行
最后一个消息报头和正文之间,是一个空行。GET请求没有正文。
4.4 请求正文
请求正文在POST方法中使用。
5 HTTP响应
HTTP响应由三部分组成,分别是:状态行、消息报头、响应正文。HTTP响应的格式于请求的格式很类似:
唯一的区别就是第一行的请求行,变成了状态行。状态行格式举例如下:
其中HTTP/1.1表示协议和版本信息,200表示服务器的响应码,OK表示状态码的描述,状态码由3位数组成,第一个数字表示了响应的类别,一共有5种类别。
-
1xx:表示服务器正在处理
-
2xx:成功,请求被正确的理解或者处理
-
3xx:重定向
-
4xx:客户端错误,客户端的请求,服务器无法理解。
-
5xx:服务器错误,服务器未能实现的合法请求。
常见的状态码举例:
-
200 OK:客户端请求成功
-
400 Bad Request:请求报文有语法错误
-
401 Unauthorized:未授权
-
403 Forbidden:服务器拒绝服务
-
404 Not Found:请求的资源不存在
-
500 Internal Server Error:服务器内部错误
-
503 Server Unavailable:服务器临时不能处理客户端请求(稍后可能可以)
响应报文例子:
6 使用HTTP协议实现通信
服务器的开发不容易,尤其是开发高性能、稳定性好服务器,更加不容易,因此人们尝试更好简单的方式来开发软件。在服务器方面,使用WEB服务器,采用HTTP协议来代替底层的SOCKET,是常见的选择。采用HTTP协议更加除了能得到稳定的服务器支持外,更加可以兼容各种客户端(手机、PC、浏览器)等等。这样实现了一个服务器之后,多个客户端可以通用。
在开发应用程序时,客户端应用程序为了和WEB服务器通信,需要对请求报文进行打包成HTTP请求格式,而服务器响应了HTTP响应报文到客户端之后,也需要对响应报文进行解释。libcurl库,可以完成这些功能。
而Web服务器的选择是多样化的:Apache,nginx,libevent,tufao。一些使用C/C++开发的开源的Http服务器列表在这里:
http://www.oschina.net/project/tag/106?lang=21&os=0&sort=time
通信时,应用程序的通信报文目前采用的json格式较多,也有一些采用xml格式或者http协议规定的表单格式。
补充
客户端:浏览器
服务器:apache + c语言写的CGI程序
实现HTTP通信
CGI技术在早期解决动态网页,和网页跟服务器交互的问题。
-
apache2安装
-
了解配置
静态文档位置:/var/www/html
apache可执行程序:/usr/lib/cgi-bin/
为了支持CGI,需要以下命令支持
简单的CGI程序代码案例,编译出的可执行程序,需要在/usr/lib/cgi-bin目录下
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
extern char** environ;
int main()
{
// http协议的规定
// 重定向
printf("Content-type:text/html\n\n");
// 输出环境变量
for(int i=0; ;++i)
{
if(environ[i])
{
// printf("%s<br>\n", environ[i]);
}
else
{
break;
}
}
// 获取环境变量QUERY_STRING的值,获取客户端参数
char* query_string = getenv("QUERY_STRING");
strtok(query_string, "=&");
char* username = strtok(NULL, "=&");
strtok(NULL, "=&");
char* password = strtok(NULL, "=&");
if(strcmp(username, "aa") == 0
&& strcmp(password, "bb") == 0)
{
printf("Login success<br>\n");
}
else
{
printf("Login error<br>\n");
}
}
6.1 libcurl
libcurl 官网:https://curl.haxx.se/
libcurl也是curl工具使用的库
-
下载源码
可以到官网下载,更方便的是到github上克隆代码
-
编译和安装
安装完毕之后,头文件
/usr/local/include/curl
/usr/local/lib/libcurl.so
可执行命令
/usr/local/bin/curl
另外一种办法是:可以简单的执行
sudo apt-get install curl
安装curl命令,该命令将curl安装/usr/bin
sudo apt-get install libcurl4-openssl-dev
来安装libcurl,libcurl的安装路径
头文件:/usr/include/curl
库目录:/usr/lib
-
使用
#include <stdio.h>
#include <curl/curl.h>
bool getUrl(char *filename)
{
CURL *curl;
CURLcode res;
FILE *fp;
if ((fp = fopen(filename, "w")) == NULL) // 返回结果用文件存储
return false;
struct curl_slist *headers = NULL;
headers = curl_slist_append(headers, "Accept: Agent-007");
curl = curl_easy_init(); // 初始化
if (curl)
{
//curl_easy_setopt(curl, CURLOPT_PROXY, "10.99.60.201:8080");// 代理
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);// 改协议头
curl_easy_setopt(curl, CURLOPT_URL,"http://www.baidu.com");
curl_easy_setopt(curl, CURLOPT_WRITEDATA, fp); //将返回的http头输出到fp指向的文件
curl_easy_setopt(curl, CURLOPT_HEADERDATA, fp); //将返回的html主体数据输出到fp指向的文件
res = curl_easy_perform(curl); // 执行
if (res != 0) {
curl_slist_free_all(headers);
curl_easy_cleanup(curl);
}
fclose(fp);
return true;
}
}
bool postUrl(char *filename)
{
CURL *curl;
CURLcode res;
FILE *fp;
if ((fp = fopen(filename, "w")) == NULL)
return false;
curl = curl_easy_init();
if (curl)
{
curl_easy_setopt(curl, CURLOPT_COOKIEFILE, "/tmp/cookie.txt"); // 指定cookie文件
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, "&logintype=uid&u=xieyan&psw=xxx86"); // 指定post内容
//curl_easy_setopt(curl, CURLOPT_PROXY, "10.99.60.201:8080");
curl_easy_setopt(curl, CURLOPT_URL, " http://mail.sina.com.cn/cgi-bin/login.cgi "); // 指定url
curl_easy_setopt(curl, CURLOPT_WRITEDATA, fp);
res = curl_easy_perform(curl);
curl_easy_cleanup(curl);
}
fclose(fp);
return true;
}
int main(void)
{
getUrl("/tmp/get.html");
postUrl("/tmp/post.html");
}
基本流程是:
-
初始化CURL环境
-
创建CURL对象
-
设置CURL
-
执行CURL
6.2 tufao
tufao是一个由QT编写的HTTP服务器。
tufao代替apache来实现http的通信。
安装tufao
-
获取代码
-
编译和安装
-
sudo apt-get install cmake qtsdk
-
在tufao目录下创建build目录
-
cd build
-
cmake .. -DCMAKE_INSTALL_PREFIX=/usr
-
make
-
sudo make install
-
-
创建工程
-
创建空的工程
-
工程文件中增加
CONFIG += TUFAO1 C++11
-
增加一个类MyServer,一定是QObject派生类
-
增加一个main.cpp实现main函数
-
在MyServer的构造函数,创建Tufao::HttpServer对象server
-
将server的信号
requestReady
和自己写的槽函数slotRequestReady
连接 -
在
slotRequestReady
函数中,实现http协议的响应报文。
-
6.3 libevent
略。
6.4 json
JSON是一种轻量级的数据交换格式,它简洁并且具有很好的可拓展性,容易阅读,也容易被机器解析,因此JSON成为当前最流行的数据交换格式。
JSON格式例子如下:
-
用JSON来描述一个人的属性。
-
嵌套对象:
-
数组:
-
根节点为数组
JSON由成对的key: value
序列组成。key的类型永远是字符串,而value的类型可以是:
-
数字:可表示整数或者浮点数
-
boolean:true或者false
-
null:null类型只有null一个值
-
字符串:
-
对象:使用{}定义
-
数组:使用[]定义
各种语言都有JSON的生成和解析的库,C语言使用cJson,C++则更多的使用rapidJson。
6.5 xml
略。
7 HTTPS安全通信
一般网络通信是不安全的,因为有许多中间设备可能通过类似抓包的技术,可以获取报文,如果报文中携带一些敏感的信息,比如用户名和密码信息。
如果使用对称加密技术,密钥传输的安全性同样值得怀疑,而一旦密钥泄露,加密形同虚设。
非对称加密技术可以使用的加密密钥和解密密钥是不同的,加密密钥不能用于解密,这样加密密钥泄漏了,也不影响数据安全性。这样客户端可以安全得到加密密钥,而其他人得到加密密钥无法解密。
在HTTPS中,客户端随机生成对称的加密密钥,然后通过服务器给的非对称的加密密钥,加密对称的加密密钥,然后发给服务器,后续的通信使用对称机密。
HTTPS技术使得传输过程的安全无懈可击,但是如果认为任何带HTTPS的网站就是安全的那就错了,因为有些钓鱼网站,来骗取用户的敏感信息,因此需要第三方机构来监控提供服务的服务器的安全性。HTTPS证书需要花钱购买,也有一些机构颁发免费的HTTPS证书。
有些更加严格的HTTPS通信,要求客户端也必须有证书。因为别人可以得到加密密钥,就可以实行假冒行为。银行的客户端一般需要证书,而证书保存在银行配的u盘中。
在实际的编码中,HTTPS并不需要增加多少额外的工作,就可以实现HTTPS通信。
8 负载均衡
负载均衡技术,可以使得多个服务器分别负担繁重的用户请求。负载均衡可以通过许多技术来实现,比如DNS、NGINX反向代理、LVS、重定向等等。
目前比较流行的有NGINX的反向代理实现负载均衡。
第十五章 Bash脚本
1 前言
通过之前的学习,懂得了如何在Linux下进行编程来拓展系统功能。但是一些小功能,可以通过脚本直接实现,就没有必要使用C语言来编程实现。
脚本语言能快速实现一些功能,并且在不需要编译情况下直接运行。因此开发效率更高,但是运行效率略低。
就像编程语言有C/C++, JAVA,Basic等等一样,脚本语言也有许多,Linux下常用的Shell脚本有Bash,也是这个课程的重点。其他的脚本语言有:JavaScript,Lua,python,php等等。
将以上的代码保存到hello.sh,然后
脚本的运行需要脚本解析器,因为脚本程序是文本格式,CPU无法直接运行它,因此通过脚本解析器间接运行脚本程序。
#
表示注释,但是第一行的#
有特殊意义,用来指示解析器。
参考:
http://www.runoob.com/linux/linux-shell.html
2 变量
在Bash中,可以访问的变量有环境变量、命令行参数和自定义变量。变量并没有类型,对于Bash来说都是字符串。
2.1 引用变量
使用$var
或者${var}
得到变量的值,建议使用${var}
2.2 环境变量
Bash脚本运行时,脚本可以访问环境变量
2.3 参数变量
如果执行脚本时,有携带参数,那么可以通过$1之类的方法访问参数。
脚本函数也是这样访问参数的
2.4 局部变量
可以自定义变量,但是要注意自定义变量时,等号左右不要有空格。
2.5 变量赋值
自定义变量可以通过=赋值,除了简单的赋值之外,有些注意的事项
-
赋值内容带空格时,需要加"",在Bash里,空格是分割符号,如果没有"",当然使用转义
\
也是可以的。
也可以用单引号来表示字符串
-
可以通过`符号获得其他命令或者脚本的运行结果(标准输出),比如
2.6 设置环境变量
通过export
命令可以修改环境变量,环境变量可以用于脚本解析器之间的参数传递。
思考:如果有一个环境变量和一个普通变量名是一样的,会是什么情况?
2.7 运算
Bash变量都是字符串,所以不能直接进行四则混合运算,可以通过expr
命令间接实现。
注意+号两边的空格是必须的,乘号*有特殊意义,所以需要\
进行转义。
3 运行流程控制
3.1 if-else
then单独一行,是必须的,如果想写成一个单行,也可以使用;
分割。
使用命令man test
查看条件格式
3.1.1 数值条件
类似-gt
,有-ge
,-lt
,-le
,-eq
,-ne
等等。
4.1.2 字符串条件
字符串的比较可以使用=
和!=
,以及使用-n
和-z
来表示是否为空。
4.1.3 文件条件
类似-e
,还有-d
,-f
等
15.4.1.4 逻辑表达式
类似-a
还有-o
。也支持使用C++
的&&
和||
来代替-a
和-o
。
使用!
表示not,表示取反。
优先级 !
> -a
> -o
4.1.5 条件短路
条件短路使用的是
3.2 循环
3.2.1 for
3.2.2 while
3.2.2 break和continue
与C语言类似。
3.3 分支
跟C语言的switch只能使用整数不同,Bash的switch可以使用字符串。
4 模块化
4.1 函数
4.2 文件包含
4.3 执行脚本
如果在一个脚本A里执行另外一个脚本B,那么和使用. a.sh
是不一样的,它fork
了新解析器进程来执行脚本。这样,一旦B脚本运行结束,在脚本B中的变量都不存在了,设置的环境变量也不存在了。
5 输入/输出和重定向
5.1 命令行参数
执行Bash脚本时,也可以传递命令行参数,在脚本中可以通过$1
,$2
等等获取参数,也可以通过$0
获得脚本文件名,通过$#
获取脚本参数个数。
5.2 重定向输入
除了可以通过命令行给Bash脚本传递参数之外,也可以通过标准输入提供参数。
在脚本中可以通过read
获取标准用户输入。
也可以通过重定向,将文件内容输入到参数。
5.3 退出码
运行脚本其实是脚本解析器在运行脚本,脚本解析器进程退出时,就像其他进程退出一样,有退出码,通过$?
可以获取进程退出码。
5.4 重定向输出
重定向输出与重定向出入差不多,通过>
可以将标准输出重定向到文件,而>>
则表示追加方式写入文件。
xargs
xargs和重定向配合,用来将之前命令的输出的每一个元素,作为后面一个命令的参数,进行执行。所以后面的命令有可能会执行多次。
6 高级Bash命令和正则表达式
6.1 正则表达式
正则表达式是用一种规则字符串来表示一类字符串的工具,在搜索匹配方面非常有用。比如在一个文档中,搜索所有的email,或者电话号码等。
正则表达式参考:
http://www.runoob.com/regexp/regexp-tutorial.html
http://www.jb51.net/tools/zhengze.html
6.1.1 例子
参考内容:
把参考内容写入a.txt,然后输入以下命令:
其中
是正则表达式,该表达式表示了电话号码的规则。
6.1.2 元字符和转义
正则表达式规定一些字符来表示简单规则,这些被称之为元字符。
元字符 | 规则 |
---|---|
. | 匹配除换行符以外的任意字符 |
\w | 匹配字母或数字或下划线或汉字 |
\s | 匹配任意的空白符 |
\d | 匹配数字,在grep中要使用-P参数才能使用该元字符 |
\b | 匹配单词的开始或结束 |
^ | 匹配字符串的开始 |
$ | 匹配字符串的结束 |
\W | 匹配任意不是字母,数字,下划线,汉字的字符 |
\S | 匹配任意不是空白符的字符 |
\D | 匹配任意非数字的字符 |
\B | 匹配不是单词开头或结束的位置 |
[a-z] | 匹配a到z的任意一个字符 |
[0-9] | 匹配数组0-9 |
[acek-i] | 匹配一些字母 |
[^x] | 匹配除了x以外的任意字符 |
[^aeiou] | 匹配除了aeiou这几个字母以外的任意字符 |
如果需要搜索元字符,那么需要转义,比如想搜索文章中的$
,那么需要使用\$
来表示。
正则表达式由原字符和普通字符组成,匹配会一个个字符进行。
6.1.3 重复
字符或者元字符可以通过定义重复次数,比如搜索有重复的字母。
重复符号 | 解释 |
---|---|
* | 重复零次或更多次 |
+ | 重复一次或更多次 |
? | 重复零次或一次 |
{n} | 重复n次 |
{n,} | 重复n次或更多次 |
{n,m} | 重复n到m次 |
6.1.4 分组
如果想实现子规则重复,那么需要使用分组。使用()
进行分组,并且使用\1
,\2
来表示组号。
6.1.5 选择
使用|
表示选择,即匹配其中之一便可。
6.1.6 贪婪和懒惰
当使用a+
时,表示匹配一个或者多个a,如果内容有aaaa
,那么可以匹配a,aa,aaa,aaaa,那么正则表达式默认是使用贪婪方式匹配,即匹配尽可能长的字符串。如果有?
,则表示懒惰匹配:a+?
代码/语法 说明
*? 重复任意次,但尽可能少重复
+? 重复1次或更多次,但尽可能少重复
?? 重复0次或1次,但尽可能少重复
{n,m}? 重复n到m次,但尽可能少重复
{n,}? 重复n次以上,但尽可能少重复
6.2 awk
参考:
http://xueguoliang.cn/awk
http://awk.readthedocs.io/en/latest/index.html
15.6.2.1 前言
awk是一个特别删除处理表格数据的字符串处理工具。比如打印所有运行的进程号:
一个更加全面的例子:
打印第三列(占用CPU)大于0.0的进程号和占用CPU的百分比。
6.2.2 awk结构
pattern是模版,action是对应的动作。
6.2.3 输出
ation最简单的做法就是通过print进行输出。$0
表示输出整行$1
表示输出第一列$2
表示输出第二列
以此类推。
可以使用NF
来表示字段数量,$NF
来表示最后一列
6.2.4 pattern
pattern表示匹配模式,用来选择处理行。比如$3>0.01
其他匹配模式
-
BEGIN { 语句 }
在读取任何输入前执行一次 语句 -
END { 语句 }
读取所有输入之后执行一次 语句 -
表达式 { 语句 }
对于 表达式 为真(即,非零或非空)的行,执行 语句 -
/正则表达式/ { 语句 }
如果输入行包含字符串与 正则表达式 相匹配,则执行 语句 -
组合模式 { 语句 }
一个 组合模式 通过与(&&),或(||),非(|),以及括弧来组合多个表达式;对于组合模式为真的每个输入行,执行 语句 -
模式1,模式2 { 语句 }
范围模式(range pattern)匹配从与 模式1 相匹配的行到与 模式2 相匹配的行(包含该行)之间的所有行,对于这些输入行,执行 语句 。
6.3 sed
参考
http://man.linuxde.net/sed
http://sed.sourceforge.net/sed1line_zh-CN.html
6.3.1 前言
sed算是一种批处理编辑器。它的功能是读取文件内容的一行,然后按照指定规则处理,然后输出。之后再重复处理下一行,直到文件末尾。
6.3.2 命令格式
其中[option]是可选的,此处不讨论。'command'指sed命令,是讨论的重点。
sed命令列表
命令 | 解析 |
---|---|
a\ | 在当前行下面插入文本。 |
i\ | 在当前行上面插入文本。 |
c\ | 把选定的行改为新的文本。 |
d | 删除,删除选择的行。 |
D | 删除模板块的第一行。 |
s | 替换指定字符 |
h | 拷贝模板块的内容到内存中的缓冲区。 |
H | 追加模板块的内容到内存中的缓冲区。 |
g | 获得内存缓冲区的内容,并替代当前模板块中的文本。 |
G | 获得内存缓冲区的内容,并追加到当前模板块文本的后面。 |
l | 列表不能打印字符的清单。 |
n | 读取下一个输入行,用下一个命令处理新的行而不是用第一个命令。 |
N | 追加下一个输入行到模板块后面并在二者间嵌入一个新行,改变当前行号码。 |
p | 打印模板块的行。 |
P(大写) | 打印模板块的第一行。 |
q | 退出Sed。 |
b lable | 分支到脚本中带有标记的地方,如果分支不存在则分支到脚本的末尾。 |
r file | 从file中读行。 |
t label | if分支,从最后一行开始,条件一旦满足或者T,t命令,将导致分支到带有标号的命令处,或者到脚本的末尾。 |
T label | 错误分支,从最后一行开始,一旦发生错误或者T,t命令,将导致分支到带有标号的命令处,或者到脚本的末尾。 |
w file | 写并追加模板块到file末尾。 |
W file | 写并追加模板块的第一行到file末尾。 |
! | 表示后面的命令对所有没有被选定的行发生作用。 |
= | 打印当前行号码。 |
# | 把注释扩展到下一个换行符以前。 |
第十六章 网络IPC:TCP套接字通信
1 前言
前章描述了在一个系统内,进程间的通信方式(IPC),而在两个系统之间,进程可以通过网络进行通信(TCP)。socket编程接口可以支持很多网络通信协议,但是本章只讨论TCP/IP协议。
如果想了解网络通信基础知识,请先看A01章。
2 TCP通信示例代码
-
服务器
1 #include<stdio.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<string.h>
#include<netinet/in.h>
int main() 2 { 3 //创建socket 4 int fd = socket(AF_INET, SOCK_STREAM, 0);//分配端口 5 if(fd<0) 6 { 7 perror(""socket); 8 return 0 9 } 10 //固定地址 11 struct sockaddr_in addr;//端口数据定义 12 addr.sin_family = AF_INET; 13 addr.sin_port = htons(9989);//端口 把一个段证书,转换程网络字节序(大端) 网络通讯协议都用大端 14 //本地端口转到网络端口 网络端口转本地端口
//htons 16 ntohs
//htonl 32 notohl
//htonll 64 ntohll
addr.sin_addr.s_addr = INADDR_ANY;//ip地址设置, 此函数表示ip地址随意 15 16 int ret=bind(fd, (struct sockaddr*)&addr, sizeof(addr));//绑定端口 17 if(ret < 0)//绑定经常会不成功,端口可能被别人占用
{
perror("bind");
return 0;
}
//后面一个参数用于链接缓冲
listen(fd, 10);
//等待客户段的链接,accept是阻塞,除非有人链接 18 int newfd = accept(fd, NULL, NULL);//收发数据 19 20 char buf[1024]=“hello client”; 21 write(newfd, buf, strlen(buf)+1);
//recv(newfd, buf, sizeof(buf), 0); 22 read(newfd, buf,sizeof(buf)); 23 printf("data: %s\n", buf); 24 //send(newfd, buf, strlen(buf)+1, 0); 25 26 close(newfd); 27 close(fd); 28 }
-
客户端
#include<stdio.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<string.h>
#include<netinet/in.h>
int main() { int fd = socket(AF_INET, SOCK_STREAM, 0);//分配端口地址 struct sockaddr_in addr; addr.sin_family = AF_INET; addr.sin_port = htons(9988);
//对方的IP地址 addr.sin_addr.s_addr = inet_addr("127.0.0.1");//IP地址为32位整数,将字符串改为整数 //链接服务器 为阻塞调用,因为connect函数,内部其实完成了三次握手 int ret = connect(fd, (struct sockaddr*)&addr, sizeof(addr)); if(ret<0)
{
perror("connect");
return -1;
}
//send(fd, "hello", 6, 0); char buf[1024]="hello server";
write(fd, buf, strlen(buf)+1);
read(fd, buf, sizeof(buf));
printf("%s\n",buf); //recv(fd, buf, sizeof(buf), 0); close(fd); return 0; }
3 套接字描述符
套接字是通信端抽象,在Linux系统中,使用文件描述符来标记套接字。而且也可以用read/write来读写套接字。
创建套接字:
套接字通信是全双工的,可以使用shutdown来关闭一个方向的通信:
4 寻址
发送数据给网络上的应用程序时,需要知道该应用程序的地址。互联网应用程序的地址由IP地址和端口号组成。IPv4地址是一个32位整数,而端口是一个16位整数。地址相关问题包括字节序、地址格式、端口。
IP地址和端口标识一台设备的服务
4.1 字节序(由CPU决定)
在同一台机器上通信时,不需要考虑字节序问题,但是在不同的机器中通信,字节序变得敏感。
有些系统采用大端字节序,而有些系统采用小端字节序。
而通信协议规定,协议中使用的数值,都是大端字节序。
这样当应用程序收到数据之后,需要进程转换。
地址格式
socket支持很多网络协议,网络协议的地址格式:
struct sockaddr_in 结构把struct sockaddr结构给细化,特例华了,其实他们的长度是一致。
4.3 端口
端口是TCP/IP协议地址的一部分,用来标识应用程序。一个系统的网卡数量有限,能分配的IP地址也有限,但是多个应用程序需要使用网络接口通信,那么需要在报文中携带端口来区分应用程序。
端口是一个非负整数,取值范围为0~65535。
5 绑定地址
服务器使用bind
来将一个socket和地址进行绑定。
客户端可以不绑定地址,由系统指派地址。
通过bind函数让系统绑定socket的端口。如果不调用bind绑定端口,那么这个socket的端口是系统指派的随机值。所以服务器一般需要绑定,而客户端不需要。
如果一个端口已经被占用,另外一个程序也尝试取绑定该端口,会失败。可以通过bind特性实现应用程序的单例。
6 建立连接
服务器使用listen
来监听连接。其实设置服务器监听的缓冲区。2001年5,2005年10, 2015年200.
客户端使用connect
来连接服务器。连接服务器会导致占用一个listen指定的缓冲区。产生一个pending的连接。
服务器使用accept
来接收连接。accept将从缓冲区获取一个pending的连接,并且处理。
connect
和accept
都是阻塞的调用。
例外:connect阻塞演示不成功。
7 数据传输
双方都可以调用recv
或者read
来接收数据。
双方都可以调用send
或者write
来发送数据。
都是阻塞的调用。write在缓冲区满的时候,会阻塞。缓冲区满不单指本地的缓冲区满。
每个socket都有两个缓冲区,一个读缓冲区,一个写缓冲区。可以设置,也可以获取。
socklen_t optlen=sizeof(optval);
getsockopt(获取缓冲区长度) 参数1:文件描述符 参数2:SOL_SOCKET 参数3:SO_SNDBUF 参数4:&optlen
setsockopt可以获取。
8 设置选项
socket描述符可以使用fcntl来修改选项,比如O_NONBLOCK。但是socket特有的选项,只能通过setsockopt来实现。
int len = 256;
setsockopt(sockfd, SOL_SOCKET, SO_RCVBUF, &len, sizeof(len));
9 关闭套接字
使用close关闭,对端关闭,本端read/recv返回0。如果对端关闭,本端写socket,会产生SIGPIPE,如果进程忽略SIGPIPE,那么write会返回-1,错误码是EPIPE。
10 常见网络设备
中继器(repeater)
集线器(HUB)
交换机(switch):来接相同的网络
路由器(router):可以链接不同的网络
网卡(interface)
网桥(bridge)
网关(gateway)
调制解调器(moden)
A1.4 常见概念
客户端
服务器
MTU 1500
TCP/UDP:传输层协议
局域网
广域网
ip地址:四个字节(IPV4),ipv6使用16字节来表示
MAC地址:网卡自带的硬件地址
端口:非负的整数范围是0~65535,匹配应用程序的
URL:唯一资源定位
web服务器:提供网页浏览服务器,apache, nginx, IIS
dns:域名解析系统
应用层网关:不同内网之间的通信
打洞:
长连接:
短连接
广播
多播(组播)
流
数据报
连接和无连接
补充:
流:一序列和时间相关的数据单位。对于TCP流来说,数据单位是字节。
比如先发送3个字节,再发送5个字节,接收端可以一次性接收8个字节,也可以先接收4个字节,再接收4个字节。