环境变量,PATH环境变量,环境变量表,错误处理

操作系统(OS):管理硬件,软件资源的系统软件

环境变量

形式:键=值,可以此添加环境变量,若存在,则修改(不能在等号左右加空格)

1
2
env //可查看变量列表
echo $home //查看环境变量的值

bash环境变量可分为全局环境变量,自定义/私有/局部环境变量
全局:env可看到,可被bash子进程继承
自定义:与全局相反

1
2
export 自定义环境变量 //添加成全局
unset xxx //删除全局

PATH环境变量

给bash找命令(可执行程序)用的
故不加./的可执行程序默认查找PATH环境变量

1
$PATH=$PATH:.   //添加PATH环境变量(当前路径),当前窗口有效

在 ~/.bashrc 里添加 $PATH=$PATH:. 可永久生效

1
source ~/.bashrc //可使文件对当前bash生效(刷新bash)

环境变量表

进程:正在执行的程序/程序进行的过程

所有程序环境变量由一个字符指针维护环境变量表(数组),最后一个元素为NULL
用二维指针(extern char **environ)来获取数组的首元素的地址(char *),

1
int main(int argc, char* argv[], char* envp[]) //命令行参数个数,命令行参数内容,环境变量表的起始地址

可发现用二维指针获取的环境变量和env获取的大致一样,因为environ继承了全局环境变量

其他

1
echo $PS1

该环境变量决定了命令行提示符的内容,可通过修改来精简内容

1
PS1='\W\$' //只显示最后一级目录

错误处理

整数类型全局变量errno中存储了最近一次的错误编号(包含errno.h头文件即可运用)
/usr/include/errno.h 包含了errno全局变量的外部声明
/usr/include/asm-generic/errno-base.h 包含各种错误号的宏定义

strerror()函数(包含string.h头文件)
char *strerror(int errnum)
参数传错误编号,返回错误信息字符串

perror()函数(包含stdio.h)
void perror(char const*tag)
参数传自定义的提示内容(错误相关信息),在标准出错设备上打印最近一次函数调用的错误信息(会自动在tag后加: )

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//环境变量的获取
#include<stdio.h>

int main(int argc,char* argv[],char* envp[]){
extern char** environ;
/*for(char** pp = environ;*pp;pp++){
printf("%s\n",*pp);
}*/
for(char** pp = envp;*pp;pp++){
printf("%s\n",*pp);
}
printf("---------------\n");
printf("environ = %p\n",environ);
printf("envp = %p\n",envp);
return 0;
}

库文件,静态库,动态库

静态库static(.a后缀)

本质:将库内容复制
步骤

  1. 实现代码(.c)和接口声明(.h),及接口文件(总的一个.h,包含所有接口声明)
  2. 编译成目标文件
    1
    2
    gcc -c calc.c //生成.o文件
    gcc -c show.c
  3. 打包成静态库
    1
    ar -r libmath.a calc.o show.o

用法

  1. 直接调用,执行时需要同时编译库和文件
  2. 用-l指定库名,-L指定路径(静态库名字为libxxx)
    1
    gcc main.c -lmath -L.. //在上层路径找math库
    或者用-l制定库名,用LIBRARY_PATH环境变量指定库路径
    1
    2
    export LIBRARY_PATH=$LIBRARY_PATH:.. //路径为..,编译期
    gcc main.c -lmath

动态库shared(.so)

本质:生成指令连接动态库(存在链接器),依赖于动态库
步骤

  1. 实现代码(.c)和接口声明(.h),及接口文件(总的一个.h,包含所有接口声明)
  2. 编译成目标文件
    1
    2
    gcc -c -fpic calc.c
    gcc -c -fpic show.c
  3. 打包成动态库
    1
    gcc -shared calc.o show.o -o libmath.so
    也可一步完成编译链接
    1
    gcc -shared -fpic calc.c show.c -o libmath.so

fPIC:代码大,速度慢,全平台
fpic:上反,上反,部分平台

用法

  1. 配置环境变量
    1
    export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:.. //运行期
  2. 直接使用
    1
    ./a.out

动态库的动态加载

什么时候用,什么时候加载,提升内存利用效率
需包含头文件 dlfcn.h
编译时需加 -ldl

载入内存

1
void* dlopen(char const* filename, int flag);

filename:路径,可文件名,可目录,若只有文件名的话需配置环境变量
flag:

  • RTLD_LAZY - 延迟加载:真正使用时加载
  • RTLD_NOW - 立即加载

返回值为句柄,失败NULL

获取函数地址

1
void* dlsym(void* handle, char const* symbol);

handle:访问句柄
symbol:符号名
返回符号地址

卸载

1
int dlclose(void* handle);

成功返回0,失败非0

返回错误信息

1
char* dlerror(void);

返回错误信息的指针

其他工具

1
2
nm xxx //列出当中的符号,其中T为自己写的,U为调用别的
ldd xxx //查看依赖的库

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
//动态库的动态加载
#include<stdio.h>
#include<dlfcn.h>

int main(void){
//将动态库载入内存
void* handle = dlopen("/home/tarena/2212/day02/shared/libmath.so",RTLD_NOW);
if(handle == NULL){//dlopen失败返回NULL
fprintf(stderr,"dlpen:%s\n",dlerror());
return -1;
}
//获取库中函数的地址
int (*add)(int,int) = dlsym(handle,"add");
if(add == NULL){
fprintf(stderr,"dlsym:%s\n",dlerror());
return -1;
}
int (*sub)(int,int) = dlsym(handle,"sub");
if(sub == NULL){
fprintf(stderr,"dlsym:%s\n",dlerror());
return -1;
}
void (*show)(int,char,int,int) = dlsym(handle,"show");
if(show == NULL){
fprintf(stderr,"dlsym:%s\n",dlerror());
return -1;
}
//使用
int a = 123,b = 456;
show(a,'+',b,add(a,b));
show(a,'-',b,sub(a,b));
//卸载动态库
//int ret = dlclose(handle);
//if(ret){
if(dlclose(handle)){
fprintf(stderr,"dlclose:%s\n",dlerror());
return -1;
}
return 0;
}

虚拟地址,内存映射,内存管理

虚拟地址

虚拟地址空间(范围):
32位:0x00000000~0xFFFFFFFF
2^32 4G大小
0~3G 用户用 3G~4G 内核用

64位: 0x0000 0000 0000 0000~0x0000 FFFF FFFF FFFF 用户
0xFFFF 0000 0000 0000~0xFFFF FFFF FFFF FFFF 内核

虚拟地址空间布局

参数和环境区:命令行参数和环境变量
栈区(stack):非静态局部变量
堆栈增长的预留空间
堆区(heap):动态内存分配
BSS区(bss):未被初始化的全局和静态局部变量
数据区(data):不具常属性且被初始化的全局和静态局部变量
代码区(text)/只读常量区:可执行指令, 字面值常量,具有常量性且被初始化的全局和静态局部变量
(BSS区和数据区有时也可统称数据区)

内存映射

虚拟地址映射物理地址
需包含sys/mman.h头文件

建立

1
void* mmap(void* start, size_t length, int prot, int flag, int fd, off_t offset);//也可映射磁盘文件

start:虚拟地址起始地址,NULL系统自动选定后返回
length:映射字节数,自动按页圆整(一页4096)
prot:映射区操作权限

  • PROT_ERAD 映射区可读
  • PROT_WRITE 可写
  • PROT_EXEC 可执行
  • PROT_NONE 不可访问

flag:

  • MAP_ANONYMOUS 匿名映射,映射物理地址而非文件
  • MAP_PRIVATE 映射区写操作只反映到缓冲区
  • MAP_SHARED 写操作直接反映到文件
  • MAP_DENYWRITE 拒绝写操作
  • MAP_FIXED 若在start上无法创建映射, 则失败(无此标志系统自动调整)

fd:文件描述符
offset:文件偏移量(按页圆整)
返回起始地址,失败MAP_FAILED (-1)

解除

1
int munmap(void* start, size_t length);

允许部分解除,但按页处理

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
//内存映射
#include<stdio.h>
#include<string.h>
#include<sys/mman.h>// mmap munmap

int main(void){
//建立映射
char* start = mmap(NULL,8192,PROT_READ | PROT_WRITE,MAP_PRIVATE | MAP_ANONYMOUS,0,0);
if(start == MAP_FAILED){
perror("mmap");
return -1;
}
//向内存中写入数据
strcpy(start,"铁锅炖大鹅");
printf("%s\n",start);

//解除映射
if(munmap(start,4096) == -1){
perror("munmap");
return -1;
}
//printf("%s\n",start);

char* start2 = start + 4096;
strcpy(start2,"小鸡炖蘑菇");
printf("%s\n",start2);
if(munmap(start2,4096) == -1){
perror("munmap");
return -1;
}
//printf("%s\n",start2);//报错
return 0;
}

其他

段错误:一切对虚拟内存的越权访问,都会导致段错误

文件系统

文件的打开与关闭

包含头文件fcntl.h

1
int open(char const* pathname, int flags, mode_t mode);

flags:

  • O_RDONLY 只读
  • O_WRONLY 只写
  • O_RDWR 读写
  • O_APPEND 追加
  • O_CREAT 不存在即创建,已存在即打开
  • O_EXCL 不存在即创建,已存在即报错
  • O_TRUNC 不存在即创建,已存在即清空

mode: 权限模式

  • 用0XXX三位八进制数表示,由高到低分别为拥有者,同组用户,其他
  • 读4,写2,执行1,加法组合

返回文件描述符(整数)

需包含unistd.h头文件

1
int close(int fd);

fd:处于打开状态的文件描述符

权限掩码:

1
umask

影响其他用户权限,例0002,表示禁止写

修改

1
2
3
chmod o+w open.txt //对文件修改权限

umask 0022 //直接修改权限掩码

文件的内核结构

文件在磁盘中以i节点+数据块形式存储
打开文件,内核会维护两个特殊的结构体,v节点包含v节点,i节点和其他信息;另一个结构体为文件表项,成员有状态标志,读写位置,v节点指针;存储到内存中
每次打开v节点只有一个,结构表项可多个

内核维护程序的一个进程表(进程描述符,进程控制块)
其中有个成员是文件描述符表,为数组,存放指向文件表项的指针,而指针的位置(数组下标)即文件描述符fd

文件描述符是当前未被使用的最小描述符,其中0,1,2默认为标准输入/输出/错误(STDIN/STDOUT/STDERR + _FILENO)
perror输出是到标准错误,不经过缓冲区,直接到屏幕

stdin,stdout,stderr为FILE*类型的指针,指向0,1,2;前者是调用标准c库的时候,后者是系统调用

文件读写

包含 unistd.h

1
2
ssize_t write(int fd, void const* buf, size_t count);
ssize_t read(int fd, void* buf, size_t count);

buf:内存缓冲区
count:字节数

当以wq保存的时,会多一个换行符

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
//读取文件
#include<stdio.h>
#include<unistd.h>
#include<fcntl.h>

int main(void){
//打开文件shared.txt
int fd = open("./shared.txt",O_RDONLY);
if(fd == -1){
perror("open");
return -1;
}
//读取文件
char buf[32] = {};
ssize_t size = read(fd,buf,sizeof(buf)-1);
if(size == -1){
perror("read");
return -1;
}
printf("%s\n",buf);
printf("实际读取到%ld个字节\n",size);
//关闭文件
close(fd);
return 0;
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
//向文件中写入数据
#include<stdio.h>
#include<string.h>
#include<fcntl.h>//open
#include<unistd.h>// close write

int main(void){
//打开shared.txt文件
int fd = open("./shared.txt",O_CREAT | O_TRUNC | O_WRONLY,0664);
if(fd == -1){
perror("open");
return -1;
}
//向文件中写入数据
char* buf = "铁锅炖大鹅";
ssize_t size = write(fd,buf,strlen(buf));
if(size == -1){
perror("write");
return -1;
}
printf("实际向文件中写入%ld个字节\n",size);
//关闭文件
close(fd);
return 0;
}

顺序读写和随机读写,文件描述符的复制,访问测试,修改文件大小

顺序读写和随机读写

更改读写位置
包含unistd.h

1
off_t lseek(int fd, off_t offset, int whence);

offset:读写位置偏移量
whence:偏移起点

  • SEEK_SET 文件头(首字节开始)
  • SEEK_CUR 当前位置(最后读写字节下一个字节开始)
  • SEEK_END 文件尾(最后一个字节的下一个字节开始)

返回调整后的位置

超过原本字符串大小后增加内容会出现文件空洞“0”

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
#include<stdio.h>
#include<string.h>
#include<unistd.h>// lseek write read close
#include<fcntl.h>// open

int main(void){
//打开文件
int fd = open("./lseek.txt",O_WRONLY | O_CREAT | O_TRUNC,0664);
if(fd == -1){
perror("open");
return -1;
}
//写入数据 hello world!
char* buf = "hello world!";
if(write(fd,buf,strlen(buf)) == -1){
perror("write");
return -1;
}
off_t len = lseek(fd,0,SEEK_CUR);
printf("此时的读写位置是%ld\n",len);
//调整文件读写位置
if(lseek(fd,-6,SEEK_END) == -1){
perror("lseek");
return -1;
}
//再次写入数据
buf = "linux!";
if(write(fd,buf,strlen(buf)) == -1){
perror("write");
return -1;
}
//再次调整文件读写位置
if(lseek(fd,8,SEEK_END) == -1){
perror("lseek");
return -1;
}
//第三次写入
buf = "感冒真难受";
if(write(fd,buf,strlen(buf)) == -1){
perror("write");
return -1;
}


//关闭文件
close(fd);
return 0;
}

文件描述符的复制

需包含unistd.h

1
int dup(int oldfd);

oldfd:文件描述符
返回复制的新的文件描述符
两者都指向同一个文件表项

1
int dup2(int oldfd, int newfd);

返回newfd,若被占用,则先关闭后复制

访问测试

包含unistd.h
判断是否有访问权限

1
int access(char const* pathname, int mode);

mode:

  • R_OK 读
  • W_OK 写
  • F_OK 存在
  • X_OK 执行

成功返回0, 失败-1

修改文件大小

包含unistd.h

1
2
int truncate(char const* path, offset_t length);
int ftruncate(int fd,off_set length);

从尾部开始调整大小

文件锁,文件元数据,内存映射文件

文件锁

包含fcntl.h
加解锁

1
int fcntl(int fd, int F_SETLK/F_SETLKW, struct flock* lock);

第二个参数前者为非阻塞模式加锁,后者为阻塞模式加锁
lock 对文件要加的锁

1
2
3
4
5
6
7
struct flock{
short l_type; //锁类型 F_RDLCK/F_WRLCK/F_UNLCK
short l_whence; //锁区偏移起点 SEEK_SET/SEEK_CUR/SEEK_END
off_t l_start; //锁区偏移字节数
off_t l_len; //锁区字节数,给0表示一直锁到文件尾
pid_t l_pid; //加锁进程的PID,-1表示自动设置
};

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
//文件锁
#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<fcntl.h>
#include<errno.h>
int main(int argc,char* argv[]){
// ./a.out hello
//打开文件
int fd = open("./shared.txt",O_WRONLY | O_CREAT | O_APPEND,0664);
if(fd == -1){
perror("open");
return -1;
}
//阻塞加锁,
struct flock lock;
lock.l_type = F_WRLCK;//写锁
lock.l_whence = SEEK_SET;
lock.l_start = 0;
lock.l_len = 0;//锁到文件尾
lock.l_pid = -1;
/*if(fcntl(fd,F_SETLKW,&lock) == -1){
perror("fcntl");
return -1;
}*/
//非阻塞方式加锁
while(fcntl(fd,F_SETLK,&lock) == -1){
if(errno == EACCES || errno == EAGAIN){
printf("文件被锁定,干点别的去...\n");
sleep(1);
}else{
perror("fcntl");
return -1;
}
}
//写入数据 argv[1] --> "hello"
for(int i = 0;i < strlen(argv[1]);i++){
if(write(fd,&argv[1][i],sizeof(argv[1][i])) == -1){
perror("write");
return -1;
}
sleep(1);
}

//解锁
struct flock unlock;
unlock.l_type = F_UNLCK;//解锁
unlock.l_whence = SEEK_SET;
unlock.l_start = 0;
unlock.l_len = 0;//锁到文件尾
unlock.l_pid = -1;
if(fcntl(fd,F_SETLKW,&unlock) == -1){
perror("fcntl");
return -1;
}

//关闭文件
close(fd);
return 0;
}

文件锁的内核结构

以链表形式组织锁,在v节点后跟锁节点,锁节点后再接下一个锁节点。。

文件的元数据

包含sys/stat.h
从i节点中提取文件的元数据,即文件的属性信息

1
2
3
int stat(char const* path, struct stat* buf);
int fstat(int fd, struct stat* buf);
int lstat(char const* path, struct stat* buf);//不跟踪符号链接

buf:输出型参数,存放提取的元数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct  stat{
dev_t st_dev; //设备ID
ino_t st_ino; //i节点号
mode_t st_mode; //文件的类型和权限
nlink_t st_nlink; //硬链接数
uid_t st_uid; //拥有者用户ID
gid_t st_gid; //拥有者组ID
dev_t st_rdev; //特殊设备ID
off_t st_size; //总字节数
blksize_t st_blksize; //I/O块字节数
blkcnt_t st_blocks; //存储块数
time_t st_atime; //最后访问时间
time_t st_mtime; //最后修改时间
time_t st_ctime; //最后状态改变时间
}

32位不同比特位的排列组合区分功能
辅助分析文件类型的使用宏

1
2
3
4
5
6
7
S_ISREG() //是否普通文件
S_ISDIR() //是否目录
S_ISSOCK() //本地套接字
S_ISCHR() //字符设备
S_ISBLK() //块设备
S_ISLNK() //符号链接
S_ISFIFO() //有名管道

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
//获取文件的元数据
#include<stdio.h>
#include<string.h>
#include<sys/stat.h>
#include<time.h>

//转类型和权限
// hello.txt --> s ---> s.st_mode --> -rw-rw-r--
char* mtos(mode_t m){
static char s[11];
if(S_ISDIR(m)){
strcpy(s,"d");
}else
if(S_ISSOCK(m)){
strcpy(s,"s");
}else
if(S_ISCHR(m)){
strcpy(s,"c");
}else
if(S_ISBLK(m)){
strcpy(s,"b");
}else
if(S_ISLNK(m)){
strcpy(s,"l");
}else
if(S_ISFIFO(m)){
strcpy(s,"p");
}else{
strcpy(s,"-");
}

strcat(s,m & S_IRUSR ? "r" : "-");
strcat(s,m & S_IWUSR ? "w" : "-");
strcat(s,m & S_IXUSR ? "x" : "-");
strcat(s,m & S_IRGRP ? "r" : "-");
strcat(s,m & S_IWGRP ? "w" : "-");
strcat(s,m & S_IXGRP ? "x" : "-");
strcat(s,m & S_IROTH ? "r" : "-");
strcat(s,m & S_IWOTH ? "w" : "-");
strcat(s,m & S_IXOTH ? "x" : "-");
return s;
}
//转换时间 time_t ---> 年月日时分表
char* ttos(time_t t){
static char time[20];
struct tm* l = localtime(&t);
sprintf(time,"%04d-%02d-%02d %02d:%02d:%02d",
l->tm_year + 1900,l->tm_mon + 1,l->tm_mday,l->tm_hour,l->tm_min,l->tm_sec);
return time;
}
int main(int argc,char* argv[]){
//./a.out hello.txt
if(argc < 2){
fprintf(stderr,"用法:%s : <文件名>\n",argv[0]);
return -1;
}
//获取文件的元数据
// argv[1] ---> "hello.txt"
struct stat s;//用来输出文件的元数据信息
if(stat(argv[1],&s) == -1){
perror("stat");
return -1;
}
//输出获取到的元数据信息
printf(" 设备ID:%lu\n",s.st_dev);
printf(" i节点号:%ld\n",s.st_ino);
printf(" 类型和权限:%s\n",mtos(s.st_mode));
printf(" 硬连接数:%lu\n",s.st_nlink);
printf(" 用户ID:%u\n",s.st_uid);
printf(" 组ID:%u\n",s.st_gid);
printf(" 特殊设备ID:%lu\n",s.st_rdev);
printf(" 总字节数:%ld\n",s.st_size);
printf(" IO块字节数:%ld\n",s.st_size);
printf(" 存储块数:%ld\n",s.st_blksize);
printf(" 最后访问时间:%s\n",ttos(s.st_atime));
printf(" 最后修改时间:%s\n",ttos(s.st_mtime));
printf(" 最后改变时间:%s\n",ttos(s.st_ctime));
return 0;
}

内存映射文件

实现不同进程间通信
比read,write快

1
2
void* mmap(void* start, size_t length, int prot, int flag, int fd, off_t offset);
int munmap(void* start, size_t length);

具体见3
代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
//内存映射文件
#include<stdio.h>
#include<string.h>
#include<sys/mman.h>
#include<fcntl.h>
#include<unistd.h>

int main(void){
//打开文件
int fd = open("./fmap.txt",O_RDWR | O_CREAT | O_TRUNC,0664);
if(fd == -1){
perror("open");
return -1;
}
//修改文件大小
if(ftruncate(fd,300) == -1){
perror("ftruncate");
return -1;
}
//建立映射
char* start = mmap(NULL,4096,PROT_READ | PROT_WRITE,MAP_SHARED,fd,0);
if(start == MAP_FAILED){
perror("mmap");
return -1;
}
//操作文件 char buf[32]
strcpy(start,"面包牛奶火腿肠");
printf("%s\n",start);
//解除映射
if(munmap(start,4096) == -1){
perror("munmap");
return -1;
}
//关闭文件
close(fd);
return 0;
}

进程,父子进程

进程

正在执行的程序,程序执行的过程

进程标识:
PID一个时刻唯一,可重用,延时重用(所有PID用完再回过头重用)

0号进程:调度进程/交换进程,系统内核一部分,所有进程的根进程
1号进程:init进程,由0号进程创建,读写系统初始化文件,引导系统到一个特定状态,永不终止
父进程PID即子进程的PPID

1
pstree //列出所有进程的树状图

相关函数:
包含unistd.h

1
2
3
4
5
6
pid_t getpid(void);
pid_t getppid(void);
uid_t getuid(void); //实际用户ID
gid_t getgid(void); //实际组ID
uid_t geteuid(void); //有效用户ID
gid_t getegid(void); //有效组ID

创建进程:
包含unistd.h

1
pid_t fork(void); //子进程一旦创建成功,fork后的代码,父进程和子进程各执行一遍

给父进程返回子进程PID,子进程返回0;可以此区分
子进程是父进程的不完全副本
复制除代码区之外的数据和文件描述符表

区分父子进程代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//创建子进程
#include<stdio.h>
#include<unistd.h>//fork

int main(void){
printf("%d进程:我是父进程,我要创建子进程了\n",getpid());
//子进程一旦创建成功,fork之后的代码,父进程会执行一遍,子进程也会执行一遍
pid_t a = fork();
if(a == -1){
perror("fork");
return -1;
}
//子进程代码
if(a == 0){
printf("%d进程:这是子进程执行的代码\n",getpid());
return 0;
}
//父进程代码
printf("%d进程:这是父进程执行的代码\n",getpid());
return 0;
}

父子进程操作同一文件代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
//子进程复制父进程的文件描述符表
#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<fcntl.h>

int main(void){
//父进程打开文件
int fd = open("./ftab.txt",O_WRONLY | O_CREAT | O_TRUNC,0664);
if(fd == -1){
perror("open");
return -1;
}
//向文件中写入数据 hello world!
char* buf = "hello world!";
if(write(fd,buf,strlen(buf)) == -1){
perror("write");
return -1;
}
//父进程创建子进程
pid_t pid = fork();
if(pid == -1){
perror("fork");
return -1;
}
//子进程代码调整文件读写位置
if(pid == 0){
if(lseek(fd,-6,SEEK_END) == -1){
perror("lseek");
return -1;
}
//关闭文件描述符
close(fd);
return 0;
}
//父进程代码再次写入数据 linux!
sleep(1);
buf = "linux!";
if(write(fd,buf,strlen(buf)) == -1){
perror("write");
return -1;
}
//关闭文件描述符
close(fd);
return 0;
}

进程的终止,回收子进程,wait,waitpid

进程终止

正常终止:

  1. return 值(退出码)给main函数,父进程接到返回值,只能拿到最低数位字节

  2. void exit(int status);需包含stdlib.h头文件,status为退出码,习惯上将EXIT_SUCCESS和EXIT_FAILURE作为成功和失败,多数系统定义为0和-1

    终止进程前还会做:

    • a 调用实现通过atexit或on_exit函数注册的退出处理函数
    • b 冲刷并关闭所有仍处于打开状态的标准I/O流
    • c 删除所有通过tmpfile函数创建的临时文件
    • d _exit(status);

    注册处理函数:
    需stdlib.h,注册顺序和调用顺序相反

    1
    int atexit(void (*function)(void));

    参数为函数指针,指向退出处理函数
    1
    int on_exit(void (*function)(int, void*), void* arg);

    参数第一个为函数指针,指向函数的其中的第一个参数是传递给exit函数的status参数或main中return返回值,第二个参数是来自on_exit的arg参数
    arg 泛型指针,传递给调用函数的第二个参数

    代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    //进程的终止
    #include<stdio.h>
    #include<stdlib.h>
    #include<unistd.h>
    //退出处理函数(遗言函数)
    void doit(void){
    printf("遗言也没啥可说的...\n");
    }
    //退出处理函数
    void doit2(int status,void* arg){
    //status 进程的退出码
    printf("status = %d\n",status);
    printf(" arg = %s\n",(char*)arg);
    }

    int hahaha(void){
    printf("哈哈哈哈\n");
    //exit(0);
    //_exit(10);
    _Exit(0);
    return 10;
    }

    int main(void){
    //注册退出处理函数
    atexit(doit);//传话的,将函数地址给内核
    on_exit(doit2,"拜拜~~");
    printf("函数返回%d\n",hahaha());

    return 0;
    }

  3. 调用_exit或_Exit函数(使程序真正结束)
    需包含unistd.h

    1
    void _exit(int status);

    需包含stdlib.h

    1
    void _Exit(int status);

    _exit收尾工作:

    • a 关闭处于打开状态的文件描述符
    • b 将调用进程的所有子进程托付给init进程收养
    • c 向调用进程的父进程发送SIGCHLD(17)信号
    • d 令调用进程终止运行,将status的低八位作为退出码保存在其终止状态中

异常终止

  1. 进程执行危险操作,系统故障
  2. 人为触发信号
  3. 向进程自己发送信号
    1
    2
    #include<stdlib.h>
    void abort(void);

回收子进程

1
2
#include <sys/wait.h>
pid_t wait(int* status);

等待回收任意子进程
参数是用于输出子进程的终止状态,可置NULL来不存终止状态
返回回收的子进程PID

1
2
3
4
5
6
7
8
#include<sys/wait.h>

WIFEXITED(status) //真:正常终止 ;假:异常终止;
WEXITSTATUS(status) //进程退出码
WTERMSIG(status) //终止进程的信号
WIFSIGNALED(status) //真:异常终止;假:正常终止;
WTERMSIG(status) //终止进程的信号
WEXITSTATUS(status) //进程退出码

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
//回收多个子进程
#include<stdio.h>
#include<unistd.h>
#include<sys/wait.h>
#include<errno.h>

int main(void){
//创建多个子进程
for(int i = 0;i < 5;i++){
pid_t pid = fork();
if(pid == -1){
perror("fork");
return -1;
}
if(pid == 0){
printf("%d进程:我是子进程\n",getpid());
sleep(i+1);
return i+1;
}
}

for(;;){
int s;//用来输出子进程的终止状态
pid_t pid = wait(&s);
if(pid == -1){
if(errno == ECHILD){
printf("没有子进程了\n");
break;
}else{
perror("wait");
return -1;
}
}
printf("%d进程:回收了%d进程的僵尸\n",getpid(),pid);
if(WIFEXITED(s)){
printf("正常终止:%d\n",WEXITSTATUS(s));
}else{
printf("异常终止:%d\n",WTERMSIG(s));
}
}


return 0;
}

另一个函数

1
pid_t waitpid(pid_t pid, int* status, int options);

参数:pid>0可指定具体的子进程PID,-1相当于wait
options:

  • 0 阻塞模式
  • WNOHANG 非阻塞模式,若子进程仍运行,返回0

返回子进程PID或0,失败-1

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
//以非阻塞方式收尸
#include<stdio.h>
#include<unistd.h>
#include<sys/wait.h>

int main(void){
//创建子进程,暂时不结束
pid_t pid = fork();
if(pid == -1){
perror("fork");
return -1;
}
//子进程代码
if(pid == 0){
printf("%d进程:我是子进程,你收不了我!\n",getpid());
sleep(10);
return 0;
}
//父进程代码
printf("%d进程:我要回收那个子进程\n",getpid());
for(;;){
pid_t childpid = waitpid(pid,NULL,WNOHANG);
if(childpid == -1){
perror("waitpid");
return -1;
}else if(childpid == 0){
printf("%d进程:子进程在运行,收不了,干点别的去\n",getpid());
sleep(1);
}else{
printf("%d进程:回收了%d进程的僵尸\n",getpid(),childpid);
break;
}
}
return 0;
}

创建新进程,system

创建新进程

  • 创建新进程,取代旧进程

    1
    2
    #include <unistd.h>
    int execl(const char* path, const char* arg, ...);

    path:可执行文件路径
    arg:命令行参数内容
    …:可变长参数,以NULL结束
    失败返回-1,成功不返回

  • 根据环境变量找

    1
    int execlp(const char* file, const char* arg, ...);

    file:文件名

  • 为新进程指定环境变量

    1
    int execle(const char* file, const char*arg, ...,char* const envp[]);

    envp:指定的新的环境变量内容

  • 传数组代替以列表形式直接传字符串

    1
    2
    3
    int execv(const char* path, char* const argv[]);
    int execvp(const char* file, char* const argv[]);
    int execve(const char* path, char* const argv[], char *const envp[]);

一般将fork和execl相结合使用

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
//fork + exec
#include<stdio.h>
#include<unistd.h>
#include<sys/wait.h>

int main(void){
//创建子进程
pid_t pid = fork();
if(pid == -1){
perror("fork");
return -1;
}
//子进程变身
if(pid == 0){
if(execl("./new","./new","123","hello",NULL) == -1){
perror("execl");
return -1;
}
//return 0;
}
//父进程收尸
int s;//用来输出子进程的终止状态
if(waitpid(pid,&s,0) == -1){
perror("waitpid");
return -1;
}
if(WIFSIGNALED(s)){
printf("异常终止:%d\n",WTERMSIG(s));
}else{
printf("正常终止:%d\n",WEXITSTATUS(s));
}

pid = fork();
if(pid == -1){
perror("fork");
return -1;
}
if(pid == 0){
if(execl("/bin/ls","ls","-l",NULL) == -1){
perror("execl");
return -1;
}
}
if(waitpid(pid,&s,0) == -1){
perror("waipid");
return -1;
}
if(WIFEXITED(s)){
printf("正常终止:%d\n",WEXITSTATUS(s));
}else{
printf("异常终止:%d\n",WTERMSIG(s));
}
return 0;
}

system

执行终端命令

1
2
#include<stdlib.h>
int system(const char* command);

参数传终端命令
返回command终止状态,失败返回-1

vfork

与父进程公用一份数据,子进程用时父进程挂起,一般用于搭配exec(免去复制)

信号基础,信号处理,太平间信号,信号发送

信号类似于硬件上的中断
64信号,32,33信号没有,前31不可靠信号

信号处理

1
2
3
#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

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
//信号处理
#include<stdio.h>
#include<unistd.h>// getpid()
#include<signal.h>// signal()

typedef void (*sighandler_t)(int);

//信号处理函数,内核负责对该函数的调用,
//并传信号的编号,给函数
void sigfun(int signum){
printf("%d进程:捕获到%d号信号\n",getpid(),signum);
printf("晚上吃多了,有点撑....\n");
}

int main(void){
printf("%d进程:我要开始死循环了\n",getpid());
//对2号信号进行忽略处理
sighandler_t ret = signal(SIGINT,SIG_IGN);
if(ret == SIG_ERR){
perror("signal");
return -1;
}
printf("ret = %p\n",ret);
//对2号信号进行捕获处理
ret = signal(SIGINT,sigfun);
if(ret == SIG_ERR){
perror("signal");
return -1;
}
printf("ret = %p\n",ret);
//对2号信号进行默认操作
ret = signal(SIGINT,SIG_DFL);
if(ret == SIG_ERR){
perror("signal");
return -1;
}
printf("ret = %p\n",ret);
printf("sigfun = %p\n",ret);
for(;;);
return 0;
}

太平间信号

17号信号,子进程死亡向父进程发送的信号。用于收尸。
在信号处理函数执行期间,若再有相同的信号多次到来,只保留一个,其余丢弃。

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
//太平间信号
#include<stdio.h>
#include<unistd.h>
#include<signal.h>
#include<sys/wait.h>
#include<errno.h>
//信号处理函数,负责对子进程收尸
void sigchild(int signum){
printf("%d进程:捕获到%d号信号\n",getpid(),signum);
sleep(2);//假装信号处理函数执行很耗时
for(;;){
pid_t pid = waitpid(-1,NULL,WNOHANG);
if(pid == -1){
if(errno == ECHILD){
printf("%d进程:没有子进程\n",getpid());
break;
}else{
perror("waitpid");
return ;
}
}else if(pid == 0){
printf("%d进程:子进程在运行,没法收\n",getpid());
break;
}else{
printf("%d进程:回收了%d进程的僵尸\n",getpid(),pid);
}
}
/*pid_t pid = wait(NULL);
if(pid == -1){
perror("wait");
return ;
}
printf("%d进程;回收了%d进程的僵尸\n",getpid(),pid);*/

}

int main(void){
//对17号信号进行捕获处理
if(signal(SIGCHLD,sigchild) == SIG_ERR){
perror("signal");
return -1;
}
//创建多个子进程
for(int i = 0;i < 5;i++){
pid_t pid = fork();
if(pid == -1){
perror("fork");
return -1;
}
if(pid == 0){
printf("%d进程;我是子进程\n",getpid());
//sleep(i+1);
sleep(1);
return 0;
}
}
//父进程忙自己的事
for(;;);

return 0;
}

发送信号

1
kill [-信号] PID

不指定信号,缺省发送15终止进程信号
例:

1
2
3
kill -KILL -1 //给所有进程发送9号信号(SIGKILL),不能捕获,忽略的终止信号
kill -9 1234 //1234为进程PID
kill -SIGKILL 1234 5678

函数:

1
2
#include <signal.h>
int kill(pid_t pid, int signum);

pid:

  • -1 所有进程
  • 0 特定进程

signum:信号编号,取0检查pid进程是否存在,不存在返回-1,errno为ESRCH
成功返回0(至少发送出一个信号),失败-1

1
int raise(int signum);

向调用进程自己发送信号

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
//kill 函数
#include<stdio.h>
#include<unistd.h>
#include<signal.h>
#include<errno.h>
#include<sys/wait.h>
//信号处理函数
void sigfun(int signum){
printf("%d进程:捕获到%d号信号\n",getpid(),signum);
}

int main(void){
//创建子进程
pid_t pid = fork();
if(pid == -1){
perror("fork");
return -1;
}
//子进程
if(pid == 0){
//对2号信号进行捕获处理
/*if(signal(SIGINT,sigfun) == SIG_ERR){
perror("signal");
return -1;
}*/
printf("%d进程:我是子进程\n",getpid());
for(;;);
return 0;
}
//父进程代码,发送2号信号给子进程
getchar();
//杀死子进程
if(kill(pid,SIGINT) == -1){
perror("kill");
return -1;
}

getchar();
//判断子进程是否存在
if(kill(pid,0) == -1){
if(errno == ESRCH){
printf("子进程不存在\n");
}else{
perror("kill");
return -1;
}
}else{
printf("子进程存在\n");
}
//收尸
getchar();
if(wait(NULL) == -1){
perror("wait");
return -1;
}
//判断子进程是否存在
if(kill(pid,0) == -1){
if(errno == ESRCH){
printf("子进程不存在\n");
}else{
perror("kill");
return -1;
}
}else{
printf("子进程存在\n");
}

return 0;
}

暂停,睡眠与闹钟,信号集,信号屏蔽

暂停

睡眠

1
2
#include <unistd.h>
unsigned int sleep(unsigned int seconds);

有限睡眠
返回0或剩余秒数

1
int usleep(useconds_t usec);

成功返回0,失败-1

暂停

1
2
#include<unsitd.h>
int pause(void);

无限睡眠
失败返回-1,信号处理函数结束后返回

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//pause函数演示
#include<stdio.h>
#include<unistd.h>
#include<signal.h>
//信号处理函数
void sigfun(int signum){
printf("%d号信号处理开始\n",signum);
sleep(3);
printf("%d号信号处理结束\n",signum);
}
int main(void){
//捕获2号信号
if(signal(SIGINT,sigfun) == SIG_ERR){
perror("signal");
return -1;
}
printf("%d进程:我要睡觉了\n",getpid());
int ret = pause();
printf("%d进程:pause函数返回%d\n",getpid(),ret);
return 0;
}

信号集

本质:一块128字节连续的存储区
内核用sigset_t类型表示信号集
看1024个比特位的值表明是否有对应信号(像操控寄存器)
填满信号集

1
2
#include <signal.h>
int sigfillset(sigset_t *sigset);

填满信号集(置1),32,33信号没有,故为0

清空信号集

1
int sigemptyset(sigset* sigset)

加入信号

1
int sigaddset(sigset_t* sigset, int signum);

sigset:信号集
signum:信号编号

删除信号

1
int sigdelset(sigset_t* sigset, int signum);

判断是否有信号

1
int sigismember(const sigset_t* sigset, int signum);

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
//信号集操作
#include<stdio.h>
#include<signal.h>
#include<unistd.h>
//实现一个字节的打印
void printb(char byte){
for(int i = 0;i < 8;i++){
printf("%d",byte & 1 << 7 - i ? 1 : 0);
}
printf(" ");//空格
}
//重复,实现一块存储区所有字节的打印
void printm(void* buf,size_t size){
for(int i = 0;i < size;i++){
//倒叙输出
printb(((char*)buf)[size-1-i]);
//8个字节换一行
if((i+1) % 8 == 0){
printf("\n");
}
}
}
int main(void){
//信号集变量
sigset_t s;
printf("填满信号集\n");
sigfillset(&s);
printm(&s,sizeof(s));
printf("清空信号集\n");
sigemptyset(&s);
printm(&s,sizeof(s));
printf("添加2号信号\n");
sigaddset(&s,SIGINT);
printm(&s,sizeof(s));
printf("添加3号信号\n");
sigaddset(&s,SIGQUIT);
printm(&s,sizeof(s));
/*if(sigaddset(&s,32) == -1){
perror("sigaddset");
return -1;
} */
printf("删除2号信号\n");
sigdelset(&s,SIGINT);
printm(&s,sizeof(s));
printf("信号集中%s2号信号\n",sigismember(&s,SIGINT) ? "有" : "无");
printf("信号集中%s3号信号\n",sigismember(&s,SIGQUIT) ? "有" : "无");
return 0;
}

信号屏蔽

每个进程都有一个信号掩码(信号集)
有哪个信号,哪个信号无法送达
信号处理函数期间,有哪个信号,信号掩码中就有哪个信号

不可靠信号,屏蔽解除后只接受一个
可靠信号,屏蔽解除后都接收

设置信号掩码

1
2
#include <signal.h>
int sigprocmask(int how, const sigset_t* sigset, sigset_t* oldset);

how:SIG_BLOCK:将sigset中的信号加入当前信号掩码

  • SIG_UNBLOCK:从当前信号掩码中删除sigset的信号
  • SIG_SETMASK:把sigset设置为当前信号掩码

oldset:输出型参数,输出原信号掩码

获取调用进程的未决信号集

1
2
#include <signal.h>
int sigpending(sigset_t* sigset);

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
//信号屏蔽
#include<stdio.h>
#include<unistd.h>
#include<signal.h>
//假装更新数据库
void updatedb(void){
for(int i = 0;i < 5;i++){
printf("正在更新第%d条数据.....\n",i + 1);
sleep(1);
}
}
//信号处理函数
void sigfun(int signum){
printf("捕获到%d号信号\n",signum);
}

int main(void){
int signum = /*SIGINT*/ 40;
//父进程对2号信号进行捕获处理
printf("%d进程:捕获%d号信号\n",getpid(),signum);
if(signal(signum,sigfun) == SIG_ERR){
perror("signal");
return -1;
}
//父进程屏蔽2号信号
printf("%d进程:屏蔽%d号信号\n",getpid(),signum);
sigset_t sigset;
sigemptyset(&sigset);//清空
sigaddset(&sigset,signum);//添加2号信号
sigset_t oldset;//用来输出以前的信号掩码
if(sigprocmask(SIG_SETMASK,&sigset,&oldset) == -1){
perror("sigprocmask");
return -1;
}
//父进程创建子进程
pid_t pid = fork();
if(pid == -1){
perror("fork");
return -1;
}
//子进程负责向父进程多次发送2号信号
if(pid == 0){
for(int i = 0;i < 5;i++){
printf("%d进程:给父进程发送%d号信号\n",getpid(),signum);
if(kill(getppid(),signum) == -1){
perror("kill");
return -1;
}
}
return 0;
}
//父进程开始更新数据库
updatedb();
//父进程解除对2号信号的屏蔽
printf("%d进程:解除对%d号信号的屏蔽\n",getpid(),signum);
if(sigprocmask(SIG_SETMASK,&oldset,NULL) == -1){
perror("sigprocmask");
return -1;
}

for(;;);
return 0;
}

内存壁垒,有名管道,无名管道

进程间通信的种类

内存壁垒:两进程间,有相同的虚拟地址,但没有相同的物理地址

进程间通信种类:

  • 命令行参数execl
  • 环境变量execle
  • 内存映射文件
  • 管道
  • 共享内存
  • 消息队列
  • 信号量
  • 本地套接字

管道:半双工,一页大小
有名管道:任意进程
无名管道:父子,兄弟进程

有名管道

亦称FIFO:特殊文件
在磁盘上只有i节点,没有数据块,不保存数据(0字节)
本质:内核存储区,伪装成特殊的文件供访问

1
mkfifo xxx //xxx为名字

函数:
创建有名管道

1
2
#include<sys/stat.h>
int mkfifo(char const* pathname, mode_t mode);

mode:权限模式

代码

  1. 写入

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    //写入有名管道
    #include<stdio.h>
    #include<string.h>
    #include<unistd.h>
    #include<fcntl.h>
    #include<sys/stat.h>

    int main(void){
    //创建有名管道文件
    printf("创建有名管道文件\n");
    if(mkfifo("./fifo",0664) == -1){
    perror("mkfifo");
    return -1;
    }
    //打开有名管道文件
    printf("打开有名管道文件\n");
    int fd = open("./fifo",O_WRONLY);
    if(fd == -1){
    perror("open");
    return -1;
    }
    //循环多次向文件中写入数据
    printf("发送数据\n");
    for(;;){
    //通过键盘获取数据
    char buf[64] = {};
    fgets(buf,sizeof(buf),stdin);
    //人为指定退出条件,输入!则退出循环
    if(strcmp(buf,"!\n") == 0){
    break;
    }
    //写入到文件中
    if(write(fd,buf,strlen(buf)) == -1){
    perror("write");
    return -1;
    }
    }
    //关闭有名管道文件
    printf("关闭有名管道文件\n");
    close(fd);
    //删除有名管道文件
    printf("删除有名管道文件\n");
    unlink("./fifo");
    printf("大功告成\n");
    return 0;
    }
  2. 读取

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    //读取有名管道
    #include<stdio.h>
    #include<unistd.h>
    #include<fcntl.h>

    int main(void){
    //打开有名管道文件
    printf("打开有名管道文件\n");
    int fd = open("./fifo",O_RDONLY);
    if(fd == -1){
    perror("open");
    return -1;
    }
    //读取有名管道文件
    printf("接受数据\n");
    for(;;){
    char buf[64] = {};
    //从管道中读取数据
    ssize_t size = read(fd,buf,sizeof(buf)-1);
    if(size == -1){
    perror("read");
    return -1;
    }
    if(size == 0){
    printf("对方关闭管道文件\n");
    break;
    }
    //输出
    printf("%s",buf);
    }
    //关闭有名管道文件
    printf("关闭有名管道文件\n");
    close(fd);
    printf("大功告成\n");
    return 0;
    }

无名管道

借助两个文件描述符控制写端和读端

1
int pipe(int pipefd[2]);

pipefd[2]:输出型参数,下标0存读端描述符,1存写端描述符

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
//无名管道
#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<sys/wait.h>

int main(void){
//父进程创建无名管道
printf("%d进程:创建无名管道\n",getpid());
int pipefd[2];// 用来输出无名官道的读端和写端描述符
if(pipe(pipefd) == -1){
perror("pipe");
return -1;
}
printf("pipefd[0] = %d\n",pipefd[0]);
printf("pipefd[1] = %d\n",pipefd[1]);
//父进程创建子进程
pid_t pid = fork();
if(pid == -1){
perror("fork");
return -1;
}
//子进程负责读取数据
if(pid == 0){
printf("%d进程:关闭写端\n",getpid());
close(pipefd[1]);
printf("%d进程:接受数据\n",getpid());
for(;;){
char buf[64] = {};
//通过管道的读端描述符,读取数据
ssize_t size = read(pipefd[0],buf,sizeof(buf)-1);
if(size == -1){
perror("read");
return -1;
}
if(size == 0){
printf("%d进程:写端被关闭\n",getpid());
break;
}
//显示
printf("%s",buf);
}
printf("%d进程:关不读端\n",getpid());
close(pipefd[0]);
printf("%d进程:大功告成\n",getpid());
return 0;
}
//父进程负责写入数据
printf("%d进程:关闭读端\n",getpid());
close(pipefd[0]);
printf("%d进程:发送数据\n",getpid());
for(;;){
//从键盘获取数据
char buf[64] = {};
fgets(buf,sizeof(buf),stdin);

if(strcmp(buf,"!\n") == 0){
break;
}

//通过写端描述符,向无名管道写入数据
if(write(pipefd[1],buf,strlen(buf)) == -1){
perror("write");
return -1;
}
}
printf("%d进程:关闭写端\n",getpid());
close(pipefd[1]);
//父进程收尸
if(wait(NULL) == -1){
perror("wait");
return -1;
}
printf("%d进程:已收尸\n",getpid());
printf("%d进程:大功告成\n",getpid());
return 0;
}

IPC对象,共享内存, 消息队列

IPC对象

分为共享内存,消息队列,信号量
键(key):IPC对象的外部名,整数

共享内存

创建新的或获取已有的共享内存

1
2
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);

key:键
size:字节数,按页圆整
shmflg:创建标志

  • 0:获取,不存在即失败
  • IPC_CREAT:创建,不存在即创建,已存在即获取
  • IPC_EXCL:排它,不存在即创建,已存在即失败

成功返回共享内存ID,失败-1

加载共享内存,映射地址

1
void* shmat(int shmid, void const* shmaddr, int shmflg);

shmid:共享内存ID
shmaddr:虚拟地址起始地址,NULL系统选择
shmflg:加载标志

  • 0:读写方式使用
  • SHM_RDONLY:只读方式

成功返回起始地址,失败返回(void*)-1

卸载共享内存,为共享内存计数-1

1
int shmdt(void const* shmaddr);

销毁共享内存,阻止任何其他进程对此块共享内存进行使用

1
int shmctl(int shmid, IPC_RMID, NULL);

当计数为0时,真正销毁

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
//向共享内存中写入数据
#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<sys/shm.h>

int main(void){
//合成键
printf("%d进程:合成键\n",getpid());
key_t key = ftok(".",123);
if(key == -1){
perror("ftok");
return -1;
}
//创建共享内存
printf("%d进程:创建共享内存\n",getpid());
int shmid = shmget(key,4096,IPC_CREAT | IPC_EXCL | 0664);
if(shmid == -1){
perror("shmget");
return -1;
}
//加载共享内存
printf("%d进程:加载共享内存\n",getpid());
char* start = shmat(shmid,NULL,0);
if(start == (void*)-1){
perror("shmat");
return -1;
}
//写入数据
sprintf(start,"shmid=%d,key=0x%x,pid=%d\n",shmid,key,getpid());
//卸载共享内存
printf("%d进程:卸载共享内存\n",getpid());
getchar();
if(shmdt(start) == -1){
perror("shmdt");
return -1;
}
//销毁共享内存
printf("%d进程:销毁共享内存\n",getpid());
getchar();
if(shmctl(shmid,IPC_RMID,NULL) == -1){
perror("shmctl");
return -1;
}
printf("%d进程:大功告成\n",getpid());
return 0;
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
//读取共享内存
#include<stdio.h>
#include<unistd.h>
#include<sys/shm.h>

int main(void){
//合成键
printf("%d进程:合成键\n",getpid());
key_t key = ftok(".",123);
if(key == -1){
perror("ftok");
return -1;
}
//获取共享内存
printf("%d进程:获取共享内存\n",getpid());
int shmid = shmget(key,0,0);
if(shmid == -1){
perror("shmget");
return -1;
}
//加载共享内存
printf("%d进程:加载共享内存\n",getpid());
char* start = shmat(shmid,NULL,0);
if(start == (void*)-1){
perror("start");
return -1;
}
//读取数据
getchar();
printf("%s\n",start);
//卸载共享内存
printf("%d进程:卸载共享内存\n",getpid());
getchar();
if(shmdt(start) == -1){
perror("shmdt");
return -1;
}
printf("%d进程:大功告成\n",getpid());
return 0;
}

消息队列

向链表插入/摘取节点

可发送消息字节数上限两页(8192)
单条队列总字节数上限16384(16K)
全系统总消息队列数上限16
全系统消息总字节数上限262144(256k)

创建新的或获取已有的消息队列

1
2
#include <sys/msg.h>
int msgget(key_t key, int msgflg);

发送消息

1
int msgsnd(int msgid, void const* msgp, size_t msgsz, int msgflg);

msgid:消息队列的ID
msgp:指向一个包含消息类型(4字节)和消息数据的内存块
msgsz:期望发送的消息数据
msgflg:发送标志,一般为0;若包含IPC_NOWAIT,为非阻塞
成功返回0,失败-1

接收消息

1
int msgrcv(int msgid, void* msgp, size_t msgsz, long msgtyp, int msgflg);

msgtyp:0为不指定类型,>0表示取特定类型,<0表示小于其绝对值的类型
msgflg:给MSG_EXCEPT,接收除了特定类型的;以及IPC_NOWAIT非阻塞

若数据长度大于msgsz,且msgflg包含MSG_NOERROR,则只截取前msgsz字节返回,剩余丢弃;不包含则返回-1,errno为E2BIG

销毁消息队列

1
int msgctl(int msgid, IPC_RMID, NULL);

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
//向消息队列写入数据
#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<sys/msg.h>

int main(void){
//合成键
printf("%d进程:合成键\n",getpid());
key_t key = ftok(".",123);
if(key == -1){
perror("ftok");
return -1;
}
//创建消息队列
printf("%d进程:创建消息队列\n",getpid());
int msgid = msgget(key,IPC_CREAT | IPC_EXCL | 0664);
if(msgid == -1){
perror("msgget");
return -1;
}
//发送数据
printf("%d进程:发送消息\n",getpid());
for(;;){
struct {
long type;//消息类型
char data[64];//数据内容
} buf = {1234,""};//"" 空串
//通过键盘获取数据
fgets(buf.data,sizeof(buf.data),stdin);

if(strcmp(buf.data,"!\n") == 0){
break;
}

//发送消息
if(msgsnd(msgid,&buf,strlen(buf.data),0) == -1){
perror("msgsnd");
return -1;
}
}
//销毁消息队列
printf("%d进程:销毁消息队列\n",getpid());
if(msgctl(msgid,IPC_RMID,NULL) == -1){
perror("msgctl");
return -1;
}
printf("%d进程:大功告成\n",getpid());
return 0;
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
//读取数据
#include<stdio.h>
#include<unistd.h>
#include<sys/msg.h>
#include<errno.h>
int main(void){
//合成键
printf("%d进程:合成键\n",getpid());
key_t key = ftok(".",123);
if(key == -1){
perror("ftok");
return -1;
}
//获取消息队列
printf("%d进程:获取消息队列\n",getpid());
int msgid = msgget(key,0);
if(msgid == -1){
perror("msgget");
return -1;
}
//接受消息
printf("%d进程:接受数据\n",getpid());
for(;;){
struct {
long type;//消息类型
char data[64];//消息内容
} buf = {};
ssize_t size = msgrcv(msgid,&buf,sizeof(buf.data)-1,1234,0);
if(size == -1){
if(errno == EIDRM){
printf("%d进程:消息队列被销毁\n",getpid());
break;
}else{
perror("msgrcv");
return -1;
}
}
printf("%ld>>%s",buf.type,buf.data);
}
printf("%d进程:大功告成\n",getpid());
return 0;
}

其他:

手动销毁消息队列

1
ipcrm -q xxx //xxx为消息队列id

网络基础,IP地址,套接字,字节序转换

IP地址

IP协议提供的逻辑地址

计算机内部,32为无符号整数表示
人们习惯用点分十进制字符串表示

192.168.182.48
网络地址 + 本地地址
192.168.182.0 (网络地址) 0.0.0.48(本地地址)

IP地址分级(32为无符号整数形式):

地址分级 网络地址 本地地址
A级地址: 0为首, 8为网络地址 + 24为本地地址
B 10 16 16
C 110 24 8
D 1110为首的 32位多播地址

IP地址分为公网IP和私网IP

子网掩码可快速区定IP地址的网络地址和本地地址
网络地址 = IP地址 & 子网掩码
本地地址 = IP地址 & ~子网掩码

192.168.222.128/24 (24指子网掩码 255.255.255.0)

套接字

代表计算机网络通讯能力,相当于一个文件描述符,网络就是一种特殊文件

端口号 65536个(16位无符号整数)
对应相应的应用程序,1024以下被其他程序占用

字节序转换

小端/本地字节序:数据低位存放在低地址
大端/网络字节序:数据低位存放在高地址

网络协议栈大端,主机小端

TCP协议,函数,编程模型

函数

创建套接字

1
2
#include <sys/socket.h>
int socket(int domain, int type, int protocol);

domain:通信域,协议族

  • PF_LOCAL/PF_UNIX 本地套接字,进程间通信
  • PF_INET 基于IPv4的网络通信
  • PF_INET6 基于IPv6的网络通信
  • PF_PACKET 基于底层包的网络通信

type:套接字类型

  • SOCK_STREAM 流式套接字,基于TCP协议
  • SOCK_DGRAM 数据报套接字,基于UDP协议
  • SOCK_RAW 原始套接字,工作在传输层以下
  • protocol:特殊协议,对于前两种套接字而言,只能取0

成功返回表示套接字对象的文件描述符,失败-1

基本地址结构,本身没有实际意义,仅用于泛型化参数

1
2
3
4
struct sockaddr{
sa_family_t sa_family;//地址族
char sa_data[14];//地址值
};

本地地址结构,用于AF_LOCAL/AF_UNIX的本地通信

1
2
3
4
struct sockaddr_un {
sa_family_t sun_family;//地址族(AF_LOCAL/AF_UNIX)
char sun_path[];//本地套接字文件的路径
};

网络地址结构,用于AF_INET的IPv4网络通信

1
2
3
4
5
struct sockaddr_in {
sa_family_t sin_family;//地址族(AF_INET)
in_port_t sin_port; //端口号(0 ~ 65535), unsigned short
struct in_addr sin_addr;//IP地址
};

网络地址结构,用于AF_INET的IPv4网络通信

1
2
3
4
5
struct in_addr {
in_addr_t s_addr;
};
typedef uint16_t in_port_t;//无符号16位整数
typedef uint32_t in_addr_t;//无符号32位整数

端口号一般选1024以上的,0到1024已被系统和一些网络服务占据

将套接字和本机的地址结构绑定在一起

1
2
#include <sys/socket.h>
int bind(int sockfd, struct sockaddr const* addr, socklen_t addrlen);

sockfd:套接字描述符
addr:自己的地址结构
addrlen:地址结构的字节数
成功返回0,失败-1

将套接字和对方的地址结构连接到一起

1
int connect(int sockfd, struct sockaddr const* addr, socklen_t addrlen);

addr:对方的地址结构
成功返回0,失败-1

TCP函数

1
2
3
4
5
6
7
uint32_t htonl(uint32_t hostlong);//长整形主机字节序到网络字节序
ntohl netllong //长整型网络字节序到主机字节序
uint16_t htons(uint16_t hostshort);//短整型主机字节序到网络字节序
ntohs netshoort //网转主
in_addr_t inet_addr(char const* ip);//点分十进制字符串地址转网络字节序整数地址
int inet_aton(char const* ip, struct in_addr* nip);//同上
char* inet_ntoa(struct in_addr nip);//网络字节序整数地址转点分十进制字符串地址

启动侦听

1
2
#include <sys/socket.h>
int listen(int sockfd, int backlog);

backlog:未决连接请求队列的最大长度,一般大于1024
成功返回0,失败-1

等待并接受连接请求,阻塞

1
int accept(int sockfd, struct sockaddr*addr, socklen_t* addrlen);

sockfd:侦听套接字描述符
addr:输出连接请求发起方的地址信息
addrlen:输出连接请求发起方的地址信息字节数
成功返回可用于后续通信的连接套接字描述符,失败-1

接收数据

1
ssize_t recv(int sockfd, void* buf, size_t count, int flags);

flags:取0与read函数等价,也可取以下值:

  • MSG_DONTWAIT:非阻塞接受
  • MSG_OOB:接收带外数据
  • MSG_WAITALL:等待所有数据,即不接收到count字节就不返回

成功返回实际接受到的字节数,失败-1

发送数据

1
ssize_t send(int sockfd, void const* buf, size_t count, int flags);

flags:取0与write等价

  • MSG_DONTWAIT
  • MSG_OOB
  • MSG_DONTROUTE:不查路由表,直接在本地网络中寻找目的主机

成功返回实际发送的字节数,失败-1

代码

服务端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
//基于TCP协议的客户端和服务器
#include<stdio.h>
#include<string.h>
#include<ctype.h> // toupper
#include<unistd.h>//提供read,write等函数
#include<sys/socket.h>
#include<sys/types.h>
#include<arpa/inet.h>

int main(void){
//创建套接字,买了个手机
printf("服务器:创建套接字\n");
int sockfd = socket(AF_INET,SOCK_STREAM,0);
if(sockfd == -1){
perror("socket");
return -1;
}
//组织地址结构 办张电话卡
printf("服务器:组织地址结构\n");
struct sockaddr_in ser ;
ser.sin_family = AF_INET;
ser.sin_port = htons(8888);//字节序转换
//ser.sin_addr.s_addr = inet_addr("192.168.222.128");//串-->整数
ser.sin_addr.s_addr = INADDR_ANY;//接受任意IP地址到来的数据
//用INADDR_ANY 的方式可以接受所有IP地址,而不是某一个(一台电脑可能有多个网卡,即存在多个IP地址)
//绑定套接字和地址结构,手机卡插入手机
printf("服务器:绑定套接字和地址结构\n");
if(bind(sockfd,(struct sockaddr*)&ser,sizeof(ser)) == -1){
perror("bind");
return -1;
}
//启动侦听,从主动变被动,可接受连接请求
printf("服务器:启动侦听\n");
if(listen(sockfd,1024) == -1){
perror("listen");
return -1;
}
//等待连接,等待客户端的连接请求到来,接受连接请求,完成三次握手
printf("服务器:等待客户端的连接请求到来\n");
struct sockaddr_in cli;//用来输出客户端的地址结构
socklen_t len = sizeof(cli);//输出地址结构的大小
int conn = accept(sockfd,(struct sockaddr*)&cli,&len);
if(conn == -1){
perror("accept");
return -1;
}
//业务处理
printf("服务器:业务处理\n");
for(;;){
//接客户端发来的小写的串
char buf[128] = {};
ssize_t size = read(conn,buf,sizeof(buf)-1);
if(size == -1){
perror("read");
return -1;
}
if(size == 0){
//客户端关闭套接字
break;
}
//转成大写
for(int i = 0;i < strlen(buf);i++){
buf[i] = toupper(buf[i]);
}
//将大写的串发会给客户端
if(write(conn,buf,strlen(buf)) == -1){
perror("write");
return -1;
}
}
//关闭套接字
printf("服务器:关闭套接字\n");
close(conn);
close(sockfd);
return 0;
}

客户端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
//基于TCP的客户端
#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<sys/socket.h>
#include<sys/types.h>
#include<arpa/inet.h>

int main(void){
//创建套接字
printf("客户端:创建套接字\n");
int sockfd = socket(AF_INET,SOCK_STREAM,0);
if(sockfd == -1){
perror("socket");
return -1;
}
//组织地址服务器的地址结构
printf("客户端:组织地址服务器的地址结构\n");
struct sockaddr_in ser;
ser.sin_family = AF_INET;
ser.sin_port = htons(8888);
ser.sin_addr.s_addr = inet_addr("192.168.222.128");
//发起连接请求
printf("客户端:发起连接请求\n");
if(connect(sockfd,(struct sockaddr*)&ser,sizeof(ser)) == -1){
perror("connect");
return -1;
}
//业务处理
printf("客户端:业务处理\n");
for(;;){
//获取小写的串
char buf[128] = {};
fgets(buf,sizeof(buf),stdin);

if(strcmp(buf,"!\n") == 0){
break;
}

//发送给服务器
if(send(sockfd,buf,strlen(buf),0) == -1){
perror("send");
return -1;
}
//接受大写的串
if(recv(sockfd,buf,sizeof(buf)-1,0) == -1){
perror("recv");
return -1;
}
//显示输出
printf(">>> %s",buf);
}
printf("客户端:关闭套接字\n");
close(sockfd);
return 0;
}

UDP协议,函数,编程模型,域名解析,http协议

并发服务器:使用子进程负责与客户端通信,使用太平间信号进行收尸

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
//基于TCP协议的客户端和服务器
#include<stdio.h>
#include<string.h>
#include<ctype.h> // toupper
#include<unistd.h>
#include<sys/socket.h>
#include<sys/types.h>
#include<arpa/inet.h>
#include<signal.h>
#include<sys/wait.h>
#include<errno.h>
//信号处理函数
void sigchild(int signum){
for(;;){
pid_t pid = waitpid(-1,NULL,WNOHANG);
if(pid == -1){
if(errno == ECHILD){
printf("服务器:没有子进程了\n");
break;
}else{
perror("waitpid");
return ;
}
}else if(pid == 0){
printf("服务器:子进程在运行,收不了\n");
break;
}else{
printf("服务器:回收了%d的僵尸\n",pid);
}
}
}

int main(void){
//对17号信号进行捕获处理
if(signal(SIGCHLD,sigchild) == SIG_ERR){
perror("signal");
return -1;
}
//创建套接字,买了个手机
printf("服务器:创建套接字\n");
int sockfd = socket(AF_INET,SOCK_STREAM,0);
if(sockfd == -1){
perror("socket");
return -1;
}
//组织地址结构 办张电话卡
printf("服务器:组织地址结构\n");
struct sockaddr_in ser ;
ser.sin_family = AF_INET;
ser.sin_port = htons(8888);//字节序转换
//ser.sin_addr.s_addr = inet_addr("192.168.222.128");//串-->整数
ser.sin_addr.s_addr = INADDR_ANY;//接受任意IP地址到来的数据
//绑定套接字和地址结构,手机卡插入手机
printf("服务器:绑定套接字和地址结构\n");
if(bind(sockfd,(struct sockaddr*)&ser,sizeof(ser)) == -1){
perror("bind");
return -1;
}
//启动侦听,从主动变被动,可接受连接请求
printf("服务器:启动侦听\n");
if(listen(sockfd,1024) == -1){
perror("listen");
return -1;
}
//等待连接,等待客户端的连接请求到来,接受连接请求,完成三次握手
for(;;){
printf("服务器:等待客户端的连接请求到来\n");
struct sockaddr_in cli;//用来输出客户端的地址结构
socklen_t len = sizeof(cli);//输出地址结构的大小
int conn = accept(sockfd,(struct sockaddr*)&cli,&len);
if(conn == -1){
perror("accept");
return -1;
}
//创建子进程,负责和客户端通信
pid_t pid = fork();
if(pid == -1){
perror("fork");
return -1;
}
if(pid == 0){
//关闭侦听套接字
close(sockfd);
//业务处理
printf("服务器:业务处理\n");
for(;;){
//接客户端发来的小写的串
char buf[128] = {};
ssize_t size = read(conn,buf,sizeof(buf)-1);
if(size == -1){
perror("read");
return -1;
}
if(size == 0){
//客户端关闭套接字
break;
}
//转成大写
for(int i = 0;i < strlen(buf);i++){
buf[i] = toupper(buf[i]);
}
//将大写的串发会给客户端
if(write(conn,buf,strlen(buf)) == -1){
perror("write");
return -1;
}
}
//关闭套接字
printf("服务器:关闭套接字\n");
close(conn);
return 0;
}
//父进程代码
//关闭通信他戒子
close(conn);
}
close(sockfd);
return 0;
}

UDP协议

从哪里接受数据

1
2
3
#include <sys/socket.h>
ssize_t recvfrom(int sockfd, void* buf, size_t count, int flags,
struct sockaddr* src_addr, socklen_t* addrlen);

src_addr:输出源主机的地址信息
addrlen:输入输出源主机的地址信息的字节数
成功返回实际接收的字节数,失败返回-1

发送数据到哪里

1
2
ssize_t sendto(int sockfd, void const* buf, size_t count, int flags,
struct sockaddr const* dest_addr, socklen_t addrlen);

dest_addr:目的主机的地址信息
addrlen:目的主机的地址信息的字节数

代码
客户端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
//基于udp的客户端
#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<sys/socket.h>
#include<sys/types.h>
#include<arpa/inet.h>

int main(void){
//创建套接字
printf("客户端:创建套接字\n");
int sockfd = socket(AF_INET,SOCK_DGRAM,0);
if(sockfd == -1){
perror("socket");
return -1;
}
//组织服务器地址结构
printf("客户端:组织服务器地址结构\n");
struct sockaddr_in ser;
ser.sin_family = AF_INET;
ser.sin_port = htons(9999);
ser.sin_addr.s_addr = inet_addr("192.168.222.128");
//业务处理
printf("客户端:业务处理\n");
for(;;){
//通过键盘获取小写的串
char buf[128] = {};
fgets(buf,sizeof(buf),stdin);

if(strcmp(buf,"!\n") == 0){
break;
}

//发送给服务器
if(sendto(sockfd,buf,strlen(buf),0,(struct sockaddr*)&ser,sizeof(ser)) == -1){
perror("sendto");
return -1;
}
//接受回传的数据
if(recv(sockfd,buf,sizeof(buf)-1,0) == -1){
perror("recv");
return -1;
}
//显示
printf("%s",buf);
}
//关闭套接字
printf("客户端:关闭套接字\n");
close(sockfd);
return 0;
}

服务端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
//基于udp协议的服务器
#include<stdio.h>
#include<string.h>
#include<ctype.h>
#include<unistd.h>
#include<sys/socket.h>
#include<sys/types.h>
#include<arpa/inet.h>

int main(void){
//创建套接字
printf("服务器:创建套接字\n");
int sockfd = socket(AF_INET,SOCK_DGRAM,0);
if(sockfd == -1){
perror("socket");
return -1;
}
//组织地址结构
printf("服务器:组织地址结构\n");
struct sockaddr_in ser;
ser.sin_family = AF_INET;
ser.sin_port = htons(9999);
ser.sin_addr.s_addr = INADDR_ANY;
//绑定
printf("服务器:绑定套接字和地址结构\n");
if(bind(sockfd,(struct sockaddr*)&ser,sizeof(ser)) == -1){
perror("bind");
return -1;
}
//业务处理
printf("服务器:业务处理\n");
for(;;){
//接受客户端的小写的串
char buf[128] = {};
struct sockaddr_in cli;//用来输出客户端的地址结构
socklen_t len = sizeof(cli);//输出地址结构大小
if(recvfrom(sockfd,buf,sizeof(buf)-1,0,(struct sockaddr*)&cli,&len) == -1){
perror("recvfrom");
return -1;
}
//转成大写
for(int i = 0;i < strlen(buf);i++){
buf[i] = toupper(buf[i]);
}
//回传给客户端
if(sendto(sockfd,buf,strlen(buf),0,(struct sockaddr*)&cli,sizeof(cli)) == -1){
perror("sendto");
return -1;
}
}
//关闭套接字
printf("服务器:关闭套接字\n");
close(sockfd);
return 0;
}

http协议

一种特定的格式要求
HTTP的请求

POST /form/entry HTTP/1.1
HOST: hackr.jp
Connection: keep-alive
Content-Type: application/x-www-form-urlencoded
Content-Length: 16
name=ueno&age=37

请求行由请求方法字段,URL字段和HTTP协议版本字段3个字段组成,用空格分隔
HTTP1.0定义了三种请求方法:GET,POST,HEAD
HTTP1.1新增了五种:OPTIONS,PUT,DELETE,TRACE,CONNECT
GET最常用,用来获取服务器的数据

请求头部由关键字/值对组成,通知服务器有关于客户端请求的信息
Accept:告诉服务器自己接受什么类型的介质
Host:客户端指定自己想访问的web服务器的域名
User_Agent:浏览器表名自己的身份
Referer:浏览器web服务器表名自己是从哪个网页URL获得点击当前请求中的网址
Connection:表示是否需要持久连接

HTTP的响应

HTTP/1.1 200 OK
Date: Tue, 10 Jul 2012 06:50:15 GMT
Content-Length: 362
Content-Type: text/html
<html>

HTTP响应也由三部分组成,分别是状态行,响应头,空行,响应正文
状态码:

  • 200 OK:客户端请求成功
  • 400 Bad Request:服务器请求有语法错误,不能被服务器所理解
  • 403 Forbidden:服务器收到请求,但是拒绝提供服务
  • 404 Not Found:请求资源不存在
  • 503 Server Unavailable:服务器当前不能处理客户端的请求,一段时间后可能恢复正常

消息报头一般包括以下内容:

  • Date:响应时间
  • Content-Type:响应类型
  • Content-Length:响应数据大小
  • Connection:连接状态

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
//客户端,向百度服务器发http请求
#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<sys/socket.h>
#include<sys/types.h>
#include<arpa/inet.h>

int main(int argc,char* argv[]){
// ./a.out <IP地址> <域名> [<资源路径>]
if(argc < 3){
fprintf(stderr,"用法:%s <IP地址> <域名> [<资源路径>]\n",argv[0]);
return -1;
}
char* ip = argv[1];
char* name = argv[2];
char* path = argc < 4 ? "" : argv[3];
//创建套接字
int sockfd = socket(AF_INET,SOCK_STREAM,0);
if(sockfd == -1){
perror("socket");
return -1;
}
//组织服务器地址结构
struct sockaddr_in ser;
ser.sin_family = AF_INET;
ser.sin_port = htons(80);
ser.sin_addr.s_addr = inet_addr(ip);
//发起连接
if(connect(sockfd,(struct sockaddr*)&ser,sizeof(ser)) == -1){
perror("connect");
return -1;
}
//组织请求
char request[1024] = {};
sprintf(request,"GET /%s HTTP/1.1\r\n"
"Host: %s\r\n"
"Accept: */*\r\n"
"Connection: close\r\n"
"User-Agent: Mozilla/5.0\r\n\r\n",path,name);
//发送请求
if(send(sockfd,request,strlen(request),0) == -1){
perror("send");
return -1;
}
//接受响应
for(;;){
char respond[1024] = {};
ssize_t size = recv(sockfd,respond,sizeof(respond)-1,0);
if(size == -1){
perror("recv");
return -1;
}
if(size == 0){
break;
}
printf("%s",respond);
}
printf("\n");
//关闭套接字
close(sockfd);
return 0;
}

线程

程序执行路线

POSIX线程

创建新线程

1
2
3
#include <pthread.h>
int pthread_create(pthread_t * tid, pthread_attr_t const* attr,
void* (*start_routine)(void*), void* arg);

tid:输出线程ID
attr:线程属性,NULL表示缺省属性
start_routine:线程过程函数指针
arg:传递给线程过程函数的参数
成功返回0,失败返回错误码

编译需指定库的名称

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//线程过程函数的参数
#include<stdio.h>
#include<unistd.h>
#include<pthread.h>
#define PI 3.14
//线程过程函数,计算圆的面积 s = PI * r * r
void* area(void* arg){
double r = *(double*)arg;
*(double*)arg = PI * r * r;
return NULL;
}

int main(void){
//创建线程
double r = 10;
pthread_t tid;//用来输出线程的tid
pthread_create(&tid,NULL,area,&r);
sleep(1);
printf("圆的面积是%lg\n",r);
return 0;
}

汇合线程

释放线程资源,获得返回值

1
int pthread_join(pthread_t pid, void** retval);

retval:一级指针的地址,用于存放过程函数的返回值

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
//线程的汇入
#include<stdio.h>
#include<unistd.h>
#include<pthread.h>
#define PI 3.14
//线程过程函数,计算圆的面积
void* area(void* arg){
double r = *(double*)arg;
static double s ;
s = PI * r * r;
sleep(5);
return &s;
}

int main(void){
double r = 10;
pthread_t tid;
pthread_create(&tid,NULL,area,&r);

double* area;//接线程的返回值
pthread_join(tid,(void**)&area);
printf("圆的面积是%lg\n",*area);

return 0;
}

并发冲突,线程同步,互斥锁,条件变量

分离线程

线程结束,不用主线程回收,内核自动回收,不阻塞,无法汇合

1
void pthread_detach(pthread_t pid);

线程同步

互斥锁

初始化互斥体

1
int pthread_mutex_init(pthread_mutex_t* mutex, pthread_mutexattr_t const* attr);

mutex:互斥体
attr:互斥体属性
成功返回0,失败返回错误码
也可以静态方式初始化互斥锁

1
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

销毁互斥体

1
int pthread_mutex_destroy(pthread_mutex_t* mutex);

加/解锁

1
2
int pthread_mutex_lock(pthread_mutex_t* mutex);
int pthread_mutex_unlock(pthread_mutex_t* mutex);

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
//并发冲突
#include<stdio.h>
#include<unistd.h>
#include<pthread.h>
int g_cn = 0;//全局变量
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;//互斥锁
//线程过程函数
void* pthread_add(void* arg){
//加锁
//pthread_mutex_lock(&mutex);
for(int i = 0;i < 1000000;i++){
//加锁
pthread_mutex_lock(&mutex);
g_cn++;
//解锁
pthread_mutex_unlock(&mutex);
}
//解锁
//pthread_mutex_unlock(&mutex);
return NULL;
}

int main(void){
pthread_t tid1,tid2;
pthread_create(&tid1,NULL,pthread_add,NULL);
pthread_create(&tid2,NULL,pthread_add,NULL);
pthread_join(tid1,NULL);
pthread_join(tid2,NULL);
printf("g_cn = %d\n",g_cn);
return 0;
}

条件变量

初始化

1
int pthread_cond_init(pthread_cond_t* cond, const pthread_condattr_t* attr);

cond:条件变量
attr:条件变量属性
也可以静态方式初始化条件变量

1
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

销毁

1
int pthread_cond_destroy(pthread_cond_t* cond);

睡入条件变量

1
int pthread_cond_wait(pthread_cond_t* cond, pthread_mutex_t* mutex);

唤醒条件变量

1
int pthread_cond_signal(pthread_cond_t* cond);

进程是资源分配的最小单位
线程是调度的最小单位

代码
生产者,消费者问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
//条件变量解决生产者和消费者问题
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<pthread.h>
char g_storage[10];//仓库
int g_stock = 0;//当前库存量
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;//互斥锁
pthread_cond_t pcond = PTHREAD_COND_INITIALIZER;//生产者卧室
pthread_cond_t ccond = PTHREAD_COND_INITIALIZER;//消费者卧室
//显示库存情况 生产者:ABC<--D 消费者:ABC-->D
void show(char* who,char* op,char prod){
printf("%s:",who);
for(int i = 0;i < g_stock;i++){
printf("%c",g_storage[i]);
}
printf("%s%c\n",op,prod);
}
//生产者线程
void* producer(void* arg){
char* who = (char*)arg;
for(;;){
//加锁
pthread_mutex_lock(&mutex);
//判断
if(g_stock == 10){//满
printf("%s:满仓\n",who);
pthread_cond_wait(&pcond,&mutex);
}
//生产
char prod = 'A' + rand() % 26;
show(who,"<--",prod);
g_storage[g_stock] = prod;
g_stock++;
//唤醒消费者
pthread_cond_signal(&ccond);
//解锁
pthread_mutex_unlock(&mutex);
usleep((rand() % 100) * 1000);
}
return NULL;
}
//消费者线程
void* customer(void* arg){
char* who = (char*)arg;
for(;;){
//加锁
pthread_mutex_lock(&mutex);
//判断
if(g_stock == 0){//空
printf("%s:空仓\n",who);
pthread_cond_wait(&ccond,&mutex);
}
//消费
char prod = g_storage[--g_stock];
show(who,"-->",prod);
//唤醒生产者
pthread_cond_signal(&pcond);
//解锁
pthread_mutex_unlock(&mutex);
usleep((rand() % 100) * 1000);
}
return NULL;
}

int main(void){
pthread_t t1,t2;
pthread_create(&t1,NULL,producer,"生产者");
pthread_create(&t2,NULL,customer,"消费者");

getchar();
return 0;
}