网络编程与高级系统编程
会当凌绝顶,一览众山小
第一课
系统编程就是利用 系统调用 system call 进行编程
不同的系统调用实现不同的功能
系统编程的特点
1、无法跨平台(OS)
2、速度慢 用户空间到内核空间的切换需要时间
3、更加底层 接口更复杂
课程目录
使用系统底层提供给我们的各种接口和函数,并深入内核,体验系统底层的精妙之处。
Linux系统编程入门
文件I/O 缓冲I/O
文件的打开、关闭、读写
阻塞与非阻塞IO
同步IO
内核内幕:虚拟文件系统
标准I/O
流的打开、关闭与读写
控制缓冲
线程
…
多进程编程
进程体系:
进行新的进程
终止进程
等待子进程退出
特殊进程
僵尸进程
孤儿进程
守护进程
高级进程管理
进程调度
…
进程间通信IPC
基于文件的简单进程间通信
共享内存:互斥锁、条件变量
管道:匿名管道与命名管道
共享存储映射
消息队列
信号量
socket套接字
多线程编程
…
线程池的实现
…
信号
基本信号管理
发送信号
信号集
高级信号管理
时间
…
终端IO编程
…
计算机网络知识补充
TCP网络通信解剖
socket网络编程接口
UDP编程实现
基于CS模型的TCP客户端
P2P模型网络服务的实现
HTTP和BS模型
简单的网络并发模型
基于多进程与多线程
简单的IO多路复用机制
…
文件与IO
标准库函数与系统调用
fopen(3)
调用open(2)打开指定的文件,返回一个文件描述符,分配一个FILE结构体,其中包含该文件的描述符、I/O缓冲区和当前读写位置登信息,返回这个FILE结构体的地址。
FILE缓冲区读写位置 即 FD 句柄
fgetc(3)
通过传入的FILE*参数找到该文件的描述符、I/O缓冲区和当前读写位置,判断能否从I/O缓冲区中读到下一个字符,如果能读到就直接返回该字符,否则调用read(2),把文件描述符传进去,让内核读取该文件的数据到I/O缓冲区,然后返回下一个字符。
fputc(3)
判断该文件的I/O缓冲区是否有空间再存放一个字符,如果有空间则直接保存在I/O缓冲区中并返回,如果I/O缓冲区已满就调用write(2),让内核把I/O缓冲区的内容写回文件。
fclose(3)
如果I/O缓冲区中还有数据没写回文件,就调用write(2)写回文件,然后调用close(2)关闭文件,释放FILE结构体和I/O缓冲区。
buffer(缓冲区) 快递驿站
![image-20210512102011408](https://i-blog.csdnimg.cn/blog_migrate/17de2bd36566417622973fb28ed02c32.png)
全缓冲——buffer(缓冲区)满了才会触发系统调用
行缓冲——buffer(缓冲区)满了 或 遇到\n 会触发系统调用
无缓冲
#include <stdio.h>
int main(void){
int i;
for(i = 0; i < 1025; i++) // 1k=1024 溢出测试 全缓冲
fputc('A',stdout);
fputc('A',stdout);
fputc('A',stdout);
fputc('\n',stdout); //测试行缓冲
fputc('A',stdout);
fputc('B',stderr); //测试无缓冲
while(1){
;
}
return 0;
}
每个进程在linux内核中都有一个task_struct结构体还维护进程相关的信息,称为进程控制块(PCB,Process Control Block)。task_struct 中有一个指针指向files_struct结构体,称为文件描述符,其中每个表项包含一个指向已打开的文件的指针,用户程序不能直接访问内核中的文件描述符,而只能用文件描述符表的索引(即0、1、2、3这些数字),这些索引就称为文件描述符(File Descriptor (FD)),用int型变量保存。
句柄思想
程序启动时会自动打开三个文件:标准输入、标准输出和标准错误输出。
在C标准库中分别用FILE* 指针stdin、stdout和stderr表示
这三个文件描述符分别是0、1、2,保存在相应的FILE结构体中
![image-20210512104557337](https://i-blog.csdnimg.cn/blog_migrate/4bf829f35d32a6d6051aaff8cb18c55a.png)
第二课
文件与IO
open函数
打开或创建一个文件 查看帮助 —— 到对应位置 2 + K
![image-20210514190306161](https://i-blog.csdnimg.cn/blog_migrate/6a8d58233cf8617438d3862733c49e94.png)
![image-20210514190842676](https://i-blog.csdnimg.cn/blog_migrate/0a9de9b3d7f1f52e357b133ac9484918.png)
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#include <stdlib.h>
#include <unistd.h>
int main(int argc, char * argv[]){
if(argc < 2){
printf("usage:cmd filename\n");
return -1;
}
int fd = open(argv[1], O_WONLY | O_CREAT | O_EXCL, 0644); //umask 0022 面具
printf("%d\n", errno);
if(fd < 0) {
perror("OPEN");
exit(-1); // 可以在任何地方 (子函数中结束程序)
return -1; // 只能在主函数中结束程序
} else {
perror("Kaikeba");
}
return 0;
}
C 库函数 void perror(const char *str) 把一个描述性错误消息输出到标准错误 stderr。首先输出字符串 str,后跟一个冒号,然后是一个空格。
close 函数
![image-20210514204731287](https://i-blog.csdnimg.cn/blog_migrate/f0a9fb9be976891bf1b41649e3b49148.png)
文件描述符 fd
open总是返回当前最小的描述符
程序运行打开3个 0 1 2 其余从3开始
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#include <stdlib.h>
#include <unistd.h>
int main(int argc, char * argv[]){
if(argv < 2){
printf("usage:cmd filename\n");
return -1;
}
int fd1 = open(argv[1], O_WONLY | O_CREAT | O_TRUNC, 0777);
if(fd1 < 0) {
perror("OPEN1");
exit(3); // 可以在任何地方 (子函数中结束程序)
} else {
perror("fd1=%d\n", fd1);
}
int fd2 = open(argv[2], O_WONLY | O_CREAT | O_TRUNC, 0777);
if(fd2 < 0) {
perror("OPEN2");
exit(3); // 可以在任何地方 (子函数中结束程序)
} else {
perror("fd2=%d\n", fd2);
}
close(fd1);
int fd3 = open(argv[3], O_WONLY | O_CREAT | O_TRUNC, 0777);
if(fd3 < 0) {
perror("OPEN3");
exit(3); // 可以在任何地方 (子函数中结束程序)
} else {
perror("fd3=%d\n", fd3);
}
close(fd2);
close(fd3);
return 0;
}
read函数
读写位置是在内核中
![image-20210515102143749](https://i-blog.csdnimg.cn/blog_migrate/5aecc4c63f2b2bb7b7acd4ea5f0ad52e.png)
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#include <stdlib.h>
#include <unistd.h>
int main(int argc, char * argv[]){
if(argv < 2){
printf("usage:cmd filename\n");
return -1;
}
int fd1 = open(argv[1], O_RDONLY);
if(fd1 < 0) {
perror("OPEN1");
exit(3); // 可以在任何地方 (子函数中结束程序)
} else {
perror("fd1=%d\n", fd1);
}
char buf[20];
ssize_t n;
int i;
while(n=read(fd,buf,10)){
printf("read %d bytes\n", n);
for(i = 0; i < n; i++) {
printf("%c", buf[i]);
}
}
close(fd1);
return 0;
}
write
![image-20210519184500413](https://i-blog.csdnimg.cn/blog_migrate/957b4a9adc0a7051055942838a613d28.png)
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#include <stdlib.h>
#include <unistd.h>
int main(int argc, char * argv[])
char buf[20];
int n, i;
while(1){
n = read(STDIN_FILENO, buf, 10);
if(n < 0){
perror("READ STDIN");
exit(1);
}
printf("read %d bytes\n", n);
write(STDOUT_FILENO, buf, n);
write(STDOUT_FILENO, "\n", 1);
}
return 0;
}
shell 自动运行 stdin
第三课
阻塞与非阻塞
轮询(Poll)
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#include <stdlib.h>
#include <unistd.h>
int main(void) {
int fd = open("/dev/tty", O-RDONLY | O_NONBLOCK); // O_NONBLOCK 非阻塞
if (fd < 0) {
perror("OPEN /dev/tty");
exit(1);
}
ssize_t n;
char buf[10];
while(1) {
n = read(fd, buf, 10);
if (~n) { // 读取成功
printf("read %d bytes\n", n);
break;
}
if (errno != EGAIN) { // 其他错误 导致 读取错误
perror ("READ /dev/tty");
exit(1);
}
write(STDIN_FILENO, "tyr try?\n", 9);
sleep(1);
}
n = read(fd, buf, 10);
if (n < 0) {
perror("READ /dev/tty");
exit(1);
} else
printf("read %d bytes\n", n);
close(fd);
return 0;
}
写一个mycat
#include <stdio.h>
int main(int argc, char *argv[]) {
if (argc < 2) {
printf("usage:cmd + filename\n");
return 1;
}
FILE *fp = fopen(argv[1], "r");
if(!fp) {
perror("open file");
return 1;
}
int i;
char c;
while(~(c = fgetc(fp))) //-1 取反为 0 fgetc
printf("%c", c);
fcolse(fp);
return 0;
}
fileopt.c
#include <stdio.h>
int main(int argc, char *argv[]) {
FILE *fp = fopen(argv[1], "r+");
if(!fp) {
perror("open file");
return 1;
}
fputc('A', fp);
fputc('B', fp);
fputc('C', fp);
fcolse(fp);
return 0;
}
写一个mycp
#include <stdio.h>
int main(int argc, char *argv[]) {
if (argc < 3) {
printf("usage:cmd + srcefilename + destfilename\n");
return 1;
}
FILE *fp1 = fopen(argv[1], "r");
if(!fp1) {
perror("open srceFile");
return 1;
}
FILE *fp2 = fopen(argv[1], "w");
if(!fp2) {
fclose(fp1);
perror("open destFile");
return 1;
}
int i;
char c;
while(~(c = fgetc(fp1))) //-1 取反为 0 fgetc
fputc(c, fp2);
fcolse(fp);
return 0;
}
argv[0]是个字符串,程序本身的文件名
fseek
#include <stdio.h>
int main(int argc, char *argv[]) {
FILE *fp = fopen(argv[1], "r+");
if(!fp) {
perror("open file");
return 1;
}
fputc('A', fp);
fputc('B', fp);
fputc('C', fp);
printf("pos = %d\n", ftell(fp));
fseek(fp, -3, SEEK_END);
printf("pos = %d\n", ftell(fp));
fputc('X', fp);
fputc('Y', fp);
fcolse(fp);
return 0;
}
第四课
lseek
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#include <stdlib.h>
#include <unistd.h>
int main(void) {
int fd;
if ((fd = open(".\test.txt", O_RDDONLY)) < 0) {
perror("OPEN");
exit(1);
}
char c;
read(fd, &c, 1);
write(STDOUT_FILENO, &c, 1);
lseek(fd, 3, SEEK_CUR); // 返回ftell
read(fd, &c, 1);
write(STDOUT_FILENO, &从, 1);
close(fd);
return 0;
}
fcntl
先前我们以read终端设备为例介绍了非阻塞I/O,因为STDIN_FILENO在程序启动时已经被自动打开了,而我们需要在调用open时指定O_NONBLOCK标志。
可以用fcntl函数改变一个已打开的文件的属性而不必重新open文件,可以重新设置读、写、追加、非阻塞等标志。
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#include <stdlib.h>
#include <unistd.h>
int main(void) {
int flags;
if ((flags = fcntl(STDIN_FILENO,F_GETFL)) < 0) {
perror( "fcntl get flags" ) ;
exit(1);
}
flags |= O_NONBLOCK;
if((flags = fcntl(STDIN_FILENO,F_SETFL, flags)) < 0) {
perror( "fcntl set flags" );
exit(1);
}
char buf[10];
ssize_t n;
while(1){
n=read(0, buf,5);
if(n>=0)
break;
if(errno != EAGAIN) {
perror("read");
exit(1);
}
write(1, "try again?\n", 11);
sleep(1);
}
write(1, buf, n);
return 0;
}
fcntl
![image-20210530105503099](https://i-blog.csdnimg.cn/blog_migrate/ff00a6d24b617eff01ad8dd5db209ba8.png)
linux中 > 表示覆盖原文件内容, >> 表示追加内容。
![image-20210530105745751](https://i-blog.csdnimg.cn/blog_migrate/cd944e0120debb9069099477e43c0762.png)
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#include <stdlib.h>
#include <unistd.h>
int main(int argc, char **argv) {
if (argc < 2) {
printf("usage:cmd fd\n");
exit(1);
}
if(argv[2])printf("argv2 = %s", argv[2]);
else printf("argv2 not found"); // shell 处理完后并没有传入参数
int flags;
if((flags = fcntl(atoi(argv[1]), F_GETFL)) < 0) { //0 1 2 标准输入、标准输出、标准错误输出 atoi(argv[1]) 手动定义的文件描述符
perror("fcntl get flags");
exit(1);
}
switch (flags & O_ACCMODE {
case O_RDONLY:
printf("read only");
break;
case O_WRONLY:
printf("write only");
break;
case O_RDWR:
printf("read write");
break;
default:
printf("invalid access mode\n");
}
if (flags & O_APPEND)
printf(", append");
if (falgs & O_NONBLOCK)
printf(", nonblock");
putchar(10); \\ 换行
/*
printf("flags = %#x\n", flags);
printf("0_RDONLY = %#x\n", 0_RDONLY);
printf("O_WRONLY = %#x\n", O_WRONLY);
printf("O_RDWR = %#x\n", O_RDWR);
printf("O_APPEND = %#x\n", O_APPEND);
printf("O_NONBLOCK = %#x\n", O_NONBLOCK);
printf("O_ACCMODE = %#x\n", O_ACCMODE);
*/
return 0;
}
![image-20210530110324135](https://i-blog.csdnimg.cn/blog_migrate/0aba7d77c678eab680ea0a4a52955520.png)
用例代码
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#include <stdlib.h>
#include <unistd.h>
int main (int argc, char **argv) {
write(5, "hello\n", 6);
return 0;
}
ioctl
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/ioctl.h>
int main (void) {
struct winsize size; // 命令行窗口结构
if (isqtty(1)) { // 判断 1 是不是文件描述符
printf("1 is not tty\n");
exit(1);
}
if (ioctl(1, TIONGWINSZ, &size) < 0) {
perror("ioctl");
exit(1);
}
printf("%d rows, %d columns\n", size.ws_row, size.ws_col); // 打印命令行窗口行数和列数
return 0;
}
第五课
mmap
![image-20210530194050992](https://i-blog.csdnimg.cn/blog_migrate/7706959659187c548aba56dbcf0b3bcb.png)
![image-20210530194439758](https://i-blog.csdnimg.cn/blog_migrate/a3174eaa2893daf9dd32be9a534b2830.png)
![image-20210530194539906](https://i-blog.csdnimg.cn/blog_migrate/91b44f331b855b946101de04c4db62b7.png)
od -tx1 -tc text.txt // 1个字节放一起 按16进制查看ASCII码
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/mman.h>
int main (void) {
int fd = open("test.txt", O_RDWR);
if (fd < 0) {
perror("open file");
exit(1);
}
//int *p = mmap(NULL, 6, PROT_WRITE, MAP_SHARED, fd, 0);//void * int 可以改四个字节
// p[0] = 0x30313233; //小端机 低位放低地址 打印出来是逆序
// ((int *))(((char *)p) + 1)[0] = 0x30313233;
char *p = mmap(NULL, 6, PROT_WRITE, MAP_SHARED, fd, 0);
p[0] = 'A';
p[2] = 'B';
p[3] = 'C';
munmap(p, 6);
return 0;
}
ext2文件系统
![image-20210530201540854](https://i-blog.csdnimg.cn/blog_migrate/c18512af1cafccfc98ebc93ea7c192a5.png)
![image-20210530201803133](https://i-blog.csdnimg.cn/blog_migrate/ebce9be51c7edba52c2050dd409dd0e7.png)
![image-20210530201816412](https://i-blog.csdnimg.cn/blog_migrate/dc0d4faacc9a2f8d2fd7afdd7facb5c5.png)
![image-20210530205737882](https://i-blog.csdnimg.cn/blog_migrate/e60bf9c96b1adefb5a0b61733fead9f8.png)
目录的data block 存文件名 // 目录也是文件,目录的文件内容就是文件名,一层存一层
设备文件 无大小 ,存主次设备号
inode 里还存了 硬链接树 inode 硬链接和原文件全部删除才真正删除,只要存在一个硬链接都不会删除,在inode中不存文件名,硬链接和原文件是同一个东西。
目录当前的名字,目录里的. ,目录里的目录里的… 都算是硬链接
![image-20210530211520853](https://i-blog.csdnimg.cn/blog_migrate/32d158a3210736a9e8ccec585ddf40cd.png)
stat(2)
![image-20210603211416511](https://i-blog.csdnimg.cn/blog_migrate/08acb05a7bdbb0aa321d24725443d5a7.png)
man 2 stat
stat test.txt man stat
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#include <stdlib.h>
#include <unistd.h>
int main(int argc, chat *argv[]) {
struct stat st;
if (argc < 2) {
printf("usage:cmd fd\n");
exit(1);
}
lstat(argv[1], &st); //stat
/*
if (S_ISDIR(st.st_mode)) {
printf("directory\n");
} else
printf("other file type\n");
*/
switch (st.st_mode & S_IFMT) {
case S_IFREG:
printf("regular file\n");
break;
case S_IFDIR:
printf("directory\n");
break;
case S_IFCHR:
printf("charactor device\n");
break;
default:
printf("other file type\n");
}
return 0;
}
int stat(const char *path, struct stat *buf);
int lstat(const char *path, struct stat *buf);
int fstat(int filedes, struct stat *buf);
聪明人一眼就能看出来fstat的第一个参数是和另外两个不一样的,对!fstat区别于另外两个系统调用的地方在于,fstat系统调用接受的是 一个“文件描述符”,而另外两个则直接接受“文件全路径”。文件描述符是需要我们用open系统调用后才能得到的,而文件全路经直接写就可以了。
stat和lstat的区别:当文件是一个符号链接时,lstat返回的是该符号链接本身的信息;而stat返回的是该链接指向的文件的信息。(似乎有些晕吧,这样记,lstat比stat多了一个l,因此它是有本事处理符号链接文件的,因此当遇到符号链接文件时,lstat当然不会放过。而 stat系统调用没有这个本事,它只能对符号链接文件睁一只眼闭一只眼,直接去处理链接所指文件喽)
第六课
strace ./a.out 查看程序执行的系统调用过程
按规定的标准格式存储数据
innode表 文件信息 datablock 内容/ 目录的文件名/文件innode位置
格式化/删除 —— innode表和目录文件名 删除 —— datablock 不会动
![image-20210603204024293](https://i-blog.csdnimg.cn/blog_migrate/aa747ef41fe7320a281879d8d01e2d2a.png)
f 一般和文件句柄有关 l 一般和软链接有关
![image-20210603211624598](https://i-blog.csdnimg.cn/blog_migrate/9ea475bc04154b708c4f5045ead2126f.png)
access 查看是否有权限访问 与innode中的 st_mode
chmod 修改访问权限
chown 修改文件所属组和所属者
utime 修改文件访问时间和修改时间
truncate 文件截断或扩展
link 创建硬链接 symlink 创建软链接
硬链接
软链接 类似快捷方式
unlink 删除链接
rename 更换名字 mv 把路径换一下 innode不用变,找到innode的路径更换
windows操作系统每个盘都有一个文件系统,同盘移动位置,不用改变innode和datablock,只用改变名字所在的目录
readlink 读取链接中的路径
mkdir 创建一个新的目录 注意:父目录要多一个硬链接
rmdir 删除一个空的目录
opendir/readdir/closedir 3
![image-20210604153857941](https://i-blog.csdnimg.cn/blog_migrate/6607756750b4f453cbba3637c435d8ce.png)
![image-20210604160231169](https://i-blog.csdnimg.cn/blog_migrate/0e6b885c99ed5065f8de8b93e65e69db.png)
系统调用
vim 查看函数原型——系统函数:shift+K
简易版ls
#include <stdio.h>
#include <sys/tyeps.h>
#include <dirent.h>
#include <stdlib.h>
int main(int argc, char **argv) {
if(argc < 2){
printf("usage:cmd + path\n");
return 1;
}
DIR *dir;
struct dirent *dp;
if (!(dir = opendir(argv[1]))) {
perror("opendir");
exit(1);
}
while (dp = readdir(dir)) {
printf("%s\t",dp->d_name);
}
putchar(10);
closedir(dir);
return 0;
}
ls -R 简易ls递归打印所有文件
#include <stdio.h>
#include <sys/tyeps.h>
#include <sys/stat.h>
#include <dirent.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
void printDir(char *dirname){
DIR *dir;
struct dirent *dp;
struct stat st; // 判断文件类型
if (!(dir = opendir(dirname))) {
perror("opendir");
exit(1);
}
while (dp = readdir(dir)) {
if(!strcmp(dp->d_name, ".") || !strcmp(dp->d_name, "..")) continue;
sprintf(pathname, "%s/%s", dirname, dp->d-name);
if(stat(pathname, &st) < 0) {
perror("stat");
exit(1);
}
if(S_ISDIR(st.st_mode)) {
printfDir(pathname);
}
printf("%s\t",dp->d_name);
}
putchar(10);
closedir(dir);
}
int main(int argc, char **argv) {
if(argc < 2){
printf("usage:cmd + path\n");
return 1;
}
printfDir(argv[1]);
return 0;
}
我是谁 我在哪 我有没有权限
虚拟文件系统vfs
![image-20210604170342460](https://i-blog.csdnimg.cn/blog_migrate/22d99d690315a585c2123143d14d6eda.png)
![image-20210604172444571](https://i-blog.csdnimg.cn/blog_migrate/dd071bc6b7537a966e2583ecad242308.png)
file_operation innode_operation
![image-20210604172333097](https://i-blog.csdnimg.cn/blog_migrate/6a1a919a9be8718679337a8f636b617e.png)
dup和dup2
![image-20210604173115625](https://i-blog.csdnimg.cn/blog_migrate/7610918b31cb35f2d26c7dced9824d7f.png)
文件重定向
#include <stdio.h>
#include <sys/tyeps.h>
#include <sys/stat.h>
#include <dirent.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
int main(void) {
int fd, save_fd;
if((fd = open("test.txt", O_PDWR)) < 0) {
perror("open");
exit(1);
}
save_fd = dup(1);
dup2(fd, 1);
close(fd);
write(1, "1234567890", 10);
dup2(save_fd, 1);
write(1,"1234567890", 10);
close(fd);
}
流程
![image-20210604174315066](https://i-blog.csdnimg.cn/blog_migrate/06c8c1194dd743a936e2b013ee366b88.png)
本节小节
![image-20210604174420362](https://i-blog.csdnimg.cn/blog_migrate/6418785c3eaa8031fed3b0866ffcff74.png)
完成ls -al 的功能
第七课
进程体系和进程管理
![image-20210604203350692](https://i-blog.csdnimg.cn/blog_migrate/69c3a9738c9ee0d6544b7dbd6cd942aa.png)
01 进程控制块PCB
ps aux 打印所有进程
task_struct结构体:
- 进程id。系统中每个进程有唯一的id,在C语言中用pid_t类型表示,其实就是一个非负整数。
- 进程的状态,有运行、挂起、停止、僵尸等状态。
- 进程切换时需要保存和恢复的一些CPU寄存器。
- 描述虚拟地址空间的信息。
- 描述控制终端的信息。
- 前工作目录(Current Working Directory) 。
- umask掩码。
- 文件描述符表,包含很多指向file结构体的指针。
- 和信号相关的信息。
- 用户id和组id。
- 控制终端、Session和进程组。
- 进程可以使用的资源上限(Resource Limit)。
02 进程控制fork
fork的作用是根据一个现有的进程复制出一个新进程,原来的进程称为父进程(ParentProcess),新进程称为子进程(Child Process)。系统中同时运行着很多进程,这些进程都是从最初只有一个进程开始一个一个复制出来的。
在Shell下输入命令可以运行一个程序,是因为Shell进程在读取用户输入的命令之后会调用fork复制出一个新的Shell进程。
![image-20210604212315431](https://i-blog.csdnimg.cn/blog_migrate/8f7ab41546e8c29abd8883d3532568f3.png)
fork()
克隆 除了 process ID 不一样,其他都一样
![image-20210604213105563](https://i-blog.csdnimg.cn/blog_migrate/3b25fa4faaf4c1b7f0ee833e867cf518.png)
父进程拿到子进程的id,子进程拿到0
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main(void) {
char *msg;
int n;
pid_t pid = fork();
if (pid < 0) {
perror("fork");
exit(1);
}
if (pid == 0) {
msg = "child process~\n";
n = 6;
} else {
msg = "parent process\n";
n = 3;
}
while (n > 0) {
printf(msg);
sleep(1);
n--;
}
return 0;
}
a.out 死了, a.out 的子进程变为孤儿进程 , bash (只管子进程) 不管孤儿进程,输出提示符
孤儿院(init进程 pid 为 1) 接管 孤儿进程
![image-20210604220700037](https://i-blog.csdnimg.cn/blog_migrate/8f8b40a3eb4d2f80ee2deebcb0de15ea.png)
getpid() getppid()
fork在子进程中返回0,子进程可以调用getpid函数得到自己的进程id,也可以调用getppid函数得到父进程的id。在父进程中用getpid可以得到自己的进程id,然而要想得到子进程的id,只有将fork的返回值记录下来,别无它法。
孤儿院( init 进程 pid 为 1) 接管 孤儿进程
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main(void) {
char *msg;
int n;
pid_t pid = fork();
if (pid < 0) {
perror("fork");
exit(1);
}
if (pid == 0) {
n = 6;
while (n > 0) {
printf("child self = %d, parent = %d\n", getpid(), getppid());
n--;
sleep(1);
}
} else {
n = 3;
while (n > 0) {
printf("father self = %d, parent = %d\n", getpid(), getppid());
n--;
sleep(1);
}
}
return 0;
}
创建10个子进程,打印他们的pid
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main(void) {
char *msg;
int n, i;
for (i = 0; i < 10; i++) {
pid_t pid = fork();
if (pid < 0) {
perror("fork");
exit(1);
}
if (!pid) {
printf("child[%d], self = %d\n, parent = %d\n", i, getpid(), getppid());
sleep(1);
break;
}
}
return 0;
}
如何用gdb调试多进程
03 exec函数族
当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。
这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回,如出错则返回-1,所以exec函数只有出错的返回值而没有成功的返回值。
exec用被执行的程序完全替换调用它的程序的影像。fork创建一个新的进程就产生了一个新的PID,exec启动一个新程序,替换原有的进程,因此这个新的被exec执行的进程的PID不会改变,和调用exec函数的进程一样。
exec函数里的参数可以分成3个部分, 执行文件部分, 命令参数部分, 环境变量部分(以e结尾,传一份新的环境变量).
![image-20210605090616465](https://i-blog.csdnimg.cn/blog_migrate/17fc4f3ad7f11f21c3ef113d41f86ca0.png)
![image-20210605090911214](https://i-blog.csdnimg.cn/blog_migrate/2dea6fb3b725921319258b4a50fe4a09.png)
#include <stdio.h>
#include <unistd.h>
int main(void) {
printf("path value = [%s]\n", getenv("PATH"));
setenv("PATH", "hell", 1); //更改环境变量 1判断是否覆盖
printf("path value = [%s]\n", getenv("PATH"));
/*extern char **environ; //环境变量
int i;
for (i = 0; environ[i]; i++) {
printf("%s\n",environ[i]);
}*/
return 0;
}
arr[6] = {“hello”};
const 在 * 前 表示那部分内存只读 ,如const char *p = arr , 在 后 如 const * p = arr 表示p 是只读的*
const放在*的左边,一律是修饰所指向的变量的值不能改变!即:仅仅是不能够通过*c或者*d 来 赋值。 (限制的赋值)
const 放在*右边,一律修饰的所指向的地址不能改变。即:不可以修改c的地址。但是可以通过*c来修改。c所指向的地址值; (限制的地址
-
带有字母l(表示list)的exec函数要求将新程序的每个命令行参数都当作一个参数传给它,命令行参数的个数是可变的,最后一个可变参数应该是NULL,起sentinel(哨兵)的作用。
-
对于带有字母v(表示vector)的函数,则应该先构造一个指向各参数的指针数.然后将该数组的首地址当作参数传给它,数组中的最后一个指针也应该是NULI
像main函数的argv参数或者环境变量表一样。 -
不带字母p(表示path)的exec函数第一个参数必须是程序的相对路径或绝对路径,
例
如"/bin/ls"或"./a.out"。 -
对于带字母p的函数:如果参数中包含/,则将其视为路径名。否则视为不带路径的程序名,在PATH环境变量的目录列表中搜索这个程序。
-
对于以e(表示environment)结尾的exec函数,可以把一份新的环境变量表其他exec函数仍使用当前的环境变量表执行新程序
#include <stdio.h> #include <unistd.h> #include <stdlib.h> int main(void) { /*execlp("ls", "hahaha", "-a", "-l", NULL); // 第 2个参数是argv[0],系统自带,给不给无所谓 perror("exec")*/ char *arr[] = {"hello", "-a", "-l", NULL}; execvp("ls", arr); exit(1); }
![image-20210605095637550](https://i-blog.csdnimg.cn/blog_migrate/c9237a62dc0c6204458f5b2936f587ac.png)
在《UNIX环境高级编程》一书中,讲到exec函数及其使用,其中有一个例子,简单来说就是这样:
execlp(“ls”, “ls”, “-al”, (char *)0);
其输出结果就跟我们在终端里输入ls命令得到的结果一样。
在说疑问之前,先看函数的定义:
int execlp(const char *file, const char arg, … / (char *) NULL */);
其中第一个参数file指向可执行文件名称,因为execlp()函数会从系统PATH路径中寻找相应命令,所以不需要带完整路径;
第二个参数之后就都是传给可执行文件的参数,类似main函数的args[],只是最后一个参数必须为空字符指针;
疑问是这样的,当我们在终端输入 ls -al 的时候,对应到execlp中应该是 file,arg[0] 或是 file,arg[1] 呢?要完全对应应该输入ls ls -al,但这又不合法,所以最有可能是file, arg[1],那此时的 arg[0] 又在哪里,谁给的?
其实这个疑问还是很好解释的,《UNIX环境高级编程》一书中有提到,当内核起动C程序时会使用一个exec函数(7.2 main函数 ),而我们也知道,shell执行命令的方式就是fork+exec,所以我们执行ls命令不带参数并非代表没有这个参数,argv[0]这个参数系统还是会传递给exec函数的,我们可以做个试验
int main(int num_args, const char* args[])
{
for(int i = 0; i < num_args; ++i)
{
printf(“args[%d] = %s\n”, i, args[i]);
}
return 0;
}编译后执行该程序
$./a.out
args[0] = ./a.out
由此可见,我们执行./a.out,本意是指示可执行程序的路径和名称(对应execlp中的file指针),看起来不带参数,但args[0]还是存在;同理我们执行ls,不带参数,但ls还是能从系统中得到arg[0]这个参数。
至于这个参数是什么,那是另外一回事了,比如某些shell会将此参数设置为完全的路径名(8.9 exec函数)。
在我们的main()函数里,可以像使用argc和argv一样地用argc和argv。在默认情况下,argc等于1,argv[0]存放着程序文件的完全路径。
所以我们用argv[0]来获取可执行程序的路径,比用其他方式方便的多。
第八课
实现流的重定向
管道(必然存在流的重定向,是程序的输出作为另一个程序的输入)
小写转大写
#include <stdio.h>
#include <ctype.h>
int main(void) {
int ch;
while(~(ch = getchar())){
putchar(toupper(ch));
}
return 0;
}
流的重定向
功能:**把 argv[1] 的字符 转为大写 输出到 argv[2] **
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
int main(int argc, char *argv[]) {
if (argc < 2) {
printf("usage:cmd + inputfile + outputfile\n");
return 1;
}
int fd = open(argv[1], O_RDONLY);
if (fd < 0) {
perror("open read");
exit(1);
}
dup2(fd, 0); // 把程序的标准输入流变为 fd
close(fd);
fd = open(argv[2], O_WRDONLY | O_CREAT, 0644);
if (fd < 0) {
perror("open write");
exit(1);
}
dup2(fd, 1); // 把fd 变为程序的标准输出流
close(fd);
excel("./upper", "./upper", NULL); // 把argv[1]的字符 转为大写 输出到 argv[2]
perror("exec");
exit(1);
}
程序运行起来的内存 分 内核空间 、 用户空间
一个进程在终止时会 关闭所有文件描述符,释放在用户空间分配的内存,
**但它的PCB(内核空间的)还保留着,内核在其中保存了一些信息 : **
如果是正常终止则保存着退出状态,如果是异常终止则保存着导致该进程终止的信号是哪个。
父进程可以调用wait或waitpid获取这些信息,然后彻底清除掉这个进程。
![image-20210605221017326](https://i-blog.csdnimg.cn/blog_migrate/2df7e2bdc592ec85f56315a14892eb3e.png)
例如:一个进程的退出状态可以在Shell中用特殊变量$?查看,
因为Shell是它的父进程,当它终止时Shell调用wait或waitpid得到它的退出状态同时彻底清除调这个进程。
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main(void) {
pid_t pid = fork();
if (pid < 0){
perror("fork");
exit(1);
}
if (pid) {
while(1)
sleep(1);
} else {
exit(3);
}
return 0;
}
父进程 睡了后 ctrl + c 退出,子进程由僵尸进程变为孤儿进程
僵尸进程 挂了没人收尸 没有回收PCB
Z+ 僵尸进程 S+ 睡着的进程
kill 无法直接杀死僵尸进程
waitpid 的 pid参数
option参数
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main(void) {
pid_t pid = fork();
if (pid < 0) {
perror("fork");
exit(1);
}
if(pid == 0) {
int n = 5;
while (n > 0) {
printf("this is child process %d\n", getpid());
sleep(1);
n--;
}
exit(3); // 子进程 退出 返回为3
else {
int stat_val;
waitpid(pid, &stat_val, 0);
if (WIFEXITED(stat_val)){
pirntf("Child exit with code %d\n", WEXITSTATUS(stat_val));
} else if (WISFSIGANLED(stat_val)){
printf("Child terminated abnormally, signal %d\n", WTERMSIG(stat_val));
}
}
return 0;
}
父进程调用wait或waitpid时可能会:
1.阻塞(如果它的所有子进程都还在运行)。
2.带子进程的终止信息立即返回(如果一个子进程已终止,正等待父进程读取其终止信息)
3.出错立即返回(如果它没有任何子进程)。
这两个函数的区别是:
如果父进程的所有子进程都还在运行,调用wait将使父进程阻塞,
而调用waitpid时如果在options参数中指定WNOHANG可以使父进程不阻塞而立即返回0。wait只能等
终止的子进程,而waitpid可以通过pid参数指定等待哪一个子进程。。
进程间通信
每个进程各自有不同的用户地址空间,任何一个进程的全局变量在另一个进程中都看不到,所以进程之间要交换数据必须通过内核,在内核中开辟一块缓冲区,进程1把数据从用户空间拷到内核缓冲区,进程2再从内核缓冲区把数据读走,内核提供的这种机制称为进程间通信(IPC,InterProcess Communication)
管道是单向的
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main(void) {
pid_t pid;
int fd[2];
if (pipe(fd) < 0) { // 管道的建立
perror("pipe");
exit(1);
}
pid = fork();
if (pid < 0) {
perror("fork");
exit(1);
}
if (pid > 0) {
close(fd[0]);
write(fd[1], "hello pipe\n", 11);
wait(NULL);
} else {
close(fd[1]);
sleep(1);
n = read(fd[0], buf, 20);
write(1, buf, n);
}
return n;
}
上面的例子是父进程把文件描述符传给子进程之后父子进程之间通信,也可以父进程fork两次,把文件描述符传给两个子进程,然后两个子进程之间通信,总之需要通过fork传递文件描述符使两个进程都能访问同一管道,它们才能通信。
使用管道需要注意以下4种特殊情况
(假设都是阻塞I/O操作,没有设置O_NONBLOCK标志):
1.如果所有指向管道写端的文件描述符都关闭了,而仍然有进程从管道的读端读数据,那么管道中剩余的数据都被读取后,再次read会返回0,就像读到文件末尾一样。
2.如果有指向管道写端的文件描述符没关闭,而持有管道写端的进程也没有向管道中写数据,这时有进程从管道读端读数据,那么管道中剩余的数据都被读取后,再次read会阻塞,直到管道中有数据可读了才读取数据并返回。
3.如果所有指向管道读端的文件描述符都关闭了,这时有进程向管道的写端write,那么该进程会收到信号SIGPIPE,通常会导致进程异常终止。
4.如果有指向管道读端的文件描述符没关闭,而持有管道读端的进程也没有从管道中读数据,这时有进程向管道写端写数据,那么在管道被写满时再次write会阻塞直到管道中有空位置了才写入数据并返回。 管道写满64k
第九课
管道(popen和pclose)
这两个函数实习的操作是:创建一个管道,fork,关闭管道的不使用端,exec一个cmd命令,等待命令终止
第十七课
学完本节课程后,同学将掌握网络结构的基本概念,如何查看IP,DHCP协议原理ARP请求、ICMP请求分析,NAT网关原理,动态路由算法的相关知识
网络结构