Linux应用开发

Posted _WILLPOWER_

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Linux应用开发相关的知识,希望对你有一定的参考价值。


参考国嵌资料

GDB调试

GDB是GNU发布的一款功能强大的程序调试工具。GDB主要完成下面三个方面的功能:

  1. 启动被调试程序。
  2. 让被调试的程序在指定的位置停住。
  3. 当程序被停住时,可以检查程序状态(如变量值)。
#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);
}
  1. 编译生成可执行文件:
    gcc -g test.c -o tst
  2. 启动GBD
    gdb tst
  3. 在main函数处设置断点
    break main
  4. 运行程序
    run
  5. 利用更加丰富的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应用程序地址布局

内存布局

  1. 从低地址到高地址分别为:代码段、数据段、BSS段、堆、栈
  2. 堆向高内存地址生长
  3. 栈向低内存地址生长
    所有的linux程序都是从相同(虚拟的地址空间)开始的

查看程序各段地址

cat /proc/进程号/maps

数据存放

  1. 代码段: 代码, 全局常量(const), 字符串常量
  2. 数据段: 全局变量(初始化以及未初始化的)、静态变量(全局的和局部的、初始化的以及未初始化的)
  3. 堆:动态分配的区域
  4. 栈:局部变量(初始化以及未初始化的,但不包含静态变量)、局部只读变量(const)

BSS段

利用readelf -S 程序名分析BSS段

Linux编程规范

文件要有头,函数头

/*****************************************************
*文件名:	pcf8591.c
*创建者:	willpower
*创建时间:	2021/09/16
*程序说明:	pcf8591驱动程序
******************************************************/

/*****************************************************
*函数名:	add
*参数:		加数a,加数b
*返回值:	整数 (相加的和)
*函数功能:	执行加法
******************************************************/

静态函数库设计

Linux应用程序设计中需要的外部函数主要由函数库和系统调用来提供。

函数库按照链接方式可分为

  • 静态链接库
  • 动态链接库

函数库-存放位置

Linux应用程序使用的主要函数库均存放于/lib,/usr/lib目录下,其中采用*.so.*方式命名的是动态函数库,而以*.a方式命令的是静态函数库。

静态链接库-特点

程序所要用到的库函数代码在链接时全部被copy到程序中。
导致的问题:
如果有多个进程在内存中同时运行,并且使用了相同的库函数,那么就会有多份拷贝,这就是对空间的浪费。

使用静态库-编译选项

  1. Linux下进行链接时,默认是链接动态库
  2. 如果需要使用静态库,需要使用编译选项 -static
    gcc -static test.c -o test
    查看readelf -d 程序名

制作静态库

  1. gcc -c mylib -o mylib.o
  2. ar cqs libmylib.a mylib.o
  3. 将制作好的libmylib.a复制到/usr/lib

编译选项
-lname: GCC在链接时,默认只会链接C函数库,而对于其他的函数库,则需要使用-选项来显示地指明需要链接。例: gcc test.c -lmylib -o test

注意打包中ar命令中使用lib作为库的前缀,但是链接库的时候不加库的前缀

#include"stdio.h"使用""是当前工程目录下面找
#include<stdio.h>使用<>是库中找

动态函数库设计

制作动态库

  1. gcc -c mylib -o mylib.o
  2. gcc -shared -fPIC mylib.o -o libmylib.so
  3. 将制作好的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运行

调度算法
在操作系统中,常见的调度算法有:

  1. 先来先服务
  2. 短进程优先调度
  3. 高优先级优先调度
  4. 时间片轮转法

调度形式

  1. 抢占式调度
  2. 非抢占式调度

死锁

多个进程因竞争资源而形成一种僵局,导致这些进程都无法继续往前执行。

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保留原有的进程,执行新的代码。

无名管道通信

  • 核心理论
    • 进程通讯方式
    • 管道通讯
    • 无名管道
  • 函数学习
    • 创建管道
    • 关闭管道
    • 读管道
    • 写管道
  • 综合实例

核心理论

通讯

  1. 数据传输
    一个进程需要将数据发送给另一个进程。
  2. 资源共享
    多个进程之间共享同样的资源。
  3. 通知事件
    一个进程需要向另一个/组进程发送消息,通知它们发生了某事件。
  4. 进程控制
    有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有操作,并能够及时知道它的状态改变。

Linux进程间通信(IPC interprodess communication)由以下几部分发展而来:

  1. UNIX进程间通信
  2. 基于System V进程间通信
  3. 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 )

管道通信

一个进程在管道的尾部写入数据,另一个进程从管道的头部读出数据。管道包括无名管道有名管道两种,前者只能用于父进程和子进程间的通信,后者可用于运行于同一系统中的任意两个进程间的通信。

特点

  1. 管道通讯是单向的,有固定的读端写端
  2. 数据被进程从管道读出后,在管道中该数据就不存在了。
  3. 当进程去读取空管道的时候,进程会阻塞。
  4. 当进程往满管道写入数据时,进程会阻塞。
  5. 管道容量为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文件在使用上和普通文件有相似之处,但是也有不同之处:

  1. 读取Fifo文件的进程只能以"RDONLY"方式打开
    fifo文件。
  2. 写Fifo文件的进程只能以"WRONLY"方式打开fifo
    文件。
  3. 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 权限简介 | 系统权限 | 用户权限 | 匿名用户权限 | 读 | 写 | 执行 | 更改组 | 更改用户 | 粘滞 )(代码片段

linux中怎么查看mysql数据库版本

-bash: /usr/bin/ls: /lib64/ld-linux-x86-64.so.2: bad ELF interpreter: No such file or directory(代码片段

jQuery应用 代码片段

微信小程序代码片段