linux c++学习笔记整理

文章目录

linux c++学习笔记整理

环境准备

cpp 复制代码
安装gcc编译环境
yum -y install gcc*

查看版本
gcc -v

yum -y install centos-release devtoolset-8-gcc*

安装库函数的帮助文档
yum -y install man-pages 

帮助文档的使用
man 级别 命令或函数
1-用户命令 2-系统接口 3-库函数

--------------windows下vs code远程连接unbuntu环境---------

 Visual Studio Code (VS Code) 连接到 Ubuntu 服务器的步骤:
1) Remote - SSH 插件:在 VS Code 的插件市场中搜索 "Remote - SSH" 并安装该插件。
2)打开远程窗口:在 VS Code 中按下 Ctrl + Shift + P(或者 Cmd + Shift + P 在 macOS 上),然后输入 "Remote-SSH: Connect to Host..." 并选择此选项。
3)点击 "Configure SSH Hosts",然后选择 "Add New SSH Host"。
配置连接信息:在弹出的输入框中输入您的 Ubuntu 服务器的连接信息,例如用户名和 IP 地址。按照提示输入密码或者选择公钥验证方式(如果您已经设置了 SSH 密钥)。

---- 问题:ssh连接Ubuntu报:设置ssh主机 正在初始化vs code服务器 
在以管理员身份打开的 PowerShell 窗口中再次尝试运行以下命令来安装 OpenSSH 客户端功能:
命令:Add-WindowsCapability -Online -Name OpenSSH.Client~~~~0.0.1.0
检查文件权限:确保 C:/Users/hp/.ssh/config 文件的所有者和权限设置正确。通常情况下,该文件应该只有您的用户(hp)具有读写权限,其他用户应该没有权限访问该文件!!!

静态库与动态库

静态库与动态库理论

cpp 复制代码
静态库
概念:
程序在编译时会把库文件的二进制代码链接到目标程序中,这种方式成为静态链接
特点:
链接在编译时期完成,执行的时候代码加载速度快
目标的可执行文件比较大,浪费空间
程序的更新和分布不方便,如果某一个静态库更新了,所有使用它的程序都需要重新编译
tree


静态库使用方法
// 编译静态库
g++ -c -o libpublic.a public.cpp

1、g++ -o demo01 demo01.cpp /home/wuca/tools/libpublic.a  // 不推荐使用
2、g++ -o demo01 demo01.cpp -L/home/wuca/tools -lpublic  //  推荐使用
// 示例:
g++ -o keep keep.cpp -L/home/pb/桌面/codekeep/ppp -lpublic

动态库
概念:
编译时不载入二进制代码,程序在运行时候才被载入
如果多个程序中用到了同一个动态库中的函数,那么在内存中只有一份,避免了空间浪费问题。
特点:
程序运行在运行的过程中,需要用到动态库的时候才把动态库的二进制代码载入内存
可以实现进程之间的代码共享,因此动态库也称为共享库
程序升级比较地点但,不需要重新编译程序,只需要更新动态库就行了。
// 编译动态库
g++ -fPIC -shared -o lib 库名.so 源代码文件清单
1、g++ 选项 源代码文件名清单 动态库文件名  // 不推荐使用
2、g++ 选项 源代码文件名清单 -L 库名 -l库文件所在的目录名 //  推荐使用

g++ -fPIC -shared -o libpublic.so public.cpp
g++ -o keep keep.cpp -L/home/pb/桌面/codekeep/ppp -lpublic

运行可执行程序的时候,需要提前设置LD_LIBRARY_PATH环境变量
echo $LD_LIBRARY_PATH // 查看环境变量
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/home/pb/桌面/codekeep/ppp

如果静态库和动态库同时存在,优先使用动态库

静态库与动态库示例代码

文件:public.h

cpp 复制代码
#include<iostream>
void func();

文件:public.cpp

cpp 复制代码
#include<iostream>
#include"/home/pb/桌面/codekeep/ppp/public.h"

// 声明 func 函数
void func(){
    std::cout<<"上山打老虎,老虎打不到!"<<std::endl;
    std::cout<<"code change world!"<<std::endl;
}

文件:keep.cpp

cpp 复制代码
#include"public.h"

using namespace std;
int main(){
    std::cout<<"test static lib"<<std::endl;
    func();
    std::cout<<"I'm a good man!"<<endl;
    return 0;
}

make

makefile

--概念:make是一个强大的实用工具,用于管理项目的编译和链接。make需要一个编译规则文件mekefile,可现实自动化编译。

需要编译很多文件

简单的方法是放在sh文件里

添加可执行权限 或者 sh sh文件名

powershell 复制代码
# 是说明文字 类型于c语言的//
all:libpublic.a libpublic.so     # 指定编译的目标文件 静态库和动态库 如果需要多行书写 可以用\隔开
libpublic.a:public.h public.cpp  # 表示编译目标文件libpublic.a需要文件public.h public.cpp
	g++ -c -o libpublic.a public.cpp # 编译指令

libpublic.so:public.h public.cpp
	g++ -fPIC -shared -o libpublic.so public.cpp

#clean 用于请忽略编译目标文件,仅在make clean才会执行。
clean:
	rm -f libpublic.a libpublic.so

main函数的参数

cpp 复制代码
main函数有三个参数,argc,argv和envp,它的标准写法如下:
int main(int argc,char* argv[], char* envp[]){
return 0;
}
argc 存放了程序参数的个数,包括程序本身
argv 字符串的数组,存放了每个参数的值,包括程序本身
envp 字符串的数组,存放了环境变量,数组的最后一个元素是空
在程序中,如果不关心main()的参数可以省略不写

cout<<"一共有"<<agrc<<"个参数";
argv从0开始输出文件传入参数的个数
setenv(const char *name, const char*value, int overwrite); // 设置环境变量
name 环境变量名 value 环境变量值 overwrite 0 无则建立,存在不替换 1 无则建立,存在替换
char *getenv(const char*name); // 获取环境变量的值

setenv("AA", "aaaa", 0);
cout<<"AA"<<getenv("AA");

gdb调试程序

yum -y install gdb // Ubuntu自己百度

前期准备:

g++ -g -o my_program my_program.cpp // 在终端中输入 gdb 命令启动GDB:

gdb ./my_program // 在终端中输入 gdb 命令启动GDB:

(gdb) break 10 // 在终端中输入 gdb 命令启动GDB:

(gdb) run // 在 GDB 中运行您的程序

在完成调试后,可以输入 quit 命令退出GDB。

一旦程序停在断点处,您可以逐步执行程序,观察程序的执行状态和变量的值。以下是一些常用的命令:

step 或 s:逐步进入函数

next 或 n:逐步执行不进入函数

continue 或 c:继续执行程序直到下一个断点

print 或 p:打印变量的值

backtrace 或 bt:显示当前的函数调用栈

set var 设置变量的值

info breakpoints 查看断点信息

delete breakpoints 取消断点

----gdb调试core文件

如果程序在运行的过程中发生了内存泄漏,会被内核强制终止,提示"段错误(吐核)",内存的状态将保存在core文件中,方便程序员进一步分析。

linux缺省不会生成core文件,需要修改系统参数

调试core文件的步骤如下:

1)用ulimit -a查看当前用户的资源限制参数;

2)用ulimit -c unlimited 把core file size改为unlimited

3)运行程序,产生core文件;不产生core文件 可以直接gdb调试也能看到栈溢出的地方

bt 函数调用栈

示例程序:

cpp 复制代码
#include<iosteam>
#include<string.h>
using namespace std;

int main(){
        char *p = null;
        *p = 1;
        return 0;
} 

---- gdb调试正在运行中的程序

程序不动了,它正在干啥,明明有数据为什么没处理?

程序死了?怎么死的?死在哪里?

从头开始调试很麻烦,并且不一定能模拟出当时的运行环境

步骤 首先获取函数进程编号

ps -ef |grep demo

sudo gdb demo -p 进程号 // 此时程序停止下来 quit后,程序继续执行

使用bt 查看函数调用栈

set var 设置变量的值 点击下一步执行完

(gdb) set var i=1000000

示例代码:

cpp 复制代码
#include<iostream>
#include<unistd.h>
using namespace std;

void printnum(){
        for(int i = 0; i < 1000000; i++){
                std::cout<<i<<endl;
                sleep(1);
        }
}

int main(){
        printnum();
        return 0;
}

linux的时间:

一、linux的时间操作

c++11提供了操作时间的chrono库,从语言级别提供了支持

chrono库屏蔽了时间操作的很多细节,简化了时间操作

如果对linux原生的时间操作不熟悉,很难用好chrono库

unix操作系统根据计算机产生的年代把1970年1月1日作为unix的纪元时间,将经过的秒数用一个整数存放

-、time_t 别名

time_t用于时间类型,它是一个long类型的别名,在<time.h>文件中定义,表示从1970年1月1日0时0分0秒到现在的秒数

typedef long time_t;

二、time()库函数

time()库函数用于获取操作系统的当前时间 头文件<time.h>

声明: time_t time(time_t *tloc);

有两种调用方法 效果一样:

1、time_t now=time(0);

2、time_t now; time(&now);

三、tm结构体

time_t是一个长整型,不符合人类的使用习惯,需要转换成tm结构体,tm结构体在<time.h>中,声明如下:

struct tm{

int tm_year; //年份 其值等于实际年份减去1990

int tm_mon; // 月份:取值区间为[0,11],其中0代表一月,11代表12月

int tm_mday; // 日期:一个月中的日期,取值范围为[0,31]

int tm_hour; // 时取值范围[0,23]

int tm_min; // 分:取值范围为[0,59]

int tm_sec; // 秒:取值范围为[0,59]

int tm_wday; // 星期:取值区间为[0.6],其中0代表星期天,6代表星期六

int tm_yday; // 从每年的1月1日开始算起的天数,取值区间为[0,365]

int tm_isdst; // 夏令时标识符,该字段意义不大

}

四、loacltime()函数

用于把time_t表示的时间转换为tm结构体表示的时间 不是线程安全,localtime_r()是线程安全的

函数声明:

struct tm *localtime(const time_t *timep)

struct tm *localtime_r(const time_t *timep, struct tm *result)

示例代码:

cpp 复制代码
#include<iostream>
#include<unistd.h>
#include<time.h>
using namespace std;

void printnum(){
	time_t now = time(0);
        cout<<now<<endl;
        tm tmnow;
        localtime_r(&now, &tmnow); // 把整数的时间转换成tm结构体

        string stime = to_string(tmnow.tm_year+1900)+"-"
        +to_string(tmnow.tm_mon+1)+"-"
        +to_string(tmnow.tm_mday)+" "
        +to_string(tmnow.tm_hour)+":"
        +to_string(tmnow.tm_min)+":"
        +to_string(tmnow.tm_sec);

        cout<<"stime="<<stime<<endl;
}

int main(){
        printnum();
        return 0;
} 

五、mktime()库函数

mktime()函数的功能与loacltime()函数相反,用于把tm结构体时间转换为time_t时间

函数声明: time_t mktime(struct tm *tm);

cpp 复制代码
```cpp
/*该函数主要用于时间的运算,例如:把2022-10-01 15:30:25加30分钟
思路:1)解析字符串格式的时间,转换成tm结构体;2)用mktime()函数把tm结构体转换成time_t时间;
3)把time_t时间加30*60秒;4)把time_t时间转换成tm结构体;5)把tm结构体抓换成字符串*/

#include <iostream>
#include <iomanip>
#include <sstream>
#include <ctime>

// 解析日期时间字符串
std::tm parseDateTime(const std::string& datetimeStr) {
    std::tm tm = {};
    std::istringstream ss(datetimeStr);
    ss >> std::get_time(&tm, "%Y-%m-%d %H:%M:%S");
    return tm;
}

// 增加分钟并返回新的日期时间字符串
std::string addMinutes(const std::string& datetimeStr, int minutesToAdd) {
    std::tm tm = parseDateTime(datetimeStr);
    // 增加分钟
    std::time_t time = std::mktime(&tm);
    time += minutesToAdd * 60;
    tm = *std::localtime(&time);
    // 格式化为字符串
    std::ostringstream oss;
    oss << std::put_time(&tm, "%Y-%m-%d %H:%M:%S");
    return oss.str();
}

int main() {
    std::string datetimeStr = "2022-10-01 15:30:25";
    int minutesToAdd = 30;
    std::string newDatetimeStr = addMinutes(datetimeStr, minutesToAdd);
    std::cout << "Original datetime: " << datetimeStr << std::endl;
    std::cout << "New datetime after adding 30 minutes: " << newDatetimeStr << std::endl;
    return 0;
}

六、gettimeofday()库函数

用于获取从1970年1月1日到现在的秒和当前秒中已逝去的微秒数,可以用于程序发计时。

包含头文件:<sys/time.h>

函数声明:

cpp 复制代码
int gettimeofday(struct timeval *tv, struct timezone *tz)

struct timeval{
time_t tv_sec; // seconds 1970-1-1 到现在的秒数
suseconds_t tv_usec; //microseconds 当前秒中,已逝去的微秒数
}
struct timezone{  // 在实际开发中,派不上用场
int tz_minuteswest; // minutes west of Greenwich
int tz_dsttime;     // type 0f DST correction
}

七、程序睡眠

如果现需要把程序挂起一段时间,可以使用sleep()和usleep()两个库函数

包含头文件:<unistd.h>

函数声明:

cpp 复制代码
unsigned int sleep(unsigned int seconds) // 形参为秒
int usleep(usecond_t usec) // 形参为微秒

linux目录操作

一、几个简单的目录操作函数

1) 获取当前工作目录

包含头文件:<unistd.h>

char *getcwd(char *buf, size_t size)

char *get_current_dir_name(void)

2) 切换工作目录

包含头文件:<unistd.h>

int chdir(const char *path); // 返回值:0-成功;其它-失败(目录不存在或没有权限)

3) 创建目录

包含头文件:<sys/stat.h>

int mkdir(const char *pathname, mode_t mode);

pathname -目录名

mode -访问权限,如0755, 不要省略前置的0

返回值:0-成功;其它失败(上级目录不存在或没有权限)

4)删除目录

包含头文件:<unistd.h>

int rmdir(const char *path); // 返回值:0-成功;其它-失败(目录不存在或没有权限)

cpp 复制代码
#include<iostream>
#include<unistd.h>

using namespace std;
int main(){
    char path1[256]; // linux系统目录的最大长度是255
    getcwd(path1, 256);
    cout<<"path1="<<path1<<endl;
    char *path2 = get_current_dir_name();
    cout<<"path2="<<path2<<endl;
    free(path2); // 注意释放内存 malloc分配 free释放
    return 0;
}

二、获取目录中文件的列表

文件存放在目录中,在处理文件之前,必须先知道目录中有哪些文件,所以要获取目录中文件的列表

1)包含头文件:<dirent.h>

2)相关库函数

cpp 复制代码
步骤一:用opendir()函数打开目录
DIR *opendir(const char* pathname); // 成功-返回目录的地址,失败-返回空地址
步骤二:用readdir()函数读取目录
struct dirent *readdir(DIR *dirp); // 成功-返回struct dirent结构体的地址,失败-返回空地址
步骤三:用closedir()关闭目录
int closedir(DIR *dirp);

三、数据结构

DIR *目录指针变量名

每次调用readdir(), 函数返回struct dirent的地址,存放了本地读取到的内容。

cpp 复制代码
struct dirent{
long d_ino; // inode number索引节点号
off_t d_off; // offset to this dirent在目录文件中的偏移
unsigned short d_reclen; // length of this d_name 文件名长度
unsigned char d_type;  // the type of d_name文件类型
char d_name[NAME_MAX+1]; // file name文件名,最长255字符
}

重点关注结构体的d_name和d_type成员

d_name -文件名或目录名

d_type -文件类型,有多种取值,最重要的是8和4,8-常规文件;4-目录,其它暂时不关心。注意d_name的数据类型是字符,不可直接显示

cpp 复制代码
#include<iostream>
#include<dirent.h>
using namespace std;

int main(int agrc, char *argv[]){
    if(agrc != 2) {cout<<"using ./demodir 目录名\n"; return -1;}
    DIR *dir; // 定义目录指针
    //打开目录
    if((dir=opendir(argv[1])) == nullptr) return -1;
    
    
    // 用于存放从目录中读取的内容
    struct  dirent *stdinfo = nullptr;
    
    while(1)
    {
        // 读取一项内容并显示出来]
        if((stdinfo = readdir(dir)) == nullptr) break;
        cout<<"文件名="<<stdinfo->d_name<<",文件类型="<<(int)stdinfo->d_type<<endl;
    }
    closedir(dir);
    return 0;
}

目录和文件的更多操作

access() 函数是一个用于检查文件或目录是否存在以及当前进程对其是否具有指定的权限的函数。

头文件: <unistd.h>

1)access函数

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

pathname 是要检查的文件或目录的路径,mode 是一个整数,用于指定检查的权限。

例如,您可以使用 F_OK 来检查文件是否存在,使用 R_OK 来检查是否具有读权限,使用 W_OK 来检查是否具有写权限,使用 X_OK 来检查是否具有执行权限

cpp 复制代码
#include <iostream>
#include <unistd.h>

int main() {
    const char *filename = "example.txt";
    if (access(filename, R_OK) == 0) {
        std::cout << "File is readable" << std::endl;
    } else {
        std::cout << "File is not readable" << std::endl;
    }
    return 0;
}

linux系统错误

在c++程序中,如果调用了库函数,可以通过函数的返回值判断调用是否成功。其实还有一个整形的全局变量errno,存放在函数调用过程中产生的错误代码

如果调用库函数失败,可以通过errno的值来查找原因,这也是调试程序的一个重要方法。

cpp 复制代码
errno在<errno.h>中声明
配合strerror()和perror()两个库函数,可以查看出错的详细信息
char *strerror(int errnum);  // 非线程安全
int strerror_r(int errnum, char *buf, size_t buflen); // 线程安全

1)strerror()库函数
strerror()在<string.h>中声明,用于获取错误码对应的消息描述。

stat库函数

stat() 函数是一个用于获取文件或目录的状态信息的函数,包括文件大小、权限、所有者等。在 C++ 中,您可以使用 <sys/stat.h> 头文件中的 stat() 函数来调用这个功能。

#include <sys/stat.h>

int stat(const char *pathname, struct stat *buf);

cpp 复制代码
#include <iostream>
#include <sys/stat.h>

int main() {
    const char *filename = "example.txt";
    struct stat fileStat;

    if (stat(filename, &fileStat) == 0) {
        std::cout << "File size: " << fileStat.st_size << " bytes" << std::endl;
    } else {
        std::cerr << "Failed to get file status" << std::endl;
    }

    return 0;
}

1)stat结构体

struct stat 是用于存储文件或目录状态信息的结构体。它在 <sys/stat.h> 头文件中定义。

cpp 复制代码
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;       // 最后状态改变时间
};

struct stat结构体成员变量比较多,重点关注st_mode,st_size,st_mtime成员。st_mtime是一个整数表示的时间,需要程序元自己写代码转换格式。

st_mode成员的取值很多,用两个宏来判断:

S_ISREG(st_mode) // 是否为普通文件,如果是,返回真

S_ISDIR(st_mode) // 是否为目录,如果是,返回真

utime() 函数用于修改文件的访问时间和修改时间。在 C/C++ 中,您可以使用 <utime.h> 头文件中的 utime() 函数来调用这个功能。

#include <utime.h>

int utime(const char *filename, const struct utimbuf *times);

cpp 复制代码
#include <iostream>
#include <utime.h>
#include <sys/stat.h>

int main() {
    const char *filename = "example.txt";
    struct utimbuf new_times;

    new_times.actime = time(0);  // 设置新的访问时间为当前时间
    new_times.modtime = time(0); // 设置新的修改时间为当前时间

    if (utime(filename, &new_times) == 0) {
        std::cout << "File access and modification times have been updated" << std::endl;
    } else {
        std::cerr << "Failed to update file times" << std::endl;
    }

    return 0;
}

utime() 函数来将名为 example.txt 的文件的访问时间和修改时间都设置为当前时间。如果函数成功执行,将会返回 0;否则,将会返回 -1

struct utimbuf {

time_t actime; // 访问时间

time_t modtime; // 修改时间

};

四)rename()库函数

cpp 复制代码
#include <stdio.h>

int rename(const char *old_filename, const char *new_filename);
#include <stdio.h>

int main() {
    const char *old_filename = "old_file.txt";
    const char *new_filename = "new_file.txt";

    if (rename(old_filename, new_filename) == 0) {
        printf("File renamed successfully\n");
    } else {
        perror("Failed to rename file");
    }

    return 0;
}

五)remove函数

cpp 复制代码
#include <stdio.h>
int remove(const char *filename);
#include <iostream>
#include <cstdio>
#include <cstdlib> // 用于 EXIT_FAILURE

int main() {
    const char *filename = "file_to_delete.txt";

    if (remove(filename) == 0) {
        std::cout << "File deleted successfully" << std::endl;
    } else {
        std::cerr << "Failed to delete file" << std::endl;
        return EXIT_FAILURE;
    }

    return 0;
}

linux的信号

信号是一种在软件和操作系统级别上用于通知进程发生某些事件的机制。这些事件可以是来自操作系统、其他进程或硬件设备的通知,例如用户输入、定时器到期、错误条件等。

killall

cpp 复制代码
#include <signal.h>
#include <stdio.h>
#include <unistd.h>

void signal_handler(int signum) {
    printf("Received signal: %d\n", signum);
}

int main() {
    signal(SIGINT, signal_handler); // 注册 SIGINT 的处理函数为 signal_handler

    // 进程执行的代码

    return 0;
}

进程可以选择对接收到的信号做出不同的反应

1、忽略信号 2、执行默认操作 3、注册一个信号处理函数来处理信号

sighandler_t 是一个类型定义,通常用于表示信号处理函数的类型。在C语言中,sighandler_t 被定义为一个指向函数的指针类型,该函数接受一个整数作为参数,并返回 void。

通常情况下,当注册一个信号处理函数时,可以使用 sighandler_t 来声明信号处理函数的类型,以确保函数签名的一致性。

cpp 复制代码
#include <stdio.h>
#include <signal.h>

// 定义一个信号处理函数,它的类型为 sighandler_t
void signal_handler(int signum) {
    printf("Received signal: %d\n", signum);
	// signal(signum, SIG_DFL); // 恢复信号的处理方法为默认行为
	alarm(5);
}

int main() {
    // 使用 sighandler_t 类型定义来声明一个变量,用于存储信号处理函数
    sighandler_t handler;

    // 将信号处理函数赋值给 handler
    handler = signal(SIGINT, signal_handler); // 收到了SIGINT的信息,执行signal_handler代码

    // 进程执行的代码

    return 0;
}

常用信号:

SIGINT (2):由终端(通常是用户按下 Ctrl+C)发送给前台进程组的中断信号。默认情况下会终止进程。

SIGILL (4):非法指令信号,表示进程执行了非法的指令。

SIGFPE (8):浮点异常信号,通常表示出现了除以零或其他算术错误。

SIGSEGV (11):段错误信号,表示进程访问了无效的内存地址。

SIGTERM (15):终止信号,用于请求进程正常终止,进程可以捕获该信号并执行清理操作后退出。

SIGCHLD (17):子进程状态改变信号,父进程接收到该信号通知子进程状态发生改变,如子进程终止。

SIGPIPE (13):管道破裂信号,通常在试图向已关闭的管道进行写操作时发出。

SIGHUP (1):挂起信号,通常在与控制终端的连接丢失时发送给进程。

SIGUSR1 (30) 和 SIGUSR2 (31):用户自定义信号,可用于进程间通信或执行特定操作。

SIGKILL (9) 和 SIGSTOP (19):不可捕获的信号,用于强制终止或暂停进程。

alarm 是一个 Unix/Linux 系统提供的系统调用,用于设置一个定时器,当定时器超时时会发送 SIGALRM 信号给调用进程。它通常用于实现对程序的超时控制和定时操作

#include <unistd.h>

alarm(int);

信号的作用

服务程序运行在后台,如果想让中止它,杀掉不是一个好办法,因为进程被杀的时候,是突然死亡,没有安排善后工作

如果向服务程序发送一个信号,服务程序收到信号后,调用一个函数,在函数中编写善后的代码,程序就可以有计划的退出。

向服务程序发送0的信号,可以检测程序是否存活。

6、发送信号

linux操作系统提供了kill和killall命令向程序发送信号,在程序中,可以用kill()库函数向其它进程发送信号。

函数声明:

int kill(pit_t pid, int sig);

kill()函数将参数sig指定的信号给参数pid指定的进程。

cpp 复制代码
#include <sys/types.h>
#include <signal.h>
#include <stdio.h>

int main() {
    pid_t target_pid = 1234;  // 替换成实际的目标进程ID
    int ret = kill(target_pid, SIGTERM);  // 向目标进程发送SIGTERM信号
    if (ret == 0) {
        printf("Signal sent successfully.\n");
    } else {
        perror("Error sending signal");
    }
    return 0;
}

进程终止

有8种方式可以中止进程,其中5中为正常终止,它们是:

1)main()函数用return返回;

2)在任意函数中调用exit()函数;

3)在任意函数中调用_exit()或_Exit()函数;

4)最后一个线程从其启动例程(线程主函数)用return返回;

5)在最后一个线程中调用pthread_exit()返回;

异常终止有3种方式,它们是:

6)调用abort()函数中止

7)接收到一个信号;

8)最后一个线程对取消请求做出响应。

一、进程终止的状态

在main()函数中,return的返回值即终止状态,如果没有return语句或调用exit(),那么该进程的终止状态是0.

在Shell中,查看进程终止的状态为:echo $?

正常终止进程的3个函数(exit(int status)和_Exit(int status)是由ISO C说明的,_exit(int status)是由POSIX说明的) status是进程终止的状态

如果进程被异常终止,终止状态为非0。服务程序的调度、日志和监控

二、资源释放的问题

return表示函数返回,会调用局部对象的析构函数,main()函数中的return还会调用全局对象的析构函数。

exit()表示终止进程,不会调用局部对象的析构函数,只调用全局对象的析构函数。

exit()会执行清理工作,然后退出,_exit()或_Exit()函数直接退出,不会执行清理工作。

三、进程的终止函数

进程可以用atexit()函数登记终止函数(最多32个),这些函数将由exit()自动调用。

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

exit()调用终止函数的顺序与登记时相反。

调用可执行程序

Linux提供了system()函数和exec函数族,在c++程序中,可以执行其它的程序(二进制文件、操作系统命令或shell脚本)

一、system()函数

system() 函数是C语言标准库中的一个函数,用于在程序中调用外部命令。通过 system() 函数,你可以执行像在终端中输入的命令一样的操作

包含头文件:#include <stdlib.h>

int system(const char* command);

返回值:1)如果执行的程序不存在 返回非0。 2)执行程序成功且终止状态为0 返回0。 3)执行程序成功且终止状态非0,返回非0。

cpp 复制代码
#include <stdlib.h>

int main() {
    int status = system("ls -l");  // 在shell中执行ls -l命令
    if (status == -1) {
        perror("Command execution failed");
    } else {
        printf("Command exited with status %d\n", status);
    }
    return 0;
}

二、exec函数族

exec函数族提供了另一种在进程中调用程序(二进制文件或shell脚本)的方法

exec 函数族是用于在C语言程序内部执行新程序的一组函数。当调用 exec 函数时,当前进程的内存映像将被新程序所替代,从而使得新程序开始执行。

注意:新进程的进程编号和原进程相同,但是,新进程取代了原进程的代码段、数据段和堆栈。

exec函数族的声明如下:

cpp 复制代码
int execl(const char *path, const char *arg0, const char *arg1, ..., const char *argn, (char *)0);
int execv(const char *path, char *const argv[]);
int execle(const char *path, const char *arg0, const char *arg1, ..., const char *argn, (char *)0, char *const envp[]);
int execve(const char *path, char *const argv[], char *const envp[]);
int execlp(const char *file, const char *arg0, const char *arg1, ..., const char *argn, (char *)0);
int execvp(const char *file, char *const argv[]);

这些函数的参数包括:

path:要执行的新程序的路径。

arg0, arg1, ..., argn:新程序的命令行参数。

argv:表示新程序的命令行参数的字符串数组。

envp:表示新程序的环境变量的字符串数组。

常用的是execl和execv函数

cpp 复制代码
#include <stdio.h>
#include <unistd.h>

int main() {
    char *args[] = {"ls", "-l", NULL};
    execvp("ls", args);
    // 如果execvp执行成功,下面的代码不会被执行
    perror("execvp");  // 只有在execvp执行失败时才会执行到这里
    return 1;
}

创建进程

一、linux的0、1和2号进程

0号进程(系统进程):所有进程的祖先,它创建了1号和2号进程。

1号进程(systemd):init 进程,用户空间中的第一个进程,负责系统的初始化和进程的管理。

2号进程(kthreadd):负责所有内核线程的调度和管理。

用pstree命令可以查看进程树(yum -y install psmisc)

pstree -p 进程编号

二、进程标识

每个进程都有一个唯一的进程标识符(Process ID,简称PID),用于区分和标识不同的进程。PID是一个非负整数,在系统范围内是唯一的。

一个进程终止后,其进程ID就成了复用的候选者,linux采用延迟复用算法,让新建进程的ID不同于最近终止的进程所使用的ID。

pid_t getpid(void); // 获取当前进程的ID

pid_t getppid(void); // 获取父进程的ID

三、fork()函数

#include <unistd.h>

一个现有的进程可以调用fork()函数创建一个新的进程。

pid_t fork(void);

由fork()创建的新进程被称为子进程。

fork()函数被调用一次,但返回两次。两次返回殴打区别是子进程的返回值是0,而父进程的返回值则是新建子进程的进程ID。

子进程和父进程继续执行fork()之后的代码,子进程是父进程的副本。

子进程获取父进程的数据空间、堆和栈的副本。(子进程是副本,不是共享)

fork()之后,父进程和子进程的执行顺序是不确定的。

四、fork()的两种用法

1)父进程希望复制自己,然后,父进程和子进程分布执行不同的代码。这种用法在网络服务程序中常用,父进程等待客户端的连接请求,当请求到达时,父进程调用fork()让子进程处理

些请求,而父进程则继续等待下一个连接请求。

2)进程要执行另一个程序。这种用法在shell中很常见,子进程从fork()返回后立即调用exec。

cpp 复制代码
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <iostream>
using namespace std;

int main() {
    if(fork() > 0){
        while (true)
        {
            sleep(1);
            cout<<"父进程运行中...\n";
        }
        
    }
    else
    {
        cout<<"子进程开始执行...\n";
        execl("/bin/ls", "/bin/ls","-lt", "/tmp", nullptr);
        cout<<"子进程执行任务结束、退出...\n";
    }
    return 0;
}

让子进程运行的两种方法:

./forktest &

if(fork() > 0) return 0;

五、共享文件

fork()的一个特性是在父进程中打开的文件描述符都会被复制到子进程中,父进程和子进程共享同一个文件偏移量。

如果父进程和子进程写同一描述符指向的文件,但又没有任何形式的同步,那么它们的输出可能会相互混合。

六、vfork()

vfork()函数的调用和返回值与fork()相同,但两者的语义不同。

vfork()函数用于创建一个新进程,而该新进程的目的是exec一个新程序,它不复制父进程的地址空间,因为子进程会立即调用exec,于是也就不会使用父进程的地址空间。如果子进程使用

父进程的地址空间,可能会带来未知的结果。

vfork()与fork()的另一个区别是:vfork()保证子进程先运行,在子进程调用exec或exit()之后父进程才恢复运行。

僵尸进程

僵尸进程是指已经结束执行的子进程,但是其父进程还没有调用 wait() 或 waitpid() 来获取子进程的终止状态信息,导致子进程的进程描述符仍然保存在系统进程表中,这种状态下的进程就被称为僵尸进程。产生僵尸进程的原因是父进程没有充分处理子进程的退出状态。

僵尸进程的产生和存在会带来一些危害:

资源浪费:虽然僵尸进程不占用CPU资源,但它占用着系统的进程表目录项和部分内存空间,如果大量的僵尸进程存在,会浪费系统资源。

进程表满:如果系统中产生了大量的僵尸进程,会导致系统的进程表被占满,从而无法再创建新的进程。

降低系统性能:虽然僵尸进程本身不占用CPU资源,但是系统需要花费时间和资源来维护这些僵尸进程的信息,长期存在大量僵尸进程会影响系统性能。

僵尸进程的避免:

1)子进程退出的时候,内核在向父进程发送SIGCHLD信号,如果父进程用signal(SIGCHLD,SIG_IGN)通知内核,表示自己堆子进程的退出不感兴趣,那么子进程退出后会立即释放数据结构。

2)父进程通过wait()/waitpid()等函数等待子进程结束,在子进程退出之前,父进程将阻塞等待。

cpp 复制代码
#include <sys/types.h>
#include <sys/wait.h>

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

返回值是子进程的编号

使用WIFEXITED(int status)获取子进程退出状态

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

int main() {
    pid_t pid = fork();

    if (pid == -1) {
        // 创建子进程失败
        perror("fork");
        return 1;
    } else if (pid == 0) {
        // 子进程
        printf("Child process: My PID is %d\n", getpid());
        sleep(2); // 模拟子进程在做一些工作
        exit(42); // 子进程退出并带出状态码
    } else {
        // 父进程
        int status;
        printf("Parent process: Waiting for child to terminate...\n");
        pid_t child_pid = wait(&status);
		// pid_t child_pid = waitpid(pid, &status, 0); // 等待特定子进程

        if (child_pid == -1) {
            perror("wait");
            return 1;
        }

        if (WIFEXITED(status)) {
            printf("Parent process: Child terminated with exit status %d\n", WEXITSTATUS(status));
        } else if (WIFSIGNALED(status)) {
            printf("Parent process: Child terminated due to signal %d\n", WTERMSIG(status));
        }
    }

    return 0;
}

3)如果父进程很忙,可以捕获SIGCHLD信号,在信号处理函数中调用wait()/waitpid()。

发送信号

linux操作系统提供了kill和killall命令向进程发送信号,在程序中,可以用kill()函数向其它进程发送信号。

在多进程的服务中,如果子进程收到退出信号,子进程自行退出,如果父进程收到退出信号,应该先向全部的子进程发送退出信号,然后自己再退出。

cpp 复制代码
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <iostream>
#include <stdlib.h>
#include <sys/wait.h>
using namespace std;

void FathEixt(int sig); // 父进程的信号处理函数
void ChldEixt(int sig); // 子进程的信号处理函数

void FathEixt(int sig)
{
    // 以下代码是为了防止信号处理函数在执行的过程中再次被信号中断。
    signal(SIGTERM, SIG_IGN); signal(SIGINT, SIG_IGN);
    cout<<"父进程退出,sig="<<sig<<endl;
    kill(0, SIGTERM);
    // 在这里增加释放资源的代码
    exit(0);
}

// 子进程的信号处理函数
void ChldEixt(int sig){
    // 以下代码是为了防止信号处理函数在执行的过程中再次被信号中断了
    signal(SIGTERM, SIG_IGN); signal(SIGINT, SIG_IGN);
    cout<<"子进程"<<getpid()<<"退出,sig="<<sig<<endl;
    // 在这里增加释放资源的代码
    exit(0);
}

int main() {
    // 忽略全部的信号,不希望被打扰
    for(int ii = 0; ii <= 64; ii++){
        signal(ii, SIG_IGN);
    }
    // 设置信号,在shell状态下可用"kill进程号"或者""ctrl+c"正常终止进程
    // 但请不要用"kill -9 进程号"强行终止
    signal(SIGTERM, FathEixt); signal(SIGINT, FathEixt); // SIGTERM 15 SIGIT 2
    while (true)
    {
        if(fork() > 0) // 父进程的流程
        {
            sleep(5);
            continue;
        }
        else
        {
            // 子进程需要重新设置信号
            signal(SIGTERM, ChldEixt); signal(SIGINT, SIG_IGN); 
            sleep(30);
            while(true){
                cout<<"子进程"<<getpid()<<"正在运行中。\n";
                sleep(3);
                continue;
            }
        }
    }
    return 0;
}

共享内存

多线程共享进程的地址空间,如果多个线程需要访问同一个内存,用全局变量就可以了。

在多进程中,每个进程的地址是独立的,不共享的。如果多个继承需要访问同一块内存,不能用全局变量,只能用共享内存。

共享内存允许多个进程(不要求进程之间有血缘关系)访问同一块内存空间,是多个进程之间共享和传递数据最高效的方式。进程可以将共享内存连接它们自己的地址空间中。

如果某个进程修改了共享内存的数据,其它的进程读到的数据也将会改变。

共享内存没有提供锁机制,也就是说在某一个进程对共享内存进行读写操作的时候,不会操作其他进程对它的读写。如果要对共享内存的读写加锁,可以使用信号量。

Linux中提供了一组函数,用于操作共享内存。

一、shmget函数

shmget()用于创建或获取共享内存

cpp 复制代码
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);

key: 共享内存的键值,是一个整数,一般采用十六进制,例如0x5005。可以通过 ftok() 函数生成,也可以是一个用户定义的常量。

size: 共享内存段的大小(以字节为单位)。如果是获取现有的共享内存段,可以指定为 0。

shmflg: 标志参数,控制共享内存段的创建和权限。常用的标志有:

0666|IPC_CREAT: 如果不存在,则创建一个新的共享内存段。

IPC_EXCL: 与 IPC_CREAT 一起使用,确保不会创建一个已经存在的共享内存段。

返回值:成功返回共享内存的id(一个大于0的整数),失败返回-1(系统内存不足,没有权限)

ipcs -m // 查看共享内存

ipcrm -m 内存编号 // 手工删除共享内存

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <string.h>

int main() {
    key_t key = ftok("shmfile", 65); // 生成唯一键
    if (key == -1) {
        perror("ftok");
        return 1;
    }

    int shmid = shmget(key, 1024, 0666 | IPC_CREAT); // 创建共享内存段
    if (shmid == -1) {
        perror("shmget");
        return 1;
    }

    char *str = (char*) shmat(shmid, NULL, 0); // 将共享内存附加到进程地址空间
    if (str == (char*) -1) {
        perror("shmat");
        return 1;
    }

    printf("Write Data : ");
    fgets(str, 1024, stdin); // 从标准输入读取数据写入共享内存

    printf("Data written in memory: %s\n", str);

    // 分离共享内存
    if (shmdt(str) == -1) {
        perror("shmdt");
        return 1;
    }

    return 0;
}

二、shmat函数

void *shmat(int shmid, const void shmaddr, int shmflg);
shmid: 共享内存段的标识符,由 shmget() 返回。
shmaddr: 指定进程地址空间中要附加共享内存段的位置。通常设为 NULL,让系统选择合适的地址。
shmflg: 标志位,通常填0。
调用成功是返回共享内存起始地址,失败返回(void
)-1。

三、shmdt函数

该函数用于将共享内存从当前进程中分离,相当于shmat函数的反操作

cpp 复制代码
#include <sys/types.h>
#include <sys/shm.h>
int shmdt(const void *shmaddr);

返回值:成功时,返回 0。 失败时,返回 -1,并设置 errno 以指示错误。

四、shmctl函数

该函数用于操作共享内存,最常用的操作是删除共享内存。

int shmctl(int shmid, int cmd, struct shmid_ds *buf);

shmid: 共享内存段的标识符,由 shmget() 返回。

cmd: 控制命令,指定要执行的操作。常用的命令包括:

IPC_STAT: 获取共享内存段的状态信息,并将其存储在 buf 中。

IPC_SET: 设置共享内存段的状态信息,buf 中包含要设置的新值。

IPC_RMID: 从系统中删除共享内存段。

buf: 指向 shmid_ds 结构的指针,用于存储或传递共享内存段的状态信息。

返回值:成功,返回由 cmd 指定的操作相关的值,通常为 0;失败返回 -1,并设置 errno 以指示错误。

注意事项:分配的内存,只能使用内置的数据类型,不能使用stl容器。

循环队列

1)共享内存不能自动扩展,只能采用C++内置的数据类型 2)共享内存不能采用STL容器,也不能使用移动语义。3)如果要实现多进程的消费/生产者模型只能采用循环队列。

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <string.h>
#include<iostream>
using namespace std;

template<typename T, int SIZE>
class CircularQueue {
public:
    T arr[SIZE]; // 存储队列元素的数组
    int front; // 队列头指针
    int rear; // 队列尾指针
    int capacity; // 队列容量
    int count; // 队列当前元素个数

public:
    CircularQueue() : front(0), rear(0), capacity(SIZE), count(0) {
        // 不再需要动态分配内存
    }

    bool isEmpty() const {
        return count == 0;
    }

    bool isFull() const {
        return count == capacity;
    }

    int size() const {
        return count;
    }

    void enqueue(const T& value) {
        if (isFull()) {
            std::cerr << "Queue is full. Cannot enqueue.\n";
            return;
        }
        arr[rear] = value;
        rear = (rear + 1) % capacity;
        count++;
    }

    T dequeue() {
        if (isEmpty()) {
            std::cerr << "Queue is empty. Cannot dequeue.\n";
            return T();
        }
        T value = arr[front];
        front = (front + 1) % capacity;
        count--;
        return value;
    }

    T peek() const {
        if (isEmpty()) {
            std::cerr << "Queue is empty. Cannot peek.\n";
            return T();
        }
        return arr[front];
    }

    void printqueue() const {
        for(int ii = 0; ii < count; ii++ ){
            std::cout<<"arr["<<(front+ii)%capacity<<"]="<<arr[(front+ii)%capacity]<<endl;
        }
    }
};

int main() {
    using queueType = int;
    key_t key = 0x5005; // 生成唯一键

    int shmid = shmget(key, sizeof(CircularQueue<queueType, 5>), 0666 | IPC_CREAT); // 创建共享内存段
    if (shmid == -1) {
        perror("shmget");
        return 1;
    }

    CircularQueue<queueType, 5> *objgril = (CircularQueue<queueType, 5>*) shmat(shmid, NULL, 0); // 将共享内存附加到进程地址空间
    if (objgril == (void*) -1) {
        perror("shmat");
        return 1;
    }

    objgril->capacity = 5;
    std::cout<<objgril->count<<std::endl;
    objgril->printqueue();
    for(queueType i =  1; i <= 3; i++ ){
        objgril->enqueue(i);
    }
    std::cout<<objgril->count<<std::endl;
    objgril->printqueue();

    // 分离共享内存
    if (shmdt(objgril) == -1) {
        perror("shmdt");
        return 1;
    }

    // 删除共享内存
    if(shmctl(shmid, IPC_RMID, 0) == -1){
        printf("shmctl failed\n");
        return -1;
    }

    return 0;
}

信号量的基本概念

信号量本质上是一个非负数的计数器。用于给共享资源建立一个标志,表示该共享资源被占用的情况。

信号量的两种操作:P操作(wait)将信号量的值减1,如果信号量的值为0,将阻塞等待,直到信号量的值大于0,V操作(post)将信号量的值加1,任何时候都不会阻塞。

信号量的使用场景:如果信号约定信号量的取值只是0和1 0资源不可用,1资源可用,可以实现互斥锁。如果约定信号量的取值表示可用资源的数量,可以实现生产/消费者模型。

// 暂时关注原理 用线程来实现pv

cpp 复制代码
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>

class Semaphore {
public:
    Semaphore(int count_ = 0)
        : count(count_) {}

    void P() { // 等同于 wait()
        std::unique_lock<std::mutex> lock(mtx);
        cv.wait(lock, [this]() { return count > 0; });
        --count;
    }

    void V() { // 等同于 signal()
        std::unique_lock<std::mutex> lock(mtx);
        ++count;
        cv.notify_one();
    }

private:
    std::mutex mtx;
    std::condition_variable cv;
    int count;
};

Semaphore semaphore(1); // 初始化信号量,初值为1

void task(const std::string& name) {
    semaphore.P();
    std::cout << "Task " << name << " is entering critical section.\n";
    std::this_thread::sleep_for(std::chrono::seconds(1)); // 模拟任务处理时间
    std::cout << "Task " << name << " is leaving critical section.\n";
    semaphore.V();
}

int main() {
    std::thread t1(task, "A");
    std::thread t2(task, "B");

    t1.join();
    t2.join();

    return 0;
}

网络编程

一、文件描述符的分配规则

/process/进程id/fd目录中,存放了每个进程打开的fd。

linux进程默认打开了三个文件描述符:0-标准输入(键盘),1-标准输出,2-标准错误 分别对应:cin cout cerr.

close(0) 关闭cin close(1) 关闭cout close(2) 关闭cerr

文件描述符的分配规则是:找到最小的,没有被占用的文件描述符。

二、sockaddr_in 和 sockaddr

在网络编程中,sockaddr_in 和 sockaddr 是用于处理网络地址的两种结构体,它们有不同的用途和内容。

struct sockaddr {

unsigned short sa_family; // 地址族(例如 AF_INET、AF_INET6)

char sa_data[14]; // 14 字节的协议地址

};

sa_family:指定地址族,例如 AF_INET 表示 IPv4,AF_INET6 表示 IPv6。

sa_data:存储协议相关的地址信息,对于不同的地址族,其内容会有所不同。

sockaddr_in

sockaddr_in 是专门为 IPv4 地址设计的结构,包含更详细的地址信息。其定义如下:

struct sockaddr_in {

short int sin_family; // 地址族,应该设置为 AF_INET

unsigned short int sin_port; // 端口号(使用网络字节序)

struct in_addr sin_addr; // IP 地址(使用结构体 in_addr)

unsigned char sin_zero[8]; // 填充,为了保持与 struct sockaddr 大小一致

};

sin_family:地址族,应设置为 AF_INET。

sin_port:端口号,必须使用网络字节序(大端字节序),可以使用 htons 函数进行转换。

sin_addr:IP 地址,使用 in_addr 结构表示,通常使用 inet_aton 或 inet_pton 函数来设置。

sin_zero:填充字节,用于保持结构体与 sockaddr 一致的大小,通常置零。

示例

以下是创建和使用 sockaddr_in 结构体的示例代码,并展示如何将其转换为通用的 sockaddr 结构体以便使用:

cpp 复制代码
#include <stdio.h>
#include <string.h>
#include <arpa/inet.h>
#include <sys/socket.h>

int main() {
    int sockfd;
    struct sockaddr_in server_addr;

    // 创建套接字
    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd == -1) {
        perror("socket");
        return 1;
    }

    // 设置服务器地址
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(8080); // 使用网络字节序
    server_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); // 本地主机地址
    memset(server_addr.sin_zero, 0, sizeof(server_addr.sin_zero));

    // 连接到服务器
    if (connect(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {
        perror("connect");
        return 1;
    }

    printf("Connected to server\n");

    // 关闭套接字
    close(sockfd);

    return 0;
}

总结

sockaddr 是一个通用的地址结构,可以表示任何协议族的地址。

sockaddr_in 是一个专门用于 IPv4 地址的结构,包含了具体的 IP 地址和端口信息。

在实际使用中,通常会先填充 sockaddr_in 结构,然后将其转换为 sockaddr 指针传递给诸如 connect、bind 等网络函数。

三、gethostbyname

struct hostent *gethostbyname(const char name);
参数:
name:要查询的主机名 C 字符串。
返回值:如果成功,返回类型为 struct hostent
的指针,该结构包含了主机的详细信息。

如果失败,返回 NULL,并且可以通过设置 errno 来查看错误信息。

cpp 复制代码
#include <stdio.h>
#include <netdb.h>
#include <arpa/inet.h>
#include <errno.h>

int main() {
    const char *hostname = "www.baidu.com";
    struct hostent *host_info;

    host_info = gethostbyname(hostname);
    if (host_info == NULL) {
        perror("gethostbyname");
        return 1;
    }

    printf("Official name: %s\n", host_info->h_name);

    // 打印主机的别名
    char **alias;
    for (alias = host_info->h_aliases; *alias != NULL; alias++) {
        printf("Alias: %s\n", *alias);
    }

    // 打印主机的 IP 地址
    char **ip;
    for (ip = host_info->h_addr_list; *ip != NULL; ip++) {
        printf("IP Address: %s\n", inet_ntoa(*(struct in_addr *)*ip));
    }

    return 0;
}

四、字符串IP与大端序IP的转换

在网络编程中,经常需要进行字符串 IP 地址和大端序(即网络字节序)IP 地址之间的转换。大端序是指数据的高位字节存储在内存的低地址处,而小端序则是相反的。网络字节序通常采用的就是大端序

字符串IP地址到大端序IP地址的转换

使用 inet_pton 函数

#include <stdio.h>

#include <arpa/inet.h>

int main() {

const char *ip_str = "192.168.1.1";

struct in_addr addr;

if (inet_pton(AF_INET, ip_str, &addr) == 1) {
    printf("IP address in network byte order: 0x%x\n", addr.s_addr);
} else {
    perror("inet_pton");
}

return 0;

}

上面的代码演示了如何使用 inet_pton 函数将字符串形式的 IPv4 地址转换为大端序的 in_addr 结构体表示。

大端序IP地址到字符串IP地址的转换

使用 inet_ntop 函数

cpp 复制代码
#include <stdio.h>
#include <arpa/inet.h>

int main() {
    struct in_addr addr;
    addr.s_addr = 0xc0a80101; // 192.168.1.1 的大端序表示

    char ip_str[INET_ADDRSTRLEN];
    if (inet_ntop(AF_INET, &addr, ip_str, INET_ADDRSTRLEN) != NULL) {
        printf("IPv4 address in string form: %s\n", ip_str);
    } else {
        perror("inet_ntop");
    }

    return 0;
}

上面的代码演示了如何使用 inet_ntop 函数将大端序的 IPv4 地址转换为字符串形式。

总结

inet_pton 函数用于将字符串形式的 IP 地址转换为大端序的二进制表示。

inet_ntop 函数用于将大端序的二进制表示的 IP 地址转换为字符串形式。

其它

cpp 复制代码
ls /proc/进程号/fd
netstat -na|less
查找并停止占用该端口的进程。可以使用以下命令(以Linux为例)
sudo lsof -i :端口号
sudo kill -9 PID

网络编程demo

文件:server.cpp

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
 
using namespace std;

int main() {
    int server_fd, new_socket;
    struct sockaddr_in address;
    int opt = 1;
    
    char buffer[1024] = {0};
    const char* greeting = "Hello from server";
    // 创建socker文件描述符
    if((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0){
        perror("socket failed!");
        exit(1);
    }

    //绑定socket到地址和端口
    address.sin_family = AF_INET;
    address.sin_addr.s_addr = INADDR_ANY;
    printf("address:%d\n", INADDR_ANY);
    address.sin_port = htons(8080);
    int addrlen = sizeof(address);

    if(bind(server_fd, (struct sockaddr*)&address, sizeof(address)) < 0){
        perror("bind failed");
        exit(1);
    }

     printf("listen1:\n");
 
    // 监听socket
    if (listen(server_fd, 3) < 0) {
        perror("listen");
        exit(EXIT_FAILURE);
    }
    printf("listen2:\n");
 
    // 接受客户端的连接
    if ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen)) < 0) {
        perror("accept");
        exit(EXIT_FAILURE);
    }
    printf("listen3:\n");
 
    // 接收客户端消息
    if (recv(new_socket, buffer, 1024, 0) <= 0) {
        perror("recv failed");
        exit(EXIT_FAILURE);
    }
 
    printf("Received message: %s\n", buffer);
 
    // 发送消息到客户端
    if (send(new_socket, greeting, strlen(greeting), 0) < 0) {
        perror("send failed");
        exit(EXIT_FAILURE);
    }
    printf("listen5:\n");
 
    // 关闭socket
    close(new_socket);
    close(server_fd);
    return 0;
}

文件:client.cpp

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

int main(int argc, char *argv[]) {
    int sock;
    struct sockaddr_in serv_addr;
    char buffer[1024] = {0};
    
    if (argc < 2) {
        printf("Usage: %s <IP address>\n", argv[0]);
        return 1;
    }
    
    // 创建套接字
    if ((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
        perror("Socket creation error");
        exit(EXIT_FAILURE);
    }
    printf("listen1:\n");
 
    memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_port = htons(8080);
 
    // 将IP地址转换为二进制形式
    if (inet_pton(AF_INET, argv[1], &serv_addr.sin_addr) <= 0) {
        perror("Invalid address / Address not supported");
        exit(EXIT_FAILURE);
    }
    printf("listen2:\n");
 
    // 连接到服务器
    if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) {
        perror("Connection Failed");
        exit(EXIT_FAILURE);
    }
    printf("listen3:\n");

    // 向服务器发送消息
    const char *hello = "Hello from client";
    send(sock, hello, strlen(hello), 0);
    printf("Message sent to server\n");
    printf("listen4:\n");
    
    // 从服务器接收响应
    int valread = read(sock, buffer, 1024);
    if (valread >= 0) {
        printf("Server response: %s\n", buffer);
    } else {
        perror("Read error");
    }
    printf("listen5:\n");

    // 关闭套接字
    close(sock);
    return 0;
}

参考链接:

吴老师课程

相关推荐
会掉头发2 分钟前
Linux进程通信之共享内存
linux·运维·共享内存·进程通信
Chef_Chen5 分钟前
从0开始学习机器学习--Day13--神经网络如何处理复杂非线性函数
神经网络·学习·机器学习
我言秋日胜春朝★5 分钟前
【Linux】冯诺依曼体系、再谈操作系统
linux·运维·服务器
饮啦冰美式36 分钟前
22.04Ubuntu---ROS2使用rclcpp编写节点
linux·运维·ubuntu
wowocpp36 分钟前
ubuntu 22.04 server 安装 和 初始化 LTS
linux·运维·ubuntu
Huaqiwill38 分钟前
Ubuntun搭建并行计算环境
linux·云计算
wclass-zhengge40 分钟前
Netty篇(入门编程)
java·linux·服务器
Lign1731442 分钟前
ubuntu unrar解压 中文文件名异常问题解决
linux·运维·ubuntu
lulu_gh_yu44 分钟前
数据结构之排序补充
c语言·开发语言·数据结构·c++·学习·算法·排序算法