Linux应用开发
Posted _WILLPOWER_
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Linux应用开发相关的知识,希望对你有一定的参考价值。
文章目录
参考国嵌资料
GDB调试
GDB是GNU发布的一款功能强大的程序调试工具。GDB主要完成下面三个方面的功能:
- 启动被调试程序。
- 让被调试的程序在指定的位置停住。
- 当程序被停住时,可以检查程序状态(如变量值)。
#include <stdio.h>
void myprint(long result)
{
printf("result is %d\\n", result);
}
void main()
{
int i;
long result = 0;
for (i = 0; i < 100; i++)
{
result += 1;
}
myprint(result);
}
- 编译生成可执行文件:
gcc -g test.c -o tst
- 启动GBD
gdb tst
- 在main函数处设置断点
break main
- 运行程序
run
- 利用更加丰富的GBD命令对程序进行调试
- list(l) 查看程序
- break(b) 函数名 在某函数入口处添加断点
- break(b) 文件名:行号 在指定文件的指定行添加断点
- info break 查看所有设置的断点
- delete 断点编号 删除断点
- next(n) 单步运行程序(不进入子函数)
- step(s) 单步运行程序(进入子函数)
- continue© 继续运行程序,直到下一个断点
- print( p) 变量名 查看指定变量值
- set var=value 设置变量的值
- quit(q) 退出gdb
Coredump故障分析
Core Dump又叫核心转存。当程序在运行过程中发生异常,这时Linux系统可以把程序出错时的内存内容存储在一个core文件中,这种过程叫Core Dump。
Core Dump主要用来对付什么样的错误呢?
Segment fault
Linux应用程序在运行过程中,经常会遇到Segment fault(段错误)这样的错误。
产生这样错误的原因通常有:
- 数组访问越界
- 访问空指针
- 栈溢出
- 修改只读内存
在Linux系统中,默认是关闭core dump功能的
,但是可以通过来ulimit命令打开/关闭core dump功能。
打开ulimit -c unlimited
关闭ulimit -c 0
发生core dump之后,可以使用gdb进行查看core文件的内容,以定位程序出错的位置.
用法: gdb 程序名 core文件名
例如:gdb ./test test.core
使用
GDB+Core file找出程序中的错误(访问空指针)
使用
GDB+Core file找出程序中的错误(访问只读内存)
Linux应用程序地址布局
内存布局
- 从低地址到高地址分别为:代码段、数据段、BSS段、堆、栈
- 堆向高内存地址生长
- 栈向低内存地址生长
所有的linux程序都是从相同(虚拟的地址空间)开始的
查看程序各段地址
cat /proc/进程号/maps
数据存放
- 代码段: 代码, 全局常量(const), 字符串常量
- 数据段: 全局变量(初始化以及未初始化的)、静态变量(全局的和局部的、初始化的以及未初始化的)
- 堆:动态分配的区域
- 栈:局部变量(初始化以及未初始化的,但不包含静态变量)、局部只读变量(const)
BSS段
利用readelf -S 程序名
分析BSS段
Linux编程规范
文件要有头,函数头
/*****************************************************
*文件名: pcf8591.c
*创建者: willpower
*创建时间: 2021/09/16
*程序说明: pcf8591驱动程序
******************************************************/
/*****************************************************
*函数名: add
*参数: 加数a,加数b
*返回值: 整数 (相加的和)
*函数功能: 执行加法
******************************************************/
静态函数库设计
Linux应用程序设计中需要的外部函数主要由函数库和系统调用来提供。
函数库按照链接方式可分为
- 静态链接库
- 动态链接库
函数库-存放位置
Linux应用程序使用的主要函数库均存放于/lib,/usr/lib
目录下,其中采用*.so.*
方式命名的是动态函数库,而以*.a
方式命令的是静态函数库。
静态链接库-特点
程序所要用到的库函数代码在链接时全部被copy到程序中。
导致的问题:
如果有多个进程在内存中同时运行,并且使用了相同的库函数,那么就会有多份拷贝,这就是对空间的浪费。
使用静态库-编译选项
- Linux下进行链接时,默认是链接动态库
- 如果需要使用静态库,需要使用编译选项
-static
gcc -static test.c -o test
查看readelf -d 程序名
制作静态库
gcc -c mylib -o mylib.o
ar cqs libmylib.a mylib.o
- 将制作好的libmylib.a复制到/usr/lib
编译选项
-lname
: GCC在链接时,默认只会链接C函数库,而对于其他的函数库,则需要使用-选项来显示地指明需要链接。例: gcc test.c -lmylib -o test
注意打包中ar命令中使用lib作为库的前缀,但是链接库的时候不加库的前缀
#include"stdio.h"使用""是当前工程目录下面找
#include<stdio.h>使用<>是库中找
动态函数库设计
制作动态库
gcc -c mylib -o mylib.o
gcc -shared -fPIC mylib.o -o libmylib.so
- 将制作好的libmylib.so复制到/usr/lib中
-fpic使输出的对象模块是按照可重定位地址方式生成的
-shared指明产生动态链接库
系统调用方式文件编程
- 核心理论
- 文件描述符
- 函数学习
- 打开文件
- 创建文件
- 关闭文件
- 读文件
- 写文件
- 文件定位
- 复制文件描述符
- 综合实例
- 文件复制程序
文件描述符
在Linux系统中,所有打开的文件也对应一个数字,这个数字由系统来分配,我们称之为:文件描述符
打开文件
函数名
open
函数原型
int open(const char *pathname, int flags)
int open(const char *pathname, int flags, mode_t mode)
函数功能
打开或者创建一个文件
所属头文件
<sys/types.h> <sys/stat.h> <fcntl.h>
返回值
成功:文件描述符 失败-1
参数说明
pathname:要打开的文件名(含路径)
flags:文件打开的标志
-O_APPEND:以追加的方式打开文件
-O_CREAT:当打开的文件不存在的时候,创建该文件
mode:
一定是在flags中使用了O_CREAT标志,mode记录待创建的访问权限
创建文件
函数名
creat
函数原型
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
函数功能
创建一个文件,并以只写的方式打开该文件
所属头文件
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
返回值
成功:返回文件描述符 失败:-1
参数说明
pathname:创建的文件名(含路径)
mode:创建的文件的读写权限
关闭文件
函数名
close
函数原型
int close(int fd);
函数功能
关闭一个打开的文件
所属头文件
#include <unistd.h>
返回值
成功:0 失败:-1
参数说明
fd: 待关闭的文件描述符
读文件
函数名
read
函数原型
ssize_t read(int fd, void *buf, size_t count);
函数功能
从一个打开的文件中去读取数据
所属头文件
#include <unistd.h>
返回值
成功:读取的字节数 失败:
参数说明
fd: 获取的文件描述符
count: 希望读取的字节数
buf:读取来的数据存到buf指向的空间
写文件
函数名
write
函数原型
ssize_t write(int fd, const void *buf, size_t count);
函数功能
从一个打开的文件中去写数据
所属头文件
#include <unistd.h>
返回值
成功:成功写入的字节数 失败:
参数说明
fd: 获取的文件描述符
count: 希望写入的字节数
buf:指向需要写入数据的位置
文件定位
函数名
lseek
函数原型
off_t lseek(int fd, off_t offset, int whence);
函数功能
从新定位文件的读写位置
所属头文件
#include <sys/types.h>
#include <unistd.h>
返回值
成功:移动后的文件指针距离文件头的位置 失败:-1
参数说明
fd: 获取的文件描述符
offset: 偏移,为正向后,为负向前
whence:从哪里开始,值为
SEEK_SET
The file offset is set to offset bytes.
SEEK_CUR
The file offset is set to its current location plus offset bytes.
SEEK_END
The file offset is set to the size of the file plus offset bytes.
复制文件描述符
函数名
dup
函数原型
int dup(int oldfd);
函数功能
复制一个文件符
所属头文件
#include <unistd.h>
返回值
成功:返回新的文件描述符 失败:-1
参数说明
oldfd: 待复制的老的文件描述符
编写文件复制程序
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
void main(int argc, char **argv)
{
int fd_s, fd_t;
char buf[512];
int count = 0;
//打开源文件
fd_s = open(argv[1], O_RDONLY);
//打开目标文件
fd_t = open(argv[2], O_RDWR | O_CREAT, 0666);
//读取源文件数据
while ((count = read(fd_s, buf, 512)) > 0)
//将读取出的源文件数据写入目标文件
write(fd_t, buf, count);//读多少写多少
//关闭文件
close(fd_s);
close(fd_t);
}
库函数方式文件编程
库函数
基于C函数库的文件编程是独立于具体的操作系统平台的,不管是在Windows、Linux还是其他的操作系统中,都是使用这些函数。使用库函数
进行程序设计可提高程序的可移植性
,而基于系统调用在windows上面是不能够使用的。
流
对于标准的C函数库,它们的操作都是围绕流来进行的。流是一个抽象的概念,当程序需要读取数据的时候,就会开启一个通向数据源
的流
,这个数据源可以是文件
,内存
,或是网络连接
。类似的,当程序需要写入数据的时候,就会开启一个通向目的地的流。这时候你就可以想象数据好像在这其中“流”动一样。
文件指针
在系统调用方式实现的文件访问中,使用文件描述符(一个整数)来指向一个文件。在库函数方式的文件访问中,使用FILE类型来表示一个打开的文件,这个类型中包含了管理文件流的信息。而指向该类型的指针FILE*则被称之为文件指针。
函数学习
打开文件
函数名
fopen
函数原型
FILE *fopen(const char *pathname, const char *mode);
函数功能
打开文件
所属头文件
#include <stdio.h>
返回值
成功:返回文件指针 失败:空指针NULL
参数说明
pathname:打开的文件名(含路径)
mode: 文件打开模式
关闭文件
函数名
fclose
函数原型
int fclose(FILE *stream);
函数功能
关闭文件
所属头文件
#include <stdio.h>
返回值
成功:0 失败:EOF
参数说明
stream:关闭的文件指针
读文件
函数名
fread
函数原型
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
函数功能
从文件中读取数据
所属头文件
#include <stdio.h>
返回值
成功:返回成功读取到的数据量 失败:0
参数说明
stream:指向要读取的文件
ptr: 指向读取出来后的数据的保存位置
nmemb:读取的数据块数
size:每块数据的大小
写文件
函数名
fwrite
函数原型
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
函数功能
向文件中写入文件
所属头文件
#include <stdio.h>
返回值
成功:返回成功写入的数据量 失败:0
参数说明
stream:指向要写入的文件
ptr: 指向写入数据的保存位置
nmemb:写入的数据块数
size:每块数据的大小
定位文件
函数名
fseek
函数原型
nt fseek(FILE *stream, long offset, int whence);
函数功能
设置文件的位置指针
所属头文件
#include <stdio.h>
返回值
成功:0 失败:-1
参数说明
stream:指向要重新定位的文件流
offset: 偏移,为正向后,为负向前
whence:从哪里开始,值为
SEEK_SET
The file offset is set to offset bytes.
SEEK_CUR
The file offset is set to its current location plus offset bytes.
SEEK_END
The file offset is set to the size of the file plus offset bytes.
时间编程
时间类型
Coordinated Universal Time (UTC):
世界标准时间,也就是大家所熟知的格林威治标准时间( Greenwich Mean Time,GMT)。
Calendar Time:
日历时间,是用“从一个标准时间点(如: 1970年1月1日O点)到此时经过的秒数”来表示的时间。
实践操作
- 获取日历时间
- 获取格林威治时间
- 获取本地时间
- 字符串方式显示时间
- 获取高精度时间
获取日历时间
函数名
time
函数原型
time_t time(time_t *tloc);
函数功能
返回日历时间
所属头文件
#include <time.h>
返回值
成功:日历时间 失败:-1
参数说明
tloc:不为空的时候,保存返回值
获取格林威治时间
函数名
gmtime
函数原型
struct tm *gmtime(const time_t *timep);
函数功能
将参数timep所指定的日历时间转换为世界标准时间
所属头文件
#include <time.h>
返回值
成功:返回标准时间,以struct tm形式存储
参数说明
timep:待转换的日历时间
获取本地时间
函数名
localtime
函数原型
struct tm *localtime(const time_t *timep);
函数功能
将参数timep所指定的日历时间转换为本地时间
所属头文件
#include <time.h>
返回值
成功:返回标准时间,以struct tm形式存储
参数说明
timep:待转换的日历时间
#include <time.h>
#include <stdio.h>
void main()
{
struct tm *tm1;
time_t timep = time(NULL);
tm1 = localtime(&timep);
printf("now is hour %d, min is %d\\n", tm1->tm_hour, tm1->tm_min);
}
格式化时间
函数名
sctime
函数原型
char *asctime(const struct tm *tm);
函数功能
将struct tm格式的时间转化为字符串
所属头文件
#include <time.h>
返回值
字符串方式显示的时间
参数说明
tm:待转换的tm格式时间
#include <time.h>
#include <stdio.h>
void main()
{
struct tm *tm1;
char *stime;
time_t timep = time(NULL);
tm1 = localtime(&timep);
stime = asctime(tm1);
printf("now is hour %s\\n", stime);
}
获取高精度时间
函数名
gettimeofday
函数原型
int gettimeofday(struct timeval *tv, struct timezone *tz);
函数功能
获取高精度的时间
所属头文件
#include <sys/time.h>
返回值
成功: 0, 失败: -1
参数说明
tv:保存从1970.1.1 0:0:0到现在经理的秒数和微秒数
tzone: 通常为NULL
进程控制理论
进程
进程是一个具有一定独立功能的程序的一次运行活动。
特点
- 动态性
- 并发性
- 独立性
- 异步性
进程ID
进程ID ( PID):标识进程的唯一数字
父进程的ID( PPID)
启动进程的用户ID(UID )
进程互斥
进程互斥是指当有若干进程
都要使用某一资源时,但该资源在同一时刻最多允许一个进程使用
,这时其他进程必须等待
,直到占用该资源者释放了该资源为止。
临界资源
操作系统中将同一时刻只允许一个进程访问的资源称为临界资源。
临界区
进程中访问临界资源的那段程序代码称为临界区
。为实现对临界资源的互斥访问,应保证诸进程互斥地进入各自的临界区。
进程同步
一组进程
按一定的顺序
执行的过程称为进程间的同步.具有同步关系的这组进程称为合作进程,最为有名的是生产者和消费者进程.
进程调度
按一定算法
,从一组待运行
的进程中选出一个来占有CPU运行
。
调度算法
在操作系统中,常见的调度算法有:
- 先来先服务
- 短进程优先调度
- 高优先级优先调度
- 时间片轮转法
调度形式
- 抢占式调度
- 非抢占式调度
死锁
多个进程因竞争资源而形成一种僵局,导致这些进程都无法继续往前执行。
getpid
函数名
getpid
函数原型
pid_t getpid(void);
函数功能
获取高精度的时间
所属头文件
#include <sys/types.h>
#include <unistd.h>
返回值
调用该函数的进程id
多进程编程
- 创建进程
- 进程退出
- 进程等待
- 进程程序
创建进程
函数名
fork
函数原型
pid_t fork(void);
函数功能
创建一个子进程
所属头文件
#include <sys/types.h>
#include <unistd.h>
返回值
成功:在父进程中返回子进程的PID,在子进程中返回的是0;失败:-1
参数说明
无
生成的子进程从fork处开始执行。
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
void main()
{
pid_t pid;
pid = fork();
printf("pid is %d\\n", pid);
exit(0);
}
函数名
vfork
函数原型
pid_t vfork(void);
函数功能
创建一个子进程,并且阻塞父进程
所属头文件
#include <sys/types.h>
#include <unistd.h>
返回值
成功:在父进程中返回子进程的PID,在子进程中返回的是0;失败:-1
参数说明
无
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
void main()
{
pid_t pid;
int count = 0;
pid = vfork();
count++;
printf("cout = %d\\n", count);
exit(0);
}
使用vfork
子进程共享父进程的栈
使用fork
创建子进程的栈是新的栈
进程退出
父进程:return 0 或者 exit(0)
子进程: 只有exit(0)
进程等待
函数名
wait
函数原型
pid_t wait(int *wstatus);
函数功能
挂起调用它的进程,直到其子进程结束
所属头文件
#include <sys/types.h>
#include <sys/wait.h>
返回值
成功:返回终止的那个子进程的id;失败:-1
参数说明
status:记录子进程的退出状态
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
void main()
{
pid_t pid;
pid = fork();
if (pid > 0)
{
wait();
printf("father process\\n");
exit(0);
}
else
{
printf("child process\\n");
exit(0);
}
}
子进程结束了,父进程才能够执行
执行程序
函数名
execl
函数原型
int execl(const char *pathname, const char arg, …/ (char *) NULL */);
函数功能
运行可执行文件
所属头文件
#include <unistd.h>
返回值
成功:无 失败:-1
参数说明
pathname:要运行的可执行文件的路径
arg: 可执行文件运行所需要的参数,直到NULL结束
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
void main()
{
pid_t pid;
pid = fork();
if (pid > 0)
{
wait(NULL);
printf("father process\\n");
exit(0);
}
else
{
execl("/bin/ls", "ls", "/home", NULL);
printf("child process\\n");//这句话没有被调用
exit(0);
}
}
fork创建
一个新的进程
,产生一个新的PID。
exec保留
原有的进程
,执行新的代码。
无名管道通信
- 核心理论
- 进程通讯方式
- 管道通讯
- 无名管道
- 函数学习
- 创建管道
- 关闭管道
- 读管道
- 写管道
- 综合实例
核心理论
通讯
- 数据传输
一个进程需要将数据发送给另一个进程。 - 资源共享
多个进程之间共享同样的资源。 - 通知事件
一个进程需要向另一个/组进程发送消息,通知它们发生了某事件。 - 进程控制
有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有操作,并能够及时知道它的状态改变。
Linux进程间通信(IPC interprodess communication)由以下几部分发展而来:
- UNIX进程间通信
- 基于System V进程间通信
- POSIX进程间通信
POSIX(Portable Operating System Interface)表示可移植操作系统接口。电气和电子工程师协会IEEE最初开发POSIX标准,是为了提高UNIX环境下应用程序的可移植性。然而,POSIX并不局限于UNIX,许多其它的操作系统,例如Microsoft Windows都支持POSIX标准。
通讯方式
1、无名管道( pipe )
2、有名管道(FIFO)
3、信号( signal )
4、消息队列
5、共享内存
6、信号量
7、套接字( socket )
管道通信
一个进程在管道的尾部写入数据,另一个进程从管道的头部读出数据。管道包括无名管道
和有名管道
两种,前者只能用于父进程和子进程间
的通信,后者可用于运行于同一系统中的任意两个进程间
的通信。
特点
- 管道通讯是单向的,有固定的
读端
和写端
。 - 数据被进程从管道读出后,在管道中该数据就不存在了。
- 当进程去读取空管道的时候,进程会阻塞。
- 当进程往满管道写入数据时,进程会阻塞。
- 管道容量为64KB ( #define PIPE_BUFFERS 16
include/linux/pipe_fs_i.h)
无名管道
在Linux系统中,无名管道一旦创建完成后,操作无名管道等同于操作文件
。无名管道的读端被视作一个文件;无名管道的写端也被视作一个文件。
因此可以使用read ,write ,close等函数来访问无名管道。
函数名
pipe
函数原型
int pipe(int pipefd[2]);
函数功能
创建无名管道
所属头文件
#include <unistd.h>
返回值
成功:0 失败:-1
参数说明
pipefd[0]:读文件地址
pipefd[1]:写文件地址
实例-创建无名管道,并用于父子进程通讯
#include <stdio.h>
#include <unistd.h>
void main()
{
pid_t pid;
int pipefd[2];
//创建管道
pipe(pipefd);
//创建子进程
pid = fork();
if (pid > 0)
{
write(pipefd[1], "1234", 4);
wait();
close(pipefd[1]);
exit(0);
}
else
{
char buf[10];
read(pipefd[0], buf, 4);
buf[4] = '\\0';
printf("buf is %s\\n", buf);
exit(0);
}
}
有名管道通信
有名管道又称为FIFO文件
,因此我们对有名管道的操作可以采用操作文件的方法,如使用open,read ,write等.
FIFO文件对比普通文件
FIFO文件在使用上和普通文件有相似之处,但是也有不同之处:
- 读取Fifo文件的进程只能以"RDONLY"方式打开
fifo文件。 - 写Fifo文件的进程只能以"WRONLY"方式打开fifo
文件。 - Fifo文件里面的内容被读取后,就消失了。但是普
通文件里面的内容读取后还存在。
- 创建管道
- 删除管道
- 打开管道
- 关闭管道
- 读管道
- 写管道
创建有名管道(FIFO)
函数名
mkfifo
函数原型
int mkfifo(const char *pathname, mode_t mode);
函数功能
创建有名管道(创建FIFO文件)
所属头文件
#include <sys/types.h>
#include <sys/stat.h>
返回值
成功:0 失败:-1
参数说明
pathname:要创建的FIFO文件的名字(带路径)
mode:创建文件的访问权限
删除有名管道
函数名
unlink
函数原型
int unlink(const char *pathname);
函数功能
删除文件
所属头文件
#include <unistd.h>
返回值
成功:0 失败:-1
参数说明
pathname:要删除的文件的名字(带路径)
综合实例编程
实现任意两个进程利用有名管道通讯
写进程
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
void main()
{
int fd;
//创建FIFO文件
mkfifo("/tmp/myfifo", 0666);
//打开FIFO文件
fd = open("/tmp/myfifo", O_WRONLY);
//写入数据到FIFO文件
write(fd, "hello fifo", 11);
close(fd);
}
读进程
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
void main()
{
char c_buf[15];
int fd;
fd = open("/tmp/myfifo", O_RDONLY);
以上是关于Linux应用开发的主要内容,如果未能解决你的问题,请参考以下文章
linux打开终端如何启动scala,如何在终端下运行Scala代码片段?
Android 逆向Linux 文件权限 ( Linux 权限简介 | 系统权限 | 用户权限 | 匿名用户权限 | 读 | 写 | 执行 | 更改组 | 更改用户 | 粘滞 )(代码片段
-bash: /usr/bin/ls: /lib64/ld-linux-x86-64.so.2: bad ELF interpreter: No such file or directory(代码片段