C++——WebServer服务器项目(1)
项目场景:
C++——WebServer服务器编程
项目搭建
(1)配置虚拟机,下载XShell、Xftp以及windows版本的VScode;
(2)***H:
sudo apt install openssh-server
(3)在XShell中配置会话以连接到虚拟机,VScode中配置远程SSH;
(4)安装数据库:
sudo apt install mysql-server sudo apt install mysql-client sudo apt install libmysqlclient-dev
查看账号以及密码以登录数据库:
sudo cat /etc/mysql/debian.cnf
之后修改密码。。。遇到修改密码问题参考mysql debian-sys-maint_关于mysql安装后登不进的问题--ERROR 1045 (28000): Access denied for user 'debian-sys-main...
(5)安装g++、gcc、make等工具:
sudo apt install build-essential
(6)在当前目录(有Makefile文件的目录):
make
生成的可执行文件在bin
目录下。
在终端输入:
bin/server
打开浏览器,输入自己虚拟机的IP地址以及端口号1316。
知识点
1.Linux相关
(1)常用命令:
ifconfig //查看网络配置和IP地址 ps aux //查看进程 ps -ef | grep ssh //ps查看进程信息 |管道符 grep过滤 top //实时显示进程动态 ulimit -a //查看进程id、状态、可使用的资源上限 kill -l //列出所有信号 netstat -anp //查看网络状态 cd //返回根目录 cd .. //返回上一级目录 pwd //查看当前目录 ll //查看当前目录下各文件(夹)的详细信息(权限、大小、修改时间等) chmod //更改权限 mkdir //新建文件夹 rename //重命名文件夹 rmdir //删除文件夹 mv //移动 改名 ls //显示当前目录下的所有文件(夹) cp //复制 rm //删除 -r 删除文件夹 touch //创建文件 cat //显示文件内容 tree //查看树形的文件分布 echo //用于字符串的输出 clear //清除屏幕 ctrl+l man //使用手册 history //查看历史使用命令 free -m //查看内存使用情况 gcc -v //查看gcc版本 g++ -v //查看g++版本
(2)程序的运行过程:
预处理-->编译-->汇编-->链接。
预处理-E
:宏替换、去掉注释、头文件拷贝生成.i文件
;
编译不汇编-S
生成.s文件
,编译汇编-c
:语法检查生成.o文件
;
链接整合定义生成可执行程序.out文件
。
【注】声明作用在编译阶段,定义(装在链接库里)作用在链接阶段。
(3)Makefile
定义了一系列规则,通过make执行——>自动化编译。
////////////////////
(4)gdb调试:
-g //调试 在可执行文件中加入源代码信息 -D //在编译时指定一个宏 -w //不生成警告 -Wall //生成所有警告 -On //优化级别 n:1 2 3 -l //指定使用的库 -L //库路径 -std //-std==c++11 -I //指定include包含搜索的目录
终端输入
g++ -g -Wall main.cpp -o main gdb main
进入调试模式,命令:
l 行号 //查看代码 b 行号 //打断点 i break //查看断点信息 d 断点编号 //删除断点 start //程序停在第一行 run //遇到断点停止 c //继续,到下一个断点停 s //向下单步调试(会进入函数体) finish //跳出函数体 n //向下直行一行代码(不进入函数体) p 变量名 //打印变量的值 ptype 变量名 //打印变量类型
【注】多进程下的GDB调试:
set follow-fork-mode child 调试子进程 set follow-fork-mode parent 调试父进程
(5)静态库(lib/a)和动态库(dll/so):
静态库在链接阶段复制到程序中(速度快,占内存) 动态库在运行时由系统动态加载到程序中(速度慢,共享库)
静态库: g++得到.o文件 ar rcs libxxx.a xxx.o 动态库: g++ -c -fpic xxx.cpp g++ -shared xxx.o -o libxxx.so // ldd 可执行文件名称 //查看动态库的依赖关系
添加环境变量:
env //查看当前系统所有的环境变量 echo $LD_LIBRARY_PATH //查看某个环境变量的值 //临时添加环境变量 export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/目录路径/ //永久添加环境变量 vim .bashrc //.bashrc是用户级的配置文件 再将export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/目录路径/ 加进去 . .bashrc 或者 source .bashrc
2.多进程
(1)程序是包含一系列信息的文件,这些信息描述了如何在运行时创建一个进程。
进程是正在运行的程序的实例。
(2)并行:同一时刻,有多条指令在多个处理器上同时执行。
并发:同一时刻,只能有一条指令执行,但多个进程指令被快速轮换执行,宏观上同时执行,微观上并不是同时执行,只是把时间片分成若干段,使多个进程快速交替的执行
(3)进程控制块PCB
:为了管理进程,内核必须对每个进程所作的事情进行清楚的描述。
Linux的进程控制块是task_struct
结构体。
里面主要有:
进程id、状态、可使用资源的上限;
切换时要保存和恢复的CPU寄存器;
虚拟地址空间信息;
终端信息、信号相关信息;
当前工作目录;
umask掩码;
文件描述符表;
用户id、组id、会话和进程组。
(4)在终端输入ulimit -a
可以查看资源上限。
可以使用ulimit -s 具体数值
进行修改。
(5)进程状态:
新建态:创建进程;
终止态:终止进程。
(6)
top //实时显示进程动态 kill 信号值 PID //杀死进程 kill -l //列出所有信号 kill -9 PID
(7)除了init进程外,每个进程都有父进程PPID。
pid_t getpid(void);//获取当前进程号 pid_t getppid(void);//获取当前进程的父进程号 pid_t getpgid(pid_t pid);//如果传None获取当前进程的进程组id,如果传进程号获取进程号的进程组ID pid_t fork(void); //创建进程 //读时共享 写时拷贝 //会返回两次 //pid_t pid = fork(); //pid > 0时,执行父进程代码,此时的pid为子进程ID;pid = 0时,执行子进程代码;
在虚拟地址空间的视角下,fork()
函数相当于把父进程的虚拟地址空间clone给子进程。fork()以后,子进程用户区数据和父进程一样。内核区也会拷贝过来,但是PID
不一样。但是两个虚拟地址空间是相互独立的。
(8)exec
函数族的作用是根据指定的文件名找到可执行文件,并用它来取代调用进程的内容。换句话说,就是在调用进程内部执行一个可执行文件。
(9)进程的退出:
exit(0); //会刷新I/O缓冲 _exit(0); //不会刷新I/O缓冲
#include <stdio.h> #include <stdlib.h> #include <unistd.h> int main() { printf("hello\n"); printf("world"); // exit(0); //输出hello world _exit(0); //输出hello return 0; }
(10)孤儿进程和僵尸进程:
孤儿进程:父死子没死,会被init进程接管回收;
僵尸进程:父活子死,无法杀死,可以杀死父进程让init进程接管回收。也可以让内核给父进程一个SIGCHLD
信号让其回收其子。
(11)进程退出:
wait(); //一次只能清理一个子进程 waitpid();
(12)进程间的通信(IPC
):数据传输、通知事件、资源共享、进程控制。
信号; //Unix 管道; //Unix 有名管道(FIFO); //Unix 消息队列; 共享内存; 信号量; Socket套接字。
(13)管道:
用于有亲缘关系的进程间通信;
一个管道是一个字节流;
通过管道传递的数据是顺序的,从管道中读取出来字节的顺序和被写入管道的顺序是完全一样的(类似一个队列);
管道中数据传递方向是单向的,一端写入一端读取,是半双工的;
管道读数据是一次性的。
终端命令:
ls | wc -l //wc——统计文件数目 ulimit -a //查看管道缓冲大小
函数:
int pipe(int pipefd[2]);//创建一个匿名管道,用来进程间通信。 long fpathconf(int fd, int name);//查看管道缓冲大小
【注】为什么管道用于有亲缘关系的进程间通信(为什么亲缘关系间的进程能通过管道通信)?
因为父进程fork出来一个子进程,会把虚拟地址空间复制一份,父进程中的这个虚拟地址空间中有一个文件描述符表,指向读端和写端,那么子进程复制出来也有一个文件描述符表,指向读端和写端,所以能进行通信。
【注】为什么要一个关闭读端一个要关闭写端?
为了避免父(子)进程写之后父(子)进程又读。
(14)有名管道FIFO
:
有名管道提供了一个路径名与之关联,所以FIFO创建的进程不存在亲缘关系的限制,进程只要能访问该路径,就能通过FIFO相互通信。
终端命令:
mkfifo 名字 //创建FIFO管道
函数:
int mkfifo(const char *pathname, mode_t mode); //创建FIFO管道
管道的读写特点:
读管道: 管道中有数据: read返回实际读到的字节数 管道中无数据: 写端被全部关闭,read返回0(相当于读到文件的末尾) 写端没有完全关闭,read阻塞等待 写管道: 读端全部被关闭: 进程异常终止(进程收到SIGPIPE信号) 读端没有全部关闭: 管道已满,write阻塞 管道没有满,write将数据写入,并返回实际写入的字节数
(15)内存映射:
(I/O)将磁盘文件数据映射到内存,通过修改内存可修改文件。
内存映射实现的进程通信是非阻塞的。
函数:
//将一个文件或者设备的数据映射到内存中 void *mmap(void *addr, size_t length, int prot, int flags,int fd, off_t offset); //释放内存映射 int munmap(void *addr, size_t length);
(16)共享内存:
共享内存允许两个或者多个进程共享物理内存的同一块区域(通常被称为段)。
无需内核介入。
终端命令:
ipcs -a //打印所有ipc信息 ipcs -m //打印共享内存ipc信息 ipcs -q //打印消息队列ipc信息 ipcs -s //打印信号ipc信息 ipcrm -M shmkey //移除shmkey创建的共享内存段 ipcrm -m shmid //移除shmid标识的共享内存段 ipcrm -Q msgkey //移除msgkey创建的消息队列 ipcrm -q msgid //移除msgid标识的消息队列 ipcrm -S semkey //移除semkey创建的信号 ipcrm -s semid //移除semid标识的信号
函数:
//创建一个新的共享内存段,或者获取一个既有的共享内存段的标识。新创建的内存段中的数据都会被初始化为0 int shmget(key_t key, size_t size, int shmflg); //和当前的进程进行关联 void *shmat(int shmid, const void *shmaddr, int shmflg); //通信 addr //释放: //解除当前进程和共享内存的关联 int shmdt(const void *shmaddr); //删除共享内存,只调用一次,所有的关联进程都解除了关联才调用 int shmctl(int shmid, int cmd, struct shmid_ds *buf);
操作系统如何知道一块共享内存被多少个进程关联? 答: 共享内存维护了一个结构体struct shmid_ds, 这个结构体中有一个成员 shm_nattch, shm_nattach 记录了关联的进程个数
(17)信号:
信号是 Linux 进程间通信的最古老的方式之一,是事件发生时对进程的通知机制,有时也称之为软件中断,它是在软件层次上对中断机制的一种模拟,是一种异步通信的方式。
信号可以导致一个正在运行的进程被另一个正在运行的异步进程中断,转而处理某一个突发事件。
信号的特点: 简单;不能携带大量信息;满足某个特定条件才发送;优先级比较高。
终端命令:
kill -l //查看所有信号
常用的信号:
SIGINT //终止进程——ctrl+C SIGQUIT //终止进程——ctrl+\ SIGKILL //杀死进程 SIGCSTOP //停止进程 SIGCONT //继续进程
信号的 5 种默认处理动作: Term 终止进程 Ign 当前进程忽略掉这个信号 Core 终止进程,并生成一个Core文件,用于保存错误信息 // Stop 暂停当前进程 Cont 继续执行当前被暂停的进程
【注】Core的使用:
ulimit -a ulimit -c unlimited //更改可使用的资源上限 g++ ./a.out -g gdb a.out core-file core //查看core文件的错误信息
信号的状态:产生、未决(信号产生了没被处理)、阻塞(阻塞信号被处理,不阻塞信号产生)。
int kill(pid_t pid, int sig);//给任何的进程或者进程组pid, 发送任何的信号 sig int raise(int sig);//给当前进程发送信号 void abort(void);//发送SIGABRT信号给当前的进程,杀死当前进程 //设置定时器(闹钟)。函数调用,开始倒计时,当倒计时为0的时候,函数会给当前的进程发送一个信号:SIGALARM,终止当前进程 unsigned int alarm(unsigned int seconds); alarm(0); //取消定时器 //设置定时器(闹钟)。可以替代alarm函数。精度微妙us,可以实现周期性定时(每隔几秒钟做一件事) int setitimer(int which, const struct itimerval *new_value, struct itimerval *old_value); //设置某个信号的捕捉行为 sighandler_t signal(int signum, sighandler_t handler); //检查或者改变信号的处理。信号捕捉 int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
【注】使用SIGCHLD
信号解决僵尸进程的问题。
多个信号可使用一个称之为信号集的数据结构来表示。
int sigemptyset(sigset_t *set);//清空信号集中的数据,将信号集中的所有的标志位置为0 int sigfillset(sigset_t *set);//将信号集中的所有的标志位置为1 int sigaddset(sigset_t *set, int signum);//设置信号集中的某一个信号对应的标志位为1,表示阻塞这个信号 int sigdelset(sigset_t *set, int signum);//设置信号集中的某一个信号对应的标志位为0,表示不阻塞这个信号 int sigismember(const sigset_t *set, int signum);//判断某个信号是否阻塞 int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);//将自定义信号集中的数据设置到内核中(设置阻塞,解除阻塞,替换) int sigpending(sigset_t *set);//获取内核中的未决信号集
(18)守护进程:在后台运行,不会被ctrl+C
停止。一般采用以 d
结尾的名字。
互联网知识点