Linux 系统编程入门
将自己之前的学习笔记保存一下,同时也方便大家学习
linux文件权限与目录配置
ls -all 可以查看文件的所有信息
ls --help 或man ls 或info ls可以看到他的基础用法
群组、文件拥有者的理解
群组里面有很多文件的拥有者
改变文件权限与属性
这些修改是在根目录下进行的
在终端输入su
,进入根目录,可是我现在这个Linux进不去根目录,或许是密码错误了
ls -al 显示文件的所有信息
chgrp:改变群组
user是要改成的群组,必须是已经存在的群组
chown:改变所属者
chmod:改变文件权限
权限对目录的重要性,上面说的是文件的权限
对于目录来说,w是相当重要的权限,他可以让使用者删除、更新、新建文件或目录。x在目录中是与【能否进入该目录】有关,r是只能读到文件名,不能获取详细的目录信息
目录的相关操作
复制、删除与移动:cp,rm,mv
文件内容查阅
学习bash
-
命令与文件补全功能: ([tab] 按键的好处)
-
命令别名设定功能: (alias),ls -al显示文件的具体信息,但是此命令有点麻烦,可以给他起个别名,如下
alias lm='ls -al'
- 想要知道/usr/bin 底下有多少以X 为开头的文件吗?使用:『ls -l /usr/bin/X* 』就能够知道啰
gcc编译
通过gcc编译器进行编译,语法规则gcc test.c -o app
,test.c是文件名称,-o后面输出的文件名
运行:./app
gcc工作流程:
先将头文件展开
gcc工作流程
-
-E test.i :进行预处理,预处理后的源码是 .i类型
-
-S:编译指定的源文件,生成.s结尾的文件
-
-o: -o是指定生成的文件名
-
gcc -c :生成.o文件
-
直接进行gcc test.c ,编译器会自动把之前的几步都做了,先进行预处理,汇编,生成可执行程序
-o 参数就是执行test.o ,可以直接执行,然后就生成了代码结果
gcc是编译c程序的
g++是编译c++程序的
静态库命名规则
静态库制作流程:
第一步:
gcc -c xxx.c(源码) //生成源码的.o文件
第二步:
命令:ar rcs libcalc.a add.o sub.o mult.o div.o //得到静态库
静态库使用
学一些linux常用指令:cp -r 递归拷贝 ../
代表上一级目录,下面代码是将lession04的文件夹中的calc和library拷贝到了lession05文件夹中
rm *.o 删除后缀名为.o的文件
静态库的使用
函数的定义在库里,我们在使用静态库时,需要将这些库中的定义与main函数放在同一个目录下
-I:找到指定的目录include,这里面定义的是头文件
-l:库的名称
-L:存放库的目录名称
-o:生成的文件名称
目录结构:
使用库的代码
gcc main.c -I./include -L ./lib/ -l suanshu -o app //顺序可以变化,使用静态库
静态库的制作与使用过程
1. gcc -c xxx.c(源码) //生成源码的.o文件
2. 命令:ar rcs libcalc.a(静态库的名称) add.o sub.o mult.o div.o //得到静态库
3. gcc main.c -I./include -L ./lib/ -l suanshu -o app //顺序可以变化,使用静态库
运行结果:
动态库的制作与使用
动态库的使用与制作
1.先生成.o文件 gcc -c -fpic add.c mult.c sub.c div.c
2.得到动态库 gcc -shared add.c sub.o mult.o div.o -o libcalc.so
//接下来生成可执行文件
//gcc main.c -o main -I ./include -L lib -l calc //但是有问题,需要解决问题,失败的原因来自于动态库的工作原理,动态库需要配置环境变量才可以
用pwd命令显示当前目录路径
将其配置给环境变量,如下语句
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/media/kimber/Data/Linux/lession06/library/lib
用户级别的配置:
输入vim .bashrc按回车,再按shift+: ,到达最后一行,按o往下插入一行
退出vim命令:按esc退出编辑,按shift+: ,到达最后一行,输入wq退出
再输入source .bashrc 配置一下 ldd命令查看
vim ~/.bashrc可以回到家目录下的vim中(不知道为什么这个配置失败了)
cd ~/ 回到主目录
系统配置输入:sudo vim /etc/profile 这种方式可以配置成功
配置好之后输入 source /etc/profile
再到library路径下输入ldd main检查一下,如果出现如下图中的路径,就说明成功了
静态库和动态库工作原理
Xshell常用快捷键


程序编译成可执行程序的过程

先对源文件进行预处理,再进行编译成汇编文件,再链接成可执行文件
在链接阶段,静态库是将其中的代码链接到可执行程序中,可以直接加载静态库中的代码
动态库有所不同,动态库链接的是动态库的名称,然后再根据名字去找文件
静态库的制作过程

使用的时候将静态库以及头文件分发给别人
动态库的制作过程

需要加上-fpic,这样可以生成与位置无关的目标代码,静态库的位置是不变的,动态库的加载位置不确定,是变化的,动态库的文件以及头文件也需要发送给别人,动态库使用的时候,是动态的加载其中的api,先去查找动态库的路径,找到之后再加载到内存空间
静态库的优缺点
库比较小,建议使用静态库,库比较大,使用动态库

动态库的优缺点
makefile
Makefile文件命名和规则
执行:vim Makefile,进入vim中进行Makefile规则指定
app:sub.c add.c mult.c div.c //这是依赖
gcc sub.c add.c mult.c div.c -o app //这是命令,前面必须有Tab键
然后回到终端,输入:make,会生成app(可执行程序)
这是编写的Makefile文件:
后续操作:
简化后的版本,可以参考ppt
再进行简化:
最后的.PHONY是伪目标,这样就不会生成clean文件 这样执行 make clean就可以一直删除了
GDB 调试
- 输入命令,后面要加一个-g
例如:gcc hello.c -o hello -g
-
然后打开可执行程序:gdb hello
-
输入
l
指令从默认位置显示10行代码 -
打上断点:b 10(在第10行处打上断点) i b 查看断点信息
-
运行程序:r 单步调试:n,执行完毕:c
IO函数
文件描述符表的结构
struct files_struct {
/* count为文件表files_struct的引用计数 */
atomic_t count;
/* 文件描述符表 */
/*
为什么有两个fdtable呢?这是内核的一种优化策略。fdt为指针,而fdtab为普通变量。一般情况下,
fdt是指向fdtab的,当需要它的时候,才会真正动态申请内存。因为默认大小的文件表足以应付大多数情况,因此这样就可以避免频繁的内存申请。这也是内核的常用技巧之一。在创建时,使用普通的变量或者数组,然后让指针指向它,作为默认情况使用。只有当进程使用量超过默认值时,才会动态申请内存。
*/
struct fdtable __rcu *fdt;
struct fdtable fdtab;
/*
* written part on a separate cache line in SMP
*/
/* 使用____cacheline_aligned_in_smp可以保证file_lock是以cache
line 对齐的,避免了false sharing */
spinlock_t file_lock ____cacheline_aligned_in_smp;
/* 用于查找下一个空闲的fd */
int next_fd;
/* 保存执行exec需要关闭的文件描述符的位图 */
struct embedded_fd_set close_on_exec_init;
/* 保存打开的文件描述符的位图 */
struct embedded_fd_set open_fds_init;
/* fd_array为一个固定大小的file结构数组。struct file是内核用于文件管理的结构。这里使用默认大小的数组,就是为了可以涵盖大多数情况,避免动态分配 */
struct file __rcu * fd_array[NR_OPEN_DEFAULT];
};
open打开文件
/*
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
// 打开一个已经存在的文件
int open(const char *pathname, int flags);
参数:
- pathname:要打开的文件路径
- flags:对文件的操作权限设置还有其他的设置
O_RDONLY, O_WRONLY, O_RDWR 这三个设置是互斥的
返回值:返回一个新的文件描述符,如果调用失败,返回-1
errno:属于Linux系统函数库,库里面的一个全局变量,记录的是最近的错误号。
#include <stdio.h>
void perror(const char *s);作用:打印errno对应的错误描述
s参数:用户描述,比如hello,最终输出的内容是 hello:xxx(实际的错误描述)
// 创建一个新的文件
int open(const char *pathname, int flags, mode_t mode);
*/
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
int main() {
// 打开一个文件
int fd = open("a.txt", O_RDONLY);
if(fd == -1) {
perror("open");
}
// 读写操作
// 关闭
close(fd);
return 0;
}
open创建新文件
·flags:用于指示打开文件的选项,常用的有O_RDONLY、O_WRONLY和O_RDWR。这三个选项必须有且只能有一个被指定。为什么O_RDWR!=O_RDONLY|O_WRONLY呢?Linux环境中,O_RDONLY被定义为0,O_WRONLY被定义为1,而O_RDWR却被定义为2。之所以有这样违反常规的设计遗留至今,就是为了兼容以前的程序。除了以上三个选项,Linux平台还支持更多的选项,APUE中对此也进行了介绍。
/*
可以输入man 2 open查看函数描述
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
//创建一个新的文件
int open(const char *pathname, int flags, mode_t mode);
参数:
- pathname:要创建的文件的路径
- flags:对文件的操作权限和其他的设置
- 必选项:O_RDONLY, O_WRONLY, O_RDWR 这三个之间是互斥的
- 可选项:O_CREAT 文件不存在,创建新文件
- mode:八进制的数,表示创建出的新的文件的操作权限,比如:0775
最终的权限是:mode & ~umask
0777 -> 111111111
& 0775 -> 111111101
----------------------------
111111101
按位与:0和任何数都为0
umask的作用就是抹去某些权限。
flags参数是一个int类型的数据,占4个字节,32位。
flags 32个位,每一位就是一个标志位。
*/
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
int main() {
// 创建一个新的文件
int fd = open("create.txt", O_RDWR | O_CREAT, 0777);
if(fd == -1) {
perror("open");
}
// 关闭
close(fd);
return 0;
}
read() write()函数
read尝试从fd中读取count个字节到buf中,并返回成功读取的字节数,同时将文件偏移向前移动相同的字节数。返回0的时候则表示已经到了“文件尾”。read还有可能读取比count小的字节数。
write尝试从buf指向的地址,写入count个字节到文件描述符fd中,并返回成功写入的字节数,同时将文件偏移向前移动相同的字节数。write有可能写入比指定count少的字节数。
/*
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
参数:
- fd:文件描述符,open得到的,通过这个文件描述符操作某个文件
- buf:需要读取数据存放的地方,数组的地址(传出参数)
- count:指定的数组的大小
返回值:
- 成功:
>0: 返回实际的读取到的字节数
=0:文件已经读取完了
- 失败:-1 ,并且设置errno
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);
参数:
- fd:文件描述符,open得到的,通过这个文件描述符操作某个文件
- buf:要往磁盘写入的数据,数据内容
- count:要写的数据的实际的大小
返回值:
成功:实际写入的字节数
失败:返回-1,并设置errno
*/
拷贝文件(读写操作)
//复制English.txt文件
/*
思路如下:
1.首先通过open函数打开文件(english.txt)
2.创建一个新的文件(拷贝文件)
3.频繁的读写操作
4.关闭文件
*/
#include<stdio.h>
#include<unistd.h>
#include<sys/stat.h>
#include<fcntl.h>
int main(){
//1.通过open函数打开english.txt文件
int srcfd = open("english.txt",O_RDONLY); //权限是只读
if(srcfd == -1){
perror("open");
return -1;
}
//2.创建一个新的文件(拷贝文件)
int destfd = open("cpy.txt",O_WRONLY | O_CREAT,0664); //权限是只写,如果没有就创建
if(destfd == -1){
perror("open");
return -1;
}
//3.频繁的读写操作
char buf[1024] = {0}; //将数据读到buf这个地址空间
//如果设置的buf空间不够存放读取的数,就返回读到的自节数
//如果返回0,就说明读完了,如果返回-1说明读取失败了
//int len = read(srcfd,buf,sizeof(buf));
int len = 0;
while((len = read(srcfd,buf,sizeof(buf))) > 0){
write(destfd,buf,len); //将读的len写进目标文件
}
//4.关闭文件
close(destfd);
close(srcfd);
return 0;
}
lseek函数
该函数用于将fd的文件偏移量设置为以whence为起点,偏移为offset的位置。其中whence可以为三个值:SEEK_SET、SEEK_CUR和SEEK_END,分别表示为“文件的起始位置”、“文件的当前位置”和“文件的末尾”,而offset的取值正负均可。lseek执行成功后,会返回新的文件偏移量。
注意 当lseek执行成功时,它会返回最终以文件起始位置为起点的偏移位置。如果出错,则返回-1,同时errno被设置为对应的错误值。
也就是说,一般情况下,对于普通文件来说,lseek都是返回非负的整数,但是对于某些设备文件来说,是允许返回负的偏移量。因此要想判断lseek是否真正出错,必须在调用lseek前将errno重置为0,然后再调用lseek,同时检查返回值是否为-1及errno的值。只有当两个同时成立时,才表明lseek真正出错了。因为这里的文件偏移都是内核的概念,所以lseek并不会引起任何真正的I/O操作。
/*
标准C库的函数
#include <stdio.h>
int fseek(FILE *stream, long offset, int whence);
Linux系统函数
#include <sys/types.h>
#include <unistd.h>
off_t lseek(int fd, off_t offset, int whence);
参数:
- fd:文件描述符,通过open得到的,通过这个fd操作某个文件
- offset:偏移量
- whence:
SEEK_SET
设置文件指针的偏移量
SEEK_CUR
设置偏移量:当前位置 + 第二个参数offset的值
SEEK_END
设置偏移量:文件大小 + 第二个参数offset的值
返回值:返回文件指针的位置
作用:
1.移动文件指针到文件头
lseek(fd, 0, SEEK_SET);
2.获取当前文件指针的位置
lseek(fd, 0, SEEK_CUR);
3.获取文件长度
lseek(fd, 0, SEEK_END);
4.拓展文件的长度,当前文件10b, 110b, 增加了100个字节
lseek(fd, 100, SEEK_END)
注意:需要写一次数据
*/
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
int main() {
int fd = open("hello.txt", O_RDWR);
if(fd == -1) {
perror("open");
return -1;
}
// 扩展文件的长度
int ret = lseek(fd, 100, SEEK_END);
if(ret == -1) {
perror("lseek");
return -1;
}
// 写入一个空数据
write(fd, " ", 1);
// 关闭文件
close(fd);
return 0;
}
stat、lstat函数
/*
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
int stat(const char *pathname, struct stat *statbuf);
作用:获取一个文件相关的一些信息
参数:
- pathname:操作的文件的路径
- statbuf:结构体变量,传出参数,用于保存获取到的文件的信息
返回值:
成功:返回0
失败:返回-1 设置errno
int lstat(const char *pathname, struct stat *statbuf);
作用:获取软链接文件的信息
参数:
- pathname:操作的文件的路径
- statbuf:结构体变量,传出参数,用于保存获取到的文件的信息
返回值:
成功:返回0
失败:返回-1 设置errno
*/
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <stdio.h>
int main() {
struct stat statbuf;
int ret = stat("a.txt", &statbuf);
if(ret == -1) {
perror("stat");
return -1;
}
printf("size: %ld\n", statbuf.st_size);
return 0;
}
模拟ls -l的功能
#include<stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include<pwd.h>
#include<grp.h>
#include<time.h>
#include<string.h>
//模拟实现ls -l指令,实现类似下面的文件信息
//-rwxrwxrwx 1 kimber kimber 12 11月 17 13:40 a.txt
int main(int argc,char* argv[]){
//判断输入的参数是否正确
if(argc < 2){
printf("%s filename\n",argv[0]);
return -1;
}
//通过stat函数获取用户传入的文件的信息
struct stat st;
int ret = stat(argv[1], &st); //文件的信息保存在st中
if(ret == -1){
perror("stat");
return -1;
}
//获取文件类型和文件权限,保存在字符串数组中
char perms[11] = {0};
//判断文件类型,一共有7种权限
switch (st.st_mode & S_IFMT)
{
case S_IFLNK:
perms[0] = 'l';
break;
case S_IFDIR:
perms[0] = 'd';
break;
case S_IFREG:
perms[0] = '-';
break;
case S_IFBLK:
perms[0] = 'b';
break;
case S_IFCHR:
perms[0] = 'c';
break;
case S_IFSOCK:
perms[0] = 's';
break;
case S_IFIFO:
perms[0] = 'p';
break;
default:
perms[0] = '?';
break;
}
//文件所有者
perms[1] = (st.st_mode & S_IRUSR) ? 'r' : '-';
perms[2] = (st.st_mode & S_IWUSR) ? 'w' : '-';
perms[3] = (st.st_mode & S_IXUSR) ? 'x' : '-';
//文件所在组权限
perms[4] = (st.st_mode & S_IRGRP) ? 'r' : '-';
perms[5] = (st.st_mode & S_IWGRP) ? 'w' : '-';
perms[6] = (st.st_mode & S_IXGRP) ? 'x' : '-';
perms[7] = (st.st_mode & S_IROTH) ? 'r' : '-';
perms[8] = (st.st_mode & S_IWOTH) ? 'w' : '-';
perms[9] = (st.st_mode & S_IXOTH) ? 'x' : '-';
//硬链接数
int linkNum = st.st_nlink;
//文件所有者
char* fileuser = getpwuid(st.st_uid) ->pw_name;
//文件所在组
char* fileGrp = getgrgid(st.st_gid)->gr_name;
//文件大小
long int fileSize = st.st_size;
//获取修改的时间,ctime将秒数转换成本地的时间,传递的指针
char* time = ctime(&st.st_mtime);
char mtime[512] = {0};
strncpy(mtime, time, strlen(time) - 1);
char buf[1024];
sprintf(buf,"%s %d %s %s %ld %s %s",perms,linkNum,fileuser,fileGrp,fileSize,mtime,argv[1]);
printf("%s\n",buf);
return 0;
}
文件属性操作函数
access函数
/*
输入man 2 access 查看
#include <unistd.h>
int access(const char *pathname, int mode);
作用:判断某个文件是否有某个权限,或者判断文件是否存在
参数:
-pathname:判断文件路径
-mode:
R_OK:判断是否有读权限
W_OK:判断是否有写权限
X_OK:判断是否有可执行权限
F_OK:判断文件是否存在
返回值:成功返回0,失败返回-1
*/
#include <unistd.h>
#include<stdio.h>
int main(){
int ret = access("a.txt",F_OK);
if(ret == -1){
perror("access");
return -1;
}
printf("文件存在!!!\n");
return 0;
}
chmod函数
#include <sys/stat.h>
#include<stdio.h>
/*
#include <sys/stat.h>
int chmod(const char *pathname, mode_t mode);
作用:修改文件的权限
参数:
-pathname:需要修改的文件的路径
-mode:需要修改的权限值,八进制的数
int fchmod(int fd, mode_t mode);
*/
int main(){
int ret = chmod("a.txt",0775);
if(ret == -1){
perror("chmod");
return -1;
}
return 0;
}
truncate函数
/*
#include <unistd.h>
#include <sys/types.h>
int truncate(const char *path, off_t length);
作用:缩减或扩展文件的尺寸至指定的大小
参数:
-path:需要修改的文件的路径
-length:需要最终文件的大小
返回值:成功返回0,失败返回-1
*/
#include <unistd.h>
#include <sys/types.h>
#include<stdio.h>
int main(){
int ret = truncate("b.txt",20);
if(ret == -1){
perror("truncate");
return -1;
}
return 0;
}
目录遍历函数
/*
//打开一个目录
#include <sys/types.h>
#include <dirent.h>
DIR *opendir(const char *name);
参数:
-name:需要打开的目录的名称
返回值:
DIR *类型,理解为目录流
错误返回NULL
//读取目录中的数据
#include <dirent.h>
struct dirent *readdir(DIR *dirp);
参数:
dirp是opendir返回的值
返回值:
struct dirent,代表读取到的文件信息
#include <sys/types.h>
#include <dirent.h>
int closedir(DIR *dirp);
*/
//读取某个目录下所有的普通文件
#include <sys/types.h>
#include <dirent.h>
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
int getFileNum(const char* path);
int main(int argc,char* argv[]){
//没有传入数据就会输出下面的话
if(argc < 2){
printf("%s path\n" , argv[0]);
return -1;
}
int num = getFileNum(argv[1]);
printf("普通文件的个数为: %d\n",num);
return 0;
}
//用于获取目录下所有普通文件的个数
//1.首先打开文件(opendir) 2.读取目录中的数据,用struct dirent*,这里面包括许多目录的信息
int getFileNum(const char* path){
//1.打开目录
DIR* dir = opendir(path);
if(dir == NULL){
perror("opendir");
exit(0);
}
struct dirent* ptr;
int total = 0;
while((ptr = readdir(dir)) != NULL){
//获取名称
char* dname = ptr->d_name;
//忽略掉. 和..
if(strcmp(dname,".") == 0 || strcmp(dname,"..") == 0){
continue;
}
//判断是普通文件还是目录
if(ptr->d_type == DT_DIR){
//目录,需要继续读取这个目录
char newpath[256];
sprintf(newpath,"%s/%s",path,dname);
total += getFileNum(newpath);
}
if(ptr->d_type == DT_REG){
//普通文件
total++;
}
}
closedir(dir);
return total;
}
//结构体和 d_type
struct dirent
{
//此目录进入点的 inode
ino_t d_ino;
//目录文件开头至此目录进入点的位移
off_t d_off;
// d_name的长度 , 不包含 NULL 字符
unsigned short int d_reclen;
// d_name所指的文件类型
unsigned char d_type;
//文件名
char d_name[256];
};
/*
d_type
DT_BLK 块设备
DT_CHR 字符设备
DT_DIR 目录
DT_LNK 软连接
DT_FIFO 管道
DT_REG 普通文件
DT_SOCK 套接字
DT_UNKNOWN 未知
*/
chdir函数
/*
#include <unistd.h>
int chdir(const char *path);
作用:修改进程的工作目录
比如在/home/nowcoder 启动了一个可执行程序a.out, 进程的工作目录 /home/nowcoder
参数:
path : 需要修改的工作目录
#include <unistd.h>
char *getcwd(char *buf, size_t size);
作用:获取当前工作目录
参数:
- buf : 存储的路径,指向的是一个数组(传出参数)
- size: 数组的大小
返回值:
返回的指向的一块内存,这个数据就是第一个参数
*/
#include <unistd.h>
#include <stdio.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
int main() {
// 获取当前的工作目录,将获取的工作目录保存在buf数组中
char buf[128];
getcwd(buf, sizeof(buf));
printf("当前的工作目录是:%s\n", buf);
// 修改工作目录,修改到lession13这个目录下
int ret = chdir("/media/kimber/Data/Linux/lession13/");
if(ret == -1) {
perror("chdir");
return -1;
}
// 创建一个新的文件
int fd = open("chdir.txt", O_CREAT | O_RDWR, 0664);
if(fd == -1) {
perror("open");
return -1;
}
close(fd);
// 获取当前的工作目录
char buf1[128];
getcwd(buf1, sizeof(buf1));
printf("当前的工作目录是:%s\n", buf1);
return 0;
}
mkdir函数
/*
#include <sys/stat.h>
#include <sys/types.h>
int mkdir(const char *pathname, mode_t mode);
作用:创建一个目录
参数:
pathname: 创建的目录的路径
mode: 权限,八进制的数
返回值:
成功返回0, 失败返回-1
*/
#include <sys/stat.h>
#include <sys/types.h>
#include <stdio.h>
int main() {
int ret = mkdir("aaa", 0777);
if(ret == -1) {
perror("mkdir");
return -1;
}
return 0;
}
dup\dup2函数
/*
#include <unistd.h>
int dup(int oldfd);
作用:复制新的文件描述符,新的文件描述符与旧的文件描述符指向同一个文件
fd=3,int fd1 = dup(fd);
fd指向的是a.txt,fd1也是指向a.txt
从空闲的文件描述符表中找一个最小的,作为新的拷贝文件描述符
*/
#include <unistd.h>
#include<stdio.h>
#include<fcntl.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<string.h>
int main(){
int fd = open("a.txt",O_CREAT | O_RDWR,0664);
int fd1 = dup(fd);
if(fd1 == -1){
perror("open");
return -1;
}
printf("fd: %d , fd1 : %d\n" ,fd,fd1); //输出fd: 3 , fd1 : 4
close(fd);
char* str = "hello,world";
int ret = write(fd1,str,strlen(str));
if(ret == -1){
perror("write");
return -1;
}
close(fd1);
return 0;
}
/*
#include <unistd.h>
int dup2(int oldfd, int newfd);
作用:重定向文件描述符
oldfd指向a.txt, newfd指向b.txt
调用函数成功后:newfd和b.txt做close,newfd指向了a.txt
oldfd必须是一个有效的文件描述符
oldfd和newfd值相同,相当于什么都没做
*/
#include <unistd.h>
#include<stdio.h>
#include<fcntl.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<string.h>
int main(){
//创建一个新的文件,返回一个文件描述符
int fd = open("1.txt",O_RDWR | O_CREAT,0664);
//判断是否创建成功
if(fd == -1){
perror("open");
return -1;
}
int fd1 = open("2.txt",O_RDWR | O_CREAT,0664);
if(fd1 == -1){
perror("open");
return -1;
}
printf("fd: %d , fd1 : %d\n" ,fd,fd1);
int fd2 = dup2(fd,fd1); //现在fd1指向了fd所代表的文件
if(fd2 == -1){
perror("dup2");
return -1;
}
//通过fd1去写数据,实际操作的是1.txt ,而不是 2.txt
char* str = "hello world";
int len = write(fd1,str,strlen(str));
if(len == -1){
perror("write");
return -1;
}
printf("fd: %d , fd1 : %d, fd2 : %d\n" ,fd, fd1, fd2);
close(fd);
close(fd1);
return 0;
}
fcntl函数
/*
#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd, ...);
参数:
fd : 表示需要操作的文件描述符
cmd: 表示对文件描述符进行如何操作
- F_DUPFD : 复制文件描述符,复制的是第一个参数fd,得到一个新的文件描述符(返回值)
int ret = fcntl(fd, F_DUPFD);
- F_GETFL : 获取指定的文件描述符文件状态flag
获取的flag和我们通过open函数传递的flag是一个东西。
- F_SETFL : 设置文件描述符文件状态flag
必选项:O_RDONLY, O_WRONLY, O_RDWR 不可以被修改
可选性:O_APPEND, O)NONBLOCK
O_APPEND 表示追加数据
NONBLOK 设置成非阻塞
阻塞和非阻塞:描述的是函数调用的行为。
*/
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
int main() {
// 1.复制文件描述符
// int fd = open("1.txt", O_RDONLY);
// int ret = fcntl(fd, F_DUPFD);
// 2.修改或者获取文件状态flag
int fd = open("1.txt", O_RDWR);
if(fd == -1) {
perror("open");
return -1;
}
// 获取文件描述符状态flag
int flag = fcntl(fd, F_GETFL);
if(flag == -1) {
perror("fcntl");
return -1;
}
flag |= O_APPEND; // flag = flag | O_APPEND
// 修改文件描述符状态的flag,给flag加入O_APPEND这个标记
int ret = fcntl(fd, F_SETFL, flag);
if(ret == -1) {
perror("fcntl");
return -1;
}
char * str = "nihao";
write(fd, str, strlen(str));
close(fd);
return 0;
}
Linux多进程开发
fork函数
/*
#include <sys/types.h>
#include <unistd.h>
pid_t fork(void);
函数的作用:创建子进程
返回值:
fork()返回值会返回两次,一次在父进程中,一次在子进程中
在父进程中返回创建的子进程的ID
在子进程中返回0
如何区分父进程和子进程:通过fork的返回值
在父进程中返回-1,表示创建子进程失败,并设置errno
*/
#include <sys/types.h>
#include <unistd.h>
#include<stdio.h>
int main(){
//创建子进程
pid_t pid = fork();
//判断是父进程还是子进程
if(pid > 0){
//如果大于0,返回的是父进程中创建的子进程的进程号
printf("pid : %d\n" ,pid); //打印的是子进程的ID
printf("i am parent process,pid: %d,ppid: %d\n",getpid(),getppid());
}else if(pid == 0){
//当前是子进程,pid是当前进程的id,ppid是父进程的id
printf("i am child process,%d,ppid: %d\n",getpid(),getppid());
}
//父子进程共享的代码,二者交替执行
for(int i = 0; i < 5; i++){
printf("i : %d , pid: %d\n",i ,getpid());
sleep(1);
}
return 0;
}
父子进程具体执行细节,子进程会clone父进程,clone出来一个新的虚拟地址空间,用户区数据都一样,但是pid不同
注意1:在栈空间存放的是fork函数的返回值,父进程的栈空间存的是10089,子进程存放的是返回值0.
此后各自进行操作的互不干扰。
注意2: 资源的复制是在写入时才会进行的,在此之前,只有以只读方式共享。fork之后父子进程共享文件。如果要写入数据,就互不干扰,父进程要修改会开辟一块新的地址空间存入修改的数据,子进程同样如此
父子进程总结
父子进程之间的关系:
区别:
1.fork()函数的返回值不同
父进程中:>0 返回子进程的pid
子进程中:=0
2.pcb中的一些数据不同
当前进程的id pid
当前进程的父进程的id ppid
信号集
共同点:
某些状态下,子进程别创建出来,还没有执行任何的写数据的操作
-用户区的数据
-文件描述符表
父子进程对于变量是否共享?
-刚开始的时候是一样的,共享的。如果修改了数据,就不共享了
-刚开始共享(子进程被创建,两个进程没有做任何写的操作),
读时共享,写时拷贝
GDB多进程调试
使用GDB调试的时候,GDB默认只能追踪一个进程,可以在fork函数调用之前,通过指令设置GDB调试工具跟踪父进程或者跟踪子进程,默认跟踪父进程
设置调试父进程或者子进程:set follow fork mode [parent (默认 )|child]
例如:set follow-fork-mode child
这样设置完成后,程序会停在子进程的断点处,将父进程执行完毕,父进程如果也设置了断点,则不会起作用。
设置调试模式:set detach-on-fork [on | off]。默认为 on ,表示调试当前进程的时候,其它的进程继续运行,如果为 off ,调试当前进程的时候,其它进程被 GDB 挂起。
查看调试的进程:info inferiors
切换当前调试的进程:inferior id
使进程脱离GDB 调试: detach inferiors id
使用查看的命令,可以看到当前的进程信息,带*指的是当前调试的进程,可以通过上面提到的切换命令进行调试进程的切换
如下所示,进程调试已经成功切换
exec函数族
工作原理:
- exec 函数族的作用是根据指定的文件名找到可执行文件,并用它来取代调用进程的内容,换句话说,就是在调用进程内部执行一个可执行文件。
- exec 函数族的函数执行成功后不会返回,因为调用进程的实体,包括代码段,数据段和堆栈等都已经被新的内容取代,只留下进程 ID 等一些表面上的信息仍保持原样,类似于金蝉脱壳,看上去还是旧的躯壳,却已经注入了新的灵魂。只有调用失败了,它们才会返回 1 ,从原程序的调用点接着往下执行。
- 不会创建新的进程,而是替换用户区的数据,
- 执行代码:gcc 文件名 -o 输出名 (不需要加 -c )
exec函数族:
int execl(const char *path, const char arg, …/ (char *) NULL */);
int execlp(const char *file, const char arg, … / (char *) NULL */);
int execle(const char *path, const char arg, …/, (char *) NULL, char *const envp[] */);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[], char *const envp[]);
int execve(const char *filename, char *const argv[], char *const envp[]);
l(list) 参数地址列表,以空指针结尾
v(vector) 存有各参数地址的指针数组的地址
p(path) 按 PATH 环境变量指定的目录搜索可执行文件
e(environment) 存有环境变量字符串地址的指针数组的地址
execl函数
/*
#include <unistd.h>
int execl(const char *path, const char *arg, ...);
- 参数:
- path:需要指定的执行的文件的路径或者名称
a.out /home/nowcoder/a.out 推荐使用绝对路径
./a.out hello world
- arg:是执行可执行文件所需要的参数列表
第一个参数一般没有什么作用,为了方便,一般写的是执行的程序的名称
从第二个参数开始往后,就是程序执行所需要的的参数列表。
参数最后需要以NULL结束(哨兵)
- 返回值:
只有当调用失败,才会有返回值,返回-1,并且设置errno
如果调用成功,没有返回值。
*/
#include <unistd.h>
#include <stdio.h>
int main() {
// 创建一个子进程,在子进程中执行exec函数族中的函数
pid_t pid = fork();
if(pid > 0) {
// 父进程
printf("i am parent process, pid : %d\n",getpid());
sleep(1);
}else if(pid == 0) {
// 子进程
// execl("hello","hello",NULL);
execl("/bin/ps", "ps", "aux", NULL);
perror("execl");
printf("i am child process, pid : %d\n", getpid());
}
for(int i = 0; i < 3; i++) {
printf("i = %d, pid = %d\n", i, getpid());
}
return 0;
}
execlp函数
/*
#include <unistd.h>
int execlp(const char *file, const char *arg, ... );
- 会到环境变量中查找指定的可执行文件,如果找到了就执行,找不到就执行不成功。
- 参数:
- file:需要执行的可执行文件的文件名
a.out
ps
- arg:是执行可执行文件所需要的参数列表
第一个参数一般没有什么作用,为了方便,一般写的是执行的程序的名称
从第二个参数开始往后,就是程序执行所需要的的参数列表。
参数最后需要以NULL结束(哨兵)
- 返回值:
只有当调用失败,才会有返回值,返回-1,并且设置errno
如果调用成功,没有返回值。
int execv(const char *path, char *const argv[]);
argv是需要的参数的一个字符串数组
char * argv[] = {"ps", "aux", NULL};
execv("/bin/ps", argv);
int execve(const char *filename, char *const argv[], char *const envp[]);
char * envp[] = {"/home/nowcoder", "/home/bbb", "/home/aaa"};
*/
#include <unistd.h>
#include <stdio.h>
int main() {
// 创建一个子进程,在子进程中执行exec函数族中的函数
pid_t pid = fork();
if(pid > 0) {
// 父进程
printf("i am parent process, pid : %d\n",getpid());
sleep(1);
}else if(pid == 0) {
// 子进程
execlp("ps", "ps", "aux", NULL);
printf("i am child process, pid : %d\n", getpid());
}
for(int i = 0; i < 3; i++) {
printf("i = %d, pid = %d\n", i, getpid());
}
return 0;
}
进程退出,孤儿进程,僵尸进程
进程退出
标准c库的函数在执行exit()会自动刷新缓冲区,Linux系统的函数_exit()不会自动刷新
代码演示:
/*
#include <stdlib.h>
void exit(int status);
#include <unistd.h>
void _exit(int status);
status参数:是进程退出时的一个状态信息。父进程回收子进程资源的时候可以获取到。
*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main() {
printf("hello\n"); //加了"\n",会自动刷新缓冲区
printf("world");
//exit(0); //打印hello world
_exit(0); //只打印hello
return 0;
}
孤儿进程
- 父进程运行结束,但子进程还在运行(未运行结束),这样的子进程就称为孤儿进程(Orphan Process )。
- 每当出现一个孤儿进程的时候,内核就把孤儿进程的父进程设置为 init ,而 init进程会循环地 wait() 它的已经退出的子进程。这样,当一个孤儿进程凄凉地结束了其生命周期的时候, init 进程就会代表党和政府出面处理它的一切善后工作。
- 因此孤儿进程并不会有什么危害。
- 子进程的父进程死亡后,但是子进程还没有结束,这时进程号为1的进程会担任此子进程的父进程,对子进程进行一些善后的工作,例如回收资源。
僵尸进程
- 每个进程结束之后 , 都会释放自己地址空间中的用户区数据,内核区的 PCB 没有办法自己释放掉,需要父进程去释放。
- 进程终止时,父进程尚未回收,子进程残留资源( PCB )存放于内核中,变成僵尸(Zombie )进程。
- 什么时候会产生僵尸进程,父进程一直在运行,子进程提前结束运行,父进程不能回收子进程的资源。
- 僵尸进程不能被 kill 9 杀死,这样就会导致一个问题,如果父进程不调用 wait()或 waitpid() 的话,那么保留的那段信息就不会释放,其进程号就会一直被占用,但是系统所能使用的进程号是有限的,如果大量的产生僵尸进程,将因为没有可用的进程号而导致系统不能产生新的进程,此即为僵尸进程的危害,应当避免。
wait函数
/*
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *wstatus);
功能:等待任意一个子进程结束,如果任意一个子进程结束了,次函数会回收子进程
参数:int *wstatus
进程退出时的状态信息,传入的是一个int类型的地址,是传出参数
子进程退出的状态写到参数int里面
返回值:
-成功:返回被回收的子进程的id
-失败:返回-1(所有的子进程结束,或者调用函数出错)
调用wait函数的进程会被挂起(阻塞),直到它的一个子进程退出或者收到一个不能被忽略的信号
这时候才会被唤醒(继续往下执行)
如果没有子进程了,函数立刻返回,返回-1;如果子进程都已经结束,也会返回-1.
*/
#include <sys/types.h>
#include <sys/wait.h>
#include<stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main(){
//有一个父进程,创建5个子进程
pid_t pid;
//创建5个子进程
for(int i = 0; i < 5; i++){
pid = fork();
if(pid ==0){
break;
}
}
if(pid > 0){ //父进程
while(1){ //这样会产生僵尸进程,父进程一直结束不了
//子进程结束之后没有父进程进行资源的回收,所以需要采取手段进行子进程资源回收
printf("parent, pid = %d\n" , getpid());
int ret = wait(NULL);
if(ret == -1) {
break;
}
printf("child die, pid = %d\n", ret);
sleep(1);
}
}else if(pid == 0){
//子进程
while(1){
printf("child, pid = %d\n" , getpid());
sleep(1);
}
}
return 0;
}
waitpid函数
/*
#include <sys/types.h>
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int *wstatus, int options);
功能:回收指定进程号的子进程,可以设置是否阻塞。
参数:
- pid:
pid > 0 : 某个子进程的pid
pid = 0 : 回收当前进程组的所有子进程
pid = -1 : 回收所有的子进程,相当于 wait() (最常用)
pid < -1 : 某个进程组的组id的绝对值,回收指定进程组中的子进程
- options:设置阻塞或者非阻塞
0 : 阻塞
WNOHANG : 非阻塞
- 返回值:
> 0 : 返回子进程的id
= 0 : 只有options=WNOHANG时会返回, 表示还有子进程活着
= -1 :错误,或者没有子进程了
*/
#include <sys/types.h>
#include <sys/wait.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main() {
// 有一个父进程,创建5个子进程(兄弟)
pid_t pid;
// 创建5个子进程
for(int i = 0; i < 5; i++) {
pid = fork();
if(pid == 0) {
break;
}
}
if(pid > 0) {
// 父进程
while(1) {
printf("parent, pid = %d\n", getpid());
sleep(1);
int st;
// int ret = waitpid(-1, &st, 0);//阻塞的情况
int ret = waitpid(-1, &st, WNOHANG); //非阻塞的情况
//非阻塞的父进程不用被挂起,可以做一些别的业务逻辑
if(ret == -1) {
break;
} else if(ret == 0) {
// 说明还有子进程存在
continue;
} else if(ret > 0) { //返回在
if(WIFEXITED(st)) {
// 是不是正常退出
printf("退出的状态码:%d\n", WEXITSTATUS(st));
}
if(WIFSIGNALED(st)) {
// 是不是异常终止
printf("被哪个信号干掉了:%d\n", WTERMSIG(st));
}
printf("child die, pid = %d\n", ret);
}
}
} else if (pid == 0){
// 子进程
while(1) {
printf("child, pid = %d\n",getpid());
sleep(1);
}
exit(0);
}
return 0;
}
进程间通信(面试经常问)
通信方式:
匿名管道
管道的特点:
-
管道其实是一个在内核内存中维护的缓冲器,这个缓冲器的存储能力是有限的,不同的操作系统大小不一定相同。
-
管道拥有文件的特质:读操作、写操作,匿名管道没有文件实体,有名管道有文件实体,但不存储数据。可以按照操作文件的方式对管道进行操作。
-
一个管道是一个字节流,使用管道时不存在消息或者消息边界的概念,从管道读取数据的进程可以读取任意大小的数据块,而不管写入进程写入管道的数据块的大小是多少。
-
通过管道传递的数据是顺序的,从管道中读取出来的字节的顺序和它们被写入管道的顺序是完全一样的。
-
在管道中的数据的传递方向是单向的,一端用于写入,一端用于读取,管道是半双工的。
-
从管道读数据是一次性操作,数据一旦被读走,它就从管道中被抛弃,释放空间以便写更多的数据,在管道中无法使用 lseek() 来随机的访问数据。
-
匿名管道只能在具有公共祖先的进程(父进程与子进程,或者两个兄弟进程,具有亲缘关系)之间使用。
匿名管道的使用:
-
创建匿名管道
#include <unistd.h>
int pipe(int pipefd[2]);
-
查看管道缓冲大小命令:
ulimit -a
-
查看管道缓冲大小函数:
#include <unistd.h>
long fpathconf(int fd, int name);
pipe的使用
/*
#include <unistd.h>
int pipe(int pipefd[2]);
功能:创建一个匿名管道,用来进程间通信。
参数:int pipefd[2] 这个数组是一个传出参数。
pipefd[0] 对应的是管道的读端
pipefd[1] 对应的是管道的写端
返回值:
成功 0
失败 -1
管道默认是阻塞的:如果管道中没有数据,read阻塞,如果管道满了,write阻塞
注意:匿名管道只能用于具有关系的进程之间的通信(父子进程,兄弟进程)
*/
// 子进程发送数据给父进程,父进程读取到数据输出
#include <unistd.h>
#include <sys/types.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main() {
// 在fork之前创建管道
int pipefd[2];
int ret = pipe(pipefd);
if(ret == -1) {
perror("pipe");
exit(0);
}
// 创建子进程
pid_t pid = fork();
if(pid > 0) {
// 父进程
printf("i am parent process, pid : %d\n", getpid());
// 关闭写端
close(pipefd[1]);
// 从管道的读取端读取数据
char buf[1024] = {0};
while(1) {
int len = read(pipefd[0], buf, sizeof(buf));
printf("parent recv : %s, pid : %d\n", buf, getpid());
// 向管道中写入数据
//char * str = "hello,i am parent";
//write(pipefd[1], str, strlen(str));
//sleep(1);
}
} else if(pid == 0){
// 子进程
printf("i am child process, pid : %d\n", getpid());
// 关闭读端
close(pipefd[0]);
char buf[1024] = {0};
while(1) {
// 向管道中写入数据
char * str = "hello,i am child";
write(pipefd[1], str, strlen(str));
//sleep(1);
// int len = read(pipefd[0], buf, sizeof(buf));
// printf("child recv : %s, pid : %d\n", buf, getpid());
// bzero(buf, 1024);
}
}
return 0;
}
父子进程间通信
/*
实现 ps aux | grep xxx 父子进程间通信
子进程: ps aux, 子进程结束后,将数据发送给父进程
父进程:获取到数据,过滤
pipe()
execlp()
子进程将标准输出 stdout_fileno 重定向到管道的写端。 dup2
*/
#include <unistd.h>
#include <sys/types.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <wait.h>
int main() {
// 创建一个管道
int fd[2];
int ret = pipe(fd);
if(ret == -1) {
perror("pipe");
exit(0);
}
// 创建子进程
pid_t pid = fork();
if(pid > 0) {
// 父进程
// 关闭写端
close(fd[1]);
// 从管道中读取
char buf[1024] = {0};
int len = -1;
while((len = read(fd[0], buf, sizeof(buf) - 1)) > 0) {
// 过滤数据输出
printf("%s", buf);
memset(buf, 0, 1024);
}
wait(NULL);
} else if(pid == 0) {
// 子进程
// 关闭读端
close(fd[0]);
// 文件描述符的重定向 stdout_fileno -> fd[1]
dup2(fd[1], STDOUT_FILENO);
// 执行 ps aux
execlp("ps", "ps", "aux", NULL);
perror("execlp");
exit(0);
} else {
perror("fork");
exit(0);
}
return 0;
}
管道读写特点和管道设为非阻塞
管道的读写特点:
使用管道时,需要注意以下几种特殊的情况(假设都是阻塞I/O操作)
1.所有的指向管道写端的文件描述符都关闭了(管道写端引用计数为0),有进程从管道的读端读数据,那么管道中剩余的数据被读取以后,再次read会返回0,就像读到文件末尾一样。
2.如果有指向管道写端的文件描述符没有关闭(管道的写端引用计数大于0),而持有管道写端的进程也没有往管道中写数据,这个时候有进程从管道中读取数据,那么管道中剩余的数据被读取后,再次read会阻塞,直到管道中有数据可以读了才读取数据并返回。
3.如果所有指向管道读端的文件描述符都关闭了(管道的读端引用计数为0),这个时候有进程向管道中写数据,那么该进程会收到一个信号SIGPIPE, 通常会导致进程异常终止。
4.如果有指向管道读端的文件描述符没有关闭(管道的读端引用计数大于0),而持有管道读端的进程也没有从管道中读数据,这时有进程向管道中写数据,那么在管道被写满的时候再次write会阻塞,直到管道中有空位置才能再次写入数据并返回。
总结:
读管道:
管道中有数据,read返回实际读到的字节数。
管道中无数据:
写端被全部关闭,read返回0(相当于读到文件的末尾)
写端没有完全关闭,read阻塞等待
写管道:
管道读端全部被关闭,进程异常终止(进程收到SIGPIPE信号)
管道读端没有全部关闭:
管道已满,write阻塞
管道没有满,write将数据写入,并返回实际写入的字节数
有名管道(fifo)
write
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <stdlib.h>
#include <unistd.h>
#include<fcntl.h>
#include<string.h>
/*
有名管道的注意事项:
1.一个为只读而打开一个管道的进程会阻塞,直到另外一个进程为只写打开管道
2.一个为只写而打开一个管道的进程会阻塞,直到另外一个进程为只读打开管道
读管道:
管道中有数据,read返回实际读到的字节数
管道中无数据:
管道写端被全部关闭,read返回0,(相当于读到文件末尾)
写端没有全部被关闭,read阻塞等待
写管道:
管道读端被全部关闭,进行异常终止(收到一个SIGPIPE信号)
管道读端没有全部关闭:
管道已经满了,write会阻塞
管道没有满,write将数据写入,并返回实际写入的字节数。
*/
int main(){
//1.判断管道是否存在,不存在就创建管道
int ret = access("test2", F_OK);
if(ret == -1){
printf("管道不存在,创建管道!");
ret = mkfifo("test2",0664);
if(ret == -1){
perror("mkfifo");
exit(0);
}
}
//2.以只写的方式打开管道,如果没有读端进行读取,管道会阻塞
int fd = open("test2", O_WRONLY); //此函数返回一个文件描述符
if(fd == -1){
perror("open");
exit(0);
}
//3.往管道里写数据
for(int i = 0; i < 100; i++){
char buf[1024];
sprintf(buf, "hello, %d\n", i); //sprintf是打印到字符串中
printf("write data: %s\n", buf);
write(fd, buf,strlen(buf));
sleep(1);
}
close(fd);
return 0;
}
read()
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <stdlib.h>
#include <unistd.h>
#include<fcntl.h>
#include<string.h>
int main(){
//打开管道文件,以只读的方式打开管道
int fd = open("test2",O_RDONLY);
if(fd == -1){
perror("open");
exit(0);
}
//读数据,以循环的方式读数据
while(1){
char buf[1024] = {0};
int len = read(fd, buf, sizeof(buf));
if(len == 0){ // 写端被全部关闭,read返回0(相当于读到文件的末尾)
printf("写端断开连接了...\n");
break;
}
printf("recv buf : %s\n", buf);
}
close(fd);
return 0;
}
使用内存映射实现进程间通信
什么是内存映射
内存映射(Memory Mapping)是一种技术,可以将一个文件或其他对象(例如共享内存区域)映射到进程的虚拟地址空间中,使得进程可以像访问内存一样访问文件。这意味着,进程可以通过指针访问文件的内容,而无需使用系统调用来读取或写入文件。
内存映射的优点在于,它可以提高 I/O 性能,因为进程可以直接访问文件的内容,而无需拷贝数据。此外,内存映射还可以提供访问同步和修改检测等功能,使得进程可以共享文件内容。
内存映射通常由操作系统提供,在许多编程语言(包括 C、C++、Java 和 Python)中都有内存映射的支持。
c语言中的内存映射( mmap)
在 C 语言中,可以使用 mmap 函数来实现内存映射进程间通信。
首先,需要使用 open 函数打开文件,并使用 fstat 函数获取文件的大小。然后,可以使用 mmap 函数将文件映射到进程的虚拟地址空间中。mmap 函数需要传入以下参数:
- addr:指定映射区域的起始地址。如果为 NULL,则由系统决定映射区域的起始地址。
- length:指定映射区域的长度。
- prot:指定映射区域的访问权限。可以是 PROT_READ、PROT_WRITE 或 PROT_EXEC。
- flags:指定映射区域的类型。可以是 MAP_SHARED 或 MAP_PRIVATE。
- fd:指定要映射的文件描述符。
- offset:指定要映射的文件偏移量。
如果 mmap 调用成功,则会返回映射区域的起始地址。如果失败,则返回 MAP_FAILED。
下面是一个使用 mmap 实现内存映射进程间通信的示例代码:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>
#include<stdio.h>
#define FILE_NAME "test.txt"
int main() {
// 打开文件
int fd = open(FILE_NAME, O_RDWR);
if (fd == -1) {
perror("open failed");
return 1;
}
// 获取文件大小
struct stat st;
fstat(fd, &st);
size_t size = st.st_size;
// 将文件映射到进程的虚拟地址空间
char* addr = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (addr == MAP_FAILED) {
perror("mmap failed");
return 1;
}
// 访问映射区域
for (size_t i = 0; i < size; i++) {
addr[i] = 'A';
}
// 释放映射区域
munmap(addr, size);
close(fd);
return 0;
}
在上面的代码中,我们首先打开了一个文件,然后使用 fstat 函数获取了文件的大小。接着,我们使用 mmap 函数将文件映射到进程的虚拟地址空间中,并访问映射区域。最后,我们使用 munmap 函数释放映射区域,并关闭文件描述符。在使用 mmap 函数之前,需要先调用 open 函数打开文件。如果文件打开失败,则可以使用 perror 函数输出错误信息。 另外,在调用 mmap 函数时,需要传入文件描述符、文件偏移量和映射区域的长度。如果 mmap 调用成功,则会返回映射区域的起始地址,否则会返回 MAP_FAILED。
映射区域之后,需要使用 munmap 函数释放映射区域。munmap 函数需要传入映射区域的起始地址和长度。如果 munmap 调用成功,则返回 0,否则返回 -1。
在完成内存映射进程间通信后,需要使用 close 函数关闭文件描述符。
需要注意的是,内存映射进程间通信只能在共享存储区域中进行。如果想要在进程间进行双向通信,则可以使用管道、消息队列、信号量或套接字等技术。
setitimer函数的用法
setitimer
是一个函数,可以在Linux系统中用于设置和控制内核中的间隔计时器。它可以用来实现定时器功能,在给定的时间间隔内发送信号。
#include <sys/time.h>
int setitimer(int which, const struct itimerval *new_value, struct itimerval *old_value);
参数:
which
:指定要设置的计时器的类型。可以为ITIMER_REAL
,ITIMER_VIRTUAL
或ITIMER_PROF
中的一个。new_value
:指向一个itimerval
结构体的指针,其中包含了新的计时器设置。该结构体包含两个字段:it_interval
:指定计时器的重载值(即计时器溢出后的重新计时值)。it_value
:指定计时器的初始值。
old_value
:如果不为NULL
,则将当前计时器设置写入该指向itimerval
结构体的指针。
返回值:
如果成功,则返回0;如果失败,则返回-1,并设置errno
。
#include <signal.h>
#include <sys/time.h>
#include <stdio.h>
#include <unistd.h>
void timer_handler (int signum)
{
static int count = 0;
printf ("timer expired %d times\n", ++count);
}
线程的创建
/*
一般情况下,main函数所在的线程我们称之为主线程(main线程),其余创建的线程
称之为子线程。
程序中默认只有一个进程,fork()函数调用,2进行
程序中默认只有一个线程,pthread_create()函数调用,2个线程。
#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);
- 功能:创建一个子线程
- 参数:
- thread:传出参数,线程创建成功后,子线程的线程ID被写到该变量中。
- attr : 设置线程的属性,一般使用默认值,NULL
- start_routine : 函数指针,这个函数是子线程需要处理的逻辑代码
- arg : 给第三个参数使用,传参
- 返回值:
成功:0
失败:返回错误号。这个错误号和之前errno不太一样。
获取错误号的信息: char * strerror(int errnum);
*/
#include <stdio.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>
void * callback(void * arg) {
printf("child thread...\n");
printf("arg value: %d\n", *(int *)arg);
return NULL;
}
int main() {
pthread_t tid;
int num = 10;
// 创建一个子线程
int ret = pthread_create(&tid, NULL, callback, (void *)&num);
if(ret != 0) {
char * errstr = strerror(ret);
printf("error : %s\n", errstr);
}
for(int i = 0; i < 5; i++) {
printf("%d\n", i);
}
sleep(1);
return 0; // exit(0);
}
#include <pthread.h>
#include <stdio.h>
// 线程函数
void *thread_function(void *arg) {
printf("Inside thread function\n");
return NULL;
}
int main() {
pthread_t thread;
// 创建线程
int result = pthread_create(&thread, NULL, thread_function, NULL);
if (result != 0) {
perror("Thread creation failed");
return 1;
}
printf("Inside main function\n");
// 等待线程结束
result = pthread_join(thread, NULL);
if (result != 0) {
perror("Thread join failed");
return 2;
}
return 0;
}
首先,你需要在你的 C 程序中包含 pthread.h
头文件。然后,你可以使用 pthread_create
函数来创建线程。 该函数接受四个参数:
- 第一个参数是一个指向线程标识符的指针。
- 第二个参数是一个指向线程属性的指针,可以将其设置为
NULL
。 - 第三个参数是一个指向线程函数的指针,线程将在此函数中执行。
- 第四个参数是一个指向线程函数的参数的指针,可以将其设置为
NULL
。
如果创建线程成功,则 pthread_create
函数将返回 0。否则,它将返回错误代码。
pthread_join
:终止线程。第二个参数是线程函数的返回值,是二级指针
HTTP请求/响应的步骤
1.客户端连接到 Web 服务器一个HTTP客户端,通常是浏览器,与 Web 服务器的 HTTP 端口(默认为 80 ) 建立一个TCP 套接字连接。例如,http://www.baidu.com。 (URL)
2.发送 HTTP 请求通过 TCP 套接字,客户端向 Web 服务器发送一个文本的请求报文,一个请求报文由请求行、请求头部、空行和请求数据 4 部分组成。
3.服务器接受请求并返回HTTP响应
Web 服务解析请求,定位请求资源。服务器将资源复本写到 套接字中,由客户端读取。一个响应由状态行、响应头部、空行和响应数据 4 部分组成。
4.释放连接 TCP 连接
若 connection 模式为 close,则服务器主动关闭 TC P连接,客户端被动关闭连接,释放 TCP 连接;若connection 模式为 keepalive,则该连接会保持一段时间,在该时间内可以继续接收请求;
5.客户端浏览器解析 HTML 内容
客户端浏览器首先解析状态行,查看表明请求是否成功的状态代码。然后解析每一个响应头,响应头告知以下为若干字节的 HTML 文档和文档的字符集。客户端浏览器读取响应数据 HTML,根据 HTML 的语法对其进行格式化,并在浏览器窗口中显示。
列如: 在浏览器地址栏键入URL,按下回车之后会经历以下流程:
1.浏览器向 DNS 服务器请求解析该 URL 中的域名所对应的 IP 地址;
2.解析出IP 地址后,根据该 IP 地址和默认端口 80,和服务器建立 TCP 连接;
3.浏览器发出读取文件(URL 中域名后面部分对应的文件)的 HTTP 求,该请求报文作为 TCP 三次握手的第三个报文的数据发送给服务器;
4.服务器对浏览器请求作出响应,并把对应的 HTML 文本发送给浏览器;
5.释放 TCP 连接
6.浏览器将该 HTML 文本并显示内容
阻塞与非阻塞
阻塞:在Linux环境下,所有的I/O操作默认都是阻塞的,何为阻塞呢,当系统进行调用时,阻塞调用就是除非出错(被信号打断也视为出错),进程将会陷入内核态,直到完成系统调用。非阻塞系统调用就是指无论I/O操作成功过与否,调用都会立即返回。
同步与异步
同步既可以是阻塞的,也可以是非阻塞的,而常用的Linux的I/O调用实际上都是同步的。这里的同步和非同步,是指I/O数据的复制工作是否同步执行。
同步表示A向B请求调用一个网络IO接口时(或者调用某个业务逻辑API接口时),数据的读写都是由请求方A自己来完成的(不管是阻塞还是非阻塞);异步表示A向B请求调用一个网络IO接口时(或者调用某个业务逻辑API接口时),向B传入请求的事件以及事件发生时通知的方式,A就可以处理其它逻辑了,当B监听到事件处理完成后,会用事先约定好的通知方式,通知A处理结果。
线程同步
临界区:是指访问某一共享资源的代码片段,并且这段代码的执行应为原子操作,也就是同时访问同一共享资源的其他线程不应终端该片段的执行。
线程同步:即当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作,其他线程才能对该内存地址进行操作,而其他线程则处于等待状态。
TCP三次握手
三次握手的目的是保证双方互相之间建立了连接。
三次握手发生在客户端连接的时候,当调用connect(),底层会通过TCP协议进行三次握手。
为什么要进行三次握手,而不是二次握手或者四次握手?
-
三次握手才可以阻止重复历史连接的初始化(主要原因)
-
三次握手才可以同步双方的初始序列号
-
三次握手才可以避免资源浪费
if (result != 0) {
perror(“Thread creation failed”);
return 1;
}printf(“Inside main function\n”);
// 等待线程结束
result = pthread_join(thread, NULL);
if (result != 0) {
perror(“Thread join failed”);
return 2;
}return 0;
}
首先,你需要在你的 C 程序中包含 `pthread.h` 头文件。然后,你可以使用 `pthread_create` 函数来创建线程。 该函数接受四个参数:
- 第一个参数是一个指向线程标识符的指针。
- 第二个参数是一个指向线程属性的指针,可以将其设置为 `NULL`。
- 第三个参数是一个指向线程函数的指针,线程将在此函数中执行。
- 第四个参数是一个指向线程函数的参数的指针,可以将其设置为 `NULL`。
如果创建线程成功,则 `pthread_create` 函数将返回 0。否则,它将返回错误代码。
`pthread_join`:终止线程。第二个参数是线程函数的返回值,是二级指针
### HTTP请求/响应的步骤
1.客户端连接到 Web 服务器一个HTTP客户端,通常是浏览器,与 Web 服务器的 HTTP 端口(默认为 80 ) 建立一个TCP 套接字连接。例如,http://www.baidu.com。 (URL)
2.发送 HTTP 请求通过 TCP 套接字,客户端向 Web 服务器发送一个文本的请求报文,一个请求报文由请求行、请求头部、空行和请求数据 4 部分组成。
3.服务器接受请求并返回HTTP响应
Web 服务解析请求,定位请求资源。服务器将资源复本写到 套接字中,由客户端读取。一个响应由状态行、响应头部、空行和响应数据 4 部分组成。
4.释放连接 TCP 连接
若 connection 模式为 close,则服务器主动关闭 TC P连接,客户端被动关闭连接,释放 TCP 连接;若connection 模式为 keepalive,则该连接会保持一段时间,在该时间内可以继续接收请求;
5.客户端浏览器解析 HTML 内容
客户端浏览器首先解析状态行,查看表明请求是否成功的状态代码。然后解析每一个响应头,响应头告知以下为若干字节的 HTML 文档和文档的字符集。客户端浏览器读取响应数据 HTML,根据 HTML 的语法对其进行格式化,并在浏览器窗口中显示。
列如: 在浏览器地址栏键入URL,按下回车之后会经历以下流程:
1.浏览器向 DNS 服务器请求解析该 URL 中的域名所对应的 IP 地址;
2.解析出IP 地址后,根据该 IP 地址和默认端口 80,和服务器建立 TCP 连接;
3.浏览器发出读取文件(URL 中域名后面部分对应的文件)的 HTTP 求,该请求报文作为 TCP 三次握手的第三个报文的数据发送给服务器;
4.服务器对浏览器请求作出响应,并把对应的 HTML 文本发送给浏览器;
5.释放 TCP 连接
6.浏览器将该 HTML 文本并显示内容
[外链图片转存中...(img-FjGpzglK-1699948182017)]
### 阻塞与非阻塞
阻塞:在Linux环境下,所有的I/O操作默认都是阻塞的,何为阻塞呢,当系统进行调用时,阻塞调用就是除非出错(被信号打断也视为出错),进程将会陷入内核态,直到完成系统调用。非阻塞系统调用就是指无论I/O操作成功过与否,调用都会立即返回。
### 同步与异步
同步既可以是阻塞的,也可以是非阻塞的,而常用的Linux的I/O调用实际上都是同步的。这里的同步和非同步,是指I/O数据的复制工作是否同步执行。
同步表示A向B请求调用一个网络IO接口时(或者调用某个业务逻辑API接口时),数据的读写都是由请求方A自己来完成的(不管是阻塞还是非阻塞);异步表示A向B请求调用一个网络IO接口时(或者调用某个业务逻辑API接口时),向B传入请求的事件以及事件发生时通知的方式,A就可以处理其它逻辑了,当B监听到事件处理完成后,会用事先约定好的通知方式,通知A处理结果。
### 线程同步
临界区:是指==访问某一共享资源的代码片段==,并且这段代码的执行应为原子操作,也就是同时访问同一共享资源的其他线程不应终端该片段的执行。
线程同步:即当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作,其他线程才能对该内存地址进行操作,而其他线程则处于等待状态。
### TCP三次握手
三次握手的目的是保证双方互相之间建立了连接。
三次握手发生在客户端连接的时候,当调用connect(),底层会通过TCP协议进行三次握手。
==为什么要进行三次握手,而不是二次握手或者四次握手?==
- 三次握手才可以阻止==重复历史连接的初始化==(主要原因)
- 三次握手才可以==同步双方的初始序列号==
- 三次握手才可以==避免资源浪费==