1 应用编程概念

目录

前言

1.1 系统调用

1.2 库函数

1.3 标准 C 语言函数库

 1.4 main 函数

1.5 开发环境


        本文参考正点原子教程,仅作为个人笔记使用。

前言

        学习应用开发并不要求先学会驱动开发,应用开发与驱动开发本就是两个不同的方向,将来在工作当中也会负责不同的任务、解决不同的问题。应用程序负责处理应用层用户需求、逻辑,而驱动程序负责内核层硬件底层操作,Linux 操作系统向应用层提供了封装好的 API 函数(也称为系统调用),应用层通过系统调用可以完成对系统硬件设备的操作、控制;所以学习应用开发并不要求大家得学会驱动开发,不过话又说回来,如果大家学过驱动开发,那么必然对你学习应用开发是有一定帮助的,当然学习 Linux 应用开发并不针对于嵌入式行业,也可从事PC 端应用程序开发工作以及其它相关工作。
        Linux 应用开发学习与具体硬件平台无关,可以在 PC Linux 操作系统下进行编程开发学习,譬如Ubuntu、 CentOS Debian 等,亦可使用嵌入式 Linux 系统开发平台进行编程实战测试。
        在正式学习 Linux 应用编程之前,可能对应用编程(也可称为系统编程)这个概念并不太了解,了解这些简单基本的概念,可以从整体上认识到 应用编程为何物?与驱动编程、裸机编程有何不同?
        了解本章所介绍的内容是掌握应用编程的先决条件,所以本章主要内容便是对 Linux 应用编程进行一个简单地介绍,让自己对此有一个基本的认识。

1.1 系统调用

        系统调用(system call )其实是 Linux 内核提供给应用层的应用编程接口( API Linux 应用层进入内核的入口。不止 Linux 系统,所有的操作系统都会向应用层提供系统调用,应用程序通过系统调用来使用操作系统提供的各种服务。
        通过系统调用,Linux 应用程序可以请求内核以自己的名义执行某些事情,譬如打开磁盘中的文件、读写文件、关闭文件以及控制其它硬件外设。
        通过系统调用 API,应用层可以实现与内核的交互,其关系可通过下图简单描述:
内核、系统调用与应用程序
        内核提供了一系列的服务、资源、支持一系列功能,应用程序通过调用系统调用 API 函数来使用内核提供的服务、资源以及各种各样的功能,如果大家接触过其它操作系统编程,想必对此并不陌生,譬如Windows 应用编程,操作系统内核一般都会向应用程序提供应用编程接口 API ,否则我们将我无法使用操作系统。

        应用编程与裸机编程、驱动编程有什么区别? 

        在学习应用编程之前,相信大家都有过软件开发经验,譬如 51 STM32 等单片机软件开发、以及嵌入式 Linux 硬件平台下的驱动开发等, 51 STM32 这类单片机的软件开发通常是裸机程序开发,并不会涉及到操作系统的概念,那应用编程与裸机编程以及驱动开发有什么区别呢?
        就拿嵌入式 Linux 硬件平台下的软件开发来说,我们大可将编程分为三种,分别为裸机编程、 Linux 驱动编程以及 Linux 应用编程。
        首先对于裸机编程这个概念来说很好理解,一般把没有操作系统支持的编程环境称为裸机编程环境,譬如单片机上的编程开发,编写直接在硬件上运行的程序,没有操作系统支持;
        狭义上 Linux 驱动编程指的是基于内核驱动框架开发驱动程序,驱动开发工程师通过调用 Linux 内核提供的接口完成设备驱动的注册,驱动程序负责底层硬件操作相关逻辑,如果学习过 Linux 驱动开发的读者,想必对此并不陌生;
        而 Linux 应用编程(系统编程)则指的是基于 Linux 操作系统的应用编程,在应用程序中通过调用系统调用 API 完成应用程序的功能和逻辑,应用程序运行于操作系统之上
        通常在操作系统下有两种不同的状态:内核态和用户态,应用程序运行在用户态、而内核则运行在内核态。
        关于应用编程这个概念,以上解释很清楚了,以实现点亮一个 LED 功能为例,简单地说明三者之间的区别,LED 裸机程序如下所示:
//LED 裸机程序
static void led_on(void)
{
    /* 点亮 LED 硬件操作代码 */
}
static void led_off(void)
{
    /* 熄灭 LED 硬件操作代码 */
}
int main(void)
{
     /* 用户逻辑 */
     for ( ; ; ) {
         led_on(); //点亮 LED
         delay(); //延时
         led_off(); //熄灭 LED
         delay(); //延时
     }
}
        可以看到在裸机程序当中,LED 硬件操作代码与用户逻辑代码全部都是在同一个源文件(同一个工程)中实现的,硬件操作代码与用户逻辑代码没有隔离,没有操作系统支持,代码编译之后直接在硬件平台运行,俗称“裸跑”。
        我们再来看一个 Linux 系统下的 LED 驱动程序示例代码,如下所示:
//Linux 下 LED 驱动程序
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/of_gpio.h>
#include <linux/delay.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>
static void led_on(void)
{
     /* 点亮 LED 硬件操作代码 */
}
static void led_off(void)
{
     /* 熄灭 LED 硬件操作代码 */
}
static int led_open(struct inode *inode, struct file *filp)
{
     /* 打开设备时需要做的事情 */
}
static ssize_t led_write(struct file *filp, const char __user *buf,
 size_t size, loff_t *offt)
{
     int flag;
     /* 获取应用层 write 的数据,存放在 flag 变量 */
     if (copy_from_user(&flag, buf, size))
     return -EFAULT;
     /* 判断用户写入的数据,如果是 0 则熄灭 LED,如果是非 0 则点亮 LED */
     if (flag)
         led_on();
     else
         led_off();
     return 0;
}
static int led_release(struct inode *inode, struct file *filp)
{
     /* 关闭设备时需要做的事情 */
}
static struct file_operations led_fops = {
     .owner = THIS_MODULE,
     .open = led_open,
     .write = led_write,
     .release = led_release,
};
static int led_probe(struct platform_device *pdev)
{
     /* 驱动加载时需要做的事情 */
}
static int led_remove(struct platform_device *pdev)
{
     /* 驱动卸载时需要做的事情 */
}
static const struct of_device_id led_of_match[] = {
    { .compatible = "alientek,led", },
    { /* sentinel */ },
};
MODULE_DEVICE_TABLE(of, led_of_match);
static struct platform_driver led_driver = {
     .driver = {
         .owner = THIS_MODULE,
         .name = "led",
         .of_match_table = led_of_match,
      },
     .probe = led_probe,
     .remove = led_remove,
};
module_platform_driver(led_driver);
MODULE_DESCRIPTION("LED Driver");
MODULE_LICENSE("GPL");
以上并不是一个完整的 LED 驱动代码,如果没有接触过 Linux 驱动开发的读者,看不懂也没有关系,并无大碍,此驱动程序使用了最基本的字符设备驱动框架编写而成,非常简单;led_fops 对象中提供了 open、write、release 方法,当应用程序调用 open 系统调用打开此 LED 设备时会执行到 led_open 函数,当调用 close系统调用关闭 LED 设备时会执行到 led_release 函数,而调用 write 系统调用时会执行到 led_write 函数,此驱动程序的设定是当应用层调用 write 写入 0 时熄灭 LED write 写入非 0 时点亮 LED
        驱动程序属于内核的一部分,当操作系统启动的时候会加载驱动程序,可以看到 LED 驱动程序中仅仅实现了点亮/ 熄灭 LED 硬件操作相关逻辑代码,应用程序可通过 write 这个系统调用 API 函数控制 LED 亮灭;接下来我们看看 Linux 系统下的 LED 应用程序示例代码,如下所示:
//Linux 下 LED 应用程序
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main(int argc, char **argv)
{
    int fd;
    int data;
    fd = open("/dev/led", O_WRONLY);//打开 LED 设备(假定 LED 的设备文件为/dev/led)
    if (0 > fd)
        return -1;
    for ( ; ; ) {
        data = 1;
        write(fd, &data, sizeof(data)); //写 1 点亮 LED
        sleep(1); //延时 1 秒
        data = 0;
        write(fd, &data, sizeof(data)); //写 0 熄灭 LED
        sleep(1); //延时 1 秒
    }
    close(fd);
    return 0;
}
        此应用程序也非常简单,仅只需实现用户逻辑代码即可,循环点亮、熄灭 LED ,并不需要实现硬件操作相关,示例代码中调用了 open write close 这三个系统调用 API 接口, open close 分别用于打开 / 关闭LED 设备, write 写入数据传给 LED 驱动,传入 0 熄灭 LED ,传入非 0 点亮 LED
        LED 应用程序与 LED 驱动程序是分隔、分离的,它们单独编译,它们并不是整合在一起的,应用程序运行在操作系统之上,有操作系统支持,应用程序处于用户态,而驱动程序处于内核态,与纯粹的裸机程序存在着质的区别。Linux 应用开发与驱动开发是两个不同的方向,将来在工作当中也会负责不同的任务、解决不同的问题。

1.2 库函数

        前面给大家介绍了系统调用,系统调用是内核直接向应用层提供的应用编程接口,譬如 open write 、read、 close 等,关于这些系统调用后面会给大家进行详细介绍。编写应用程序除了使用系统调用之外,我们还可以使用库函数,本小节来聊一聊库函数。
        库函数也就是 C 语言库函数 C 语言库是应用层使用的一套函数库,在 Linux 下,通常以动态(.so)库文件的形式提供,存放在根文件系统/lib 目录下 C 语言库函数构建于系统调用之上,也就是说库函数其实是由系统调用封装而来的,当然也不能完全这么说,原因在于有些库函数并不调用任何系统调用,譬如一些字符串处理函数 strlen() strcat() memcpy() memset() strchr() 等等;而有些库函数则会使用系统调用来帮它完成实际的操作,譬如库函数 fopen 内部调用了系统调用 open() 来帮它打开文件、库函数 fread() 就利用了系统调用 read() 来完成读文件操作、 fwrite() 就利用了系统调用 write() 来完成写文件操作。
        Linux 系统内核提供了一系列的系统调用供应用层使用,我们直接使用系统调用就可以了呀,那为何还要设计出库函数呢?事实上,有些系统调用使用起来并不是很方便,于是就出现了 C 语言库,这些 C 语言库函数的设计是为了提供比底层系统调用更为方便、更为好用、且更具有可移植性的调用接口
来看一看它们之间的区别:
 
库函数是属于应用层,而系统调用是内核提供给应用层的编程接口,属于系统内核的一部分;
库函数运行在用户空间,调用系统调用会由用户空间(用户态)陷入到内核空间(内核态);
库函数通常是有缓存的,而系统调用是无缓存的,所以在性能、效率上,库函数通常要优于系统调用;
可移植性:库函数相比于系统调用具有更好的可移植性,通常对于不同的操作系统,其内核向应用层提供的系统调用往往都是不同,譬如系统调用的定义、功能、参数列表、返回值等往往都是不一 样的;而对于 C 语言库函数来说,由于很多操作系统都实现了 C 语言库, C 语言库在不同的操作系统之间其接口定义几乎是一样的,所以库函数在不同操作系统之间相比于系统调用具有更好的可移植性。
        以上便上它们之间一个大致的区别,从实现者的角度来看,系统调用与库函数之间有根本的区别,但从用户使用角度来看,其区别并不重要,它们都是 C 语言函数。在实际应用编程中,库函数和系统调用都会使用到,所以对于我们来说,直接把它们当做是 C 函数即可,知道你自己调用的函数是系统调用还是库函数即可,不用太过于区分它们之间的差别
        所以应用编程简单点来说就是:开发 Linux 应用程序,通过调用内核提供的系统调用或使用 C 库函数来开发具有相应功能的应用程序

1.3 标准 C 语言函数库

        在 Linux 系统下 , 使用的 C 语言库为 GNU C 语言函数库( 也 叫 作 glibc) glibc网址,作为 Linux 下的标准 C 语言函数库。进入到 glibc网址, 如下所示:
glibc 官网
        点击上面的 Sources 选项可以查看它的源码实现: 
获取源码的方式
        glibc 源码的获取方式很简单,直接直接从 git 仓库下载,也可以通过 ftp 下载,如果大家有兴趣、或者想要了解某一个库函数它的具体实现,那么就可以获取到它源码来进行分析,好了,这里就不再多说了!
        确定 Linux 系统的 glibc 版本
        前面提到过了,C 语言库是以动态库文件的形式提供的,通常存放在 /lib 目录,它的命名方式通常是libc.so.6,不过这个是一个软链接文件,它会链接到真正的库文件。在我的/lib 目录下并没有发现libc.so.6 这个文件,其实是在/lib/x86_64-linux-gnu 目录下,进入到该目录:
libc.so.6 文件
        可以看到 libc.so.6 链接到了 libc-2.27.so 库文件, 2.27 表示的就是这个 glibc 库的版本号为 2.27 。除此之外,我们还可以直接运行该共享库来获取到它的信息,如下所示:
确定 glibc 版本号
        从打印信息可以看到,我所使用的 Ubuntu 系统对应的 glibc 版本号为 2.27

 1.4 main 函数

        对学习过 C 语言编程的读者来说,譬如单片机编程、 Windows 应用编程等, main 函数想必大家再熟悉不过了,很多编程开发都是以 main 函数作为程序的入口函数,同样 Linux 应用程序中,main 函数也是作为应用程序的入口函数存在,main 函数的形参一般会有两种写法,如果执行应用程序无需传参,则可以写成如下形式:
//main 函数写法之无传参
int main(void)
{
 /* 代码 */
}
        如果在执行应用程序的时候需要向应用程序传递参数,则写法如下:
//main 函数写法之有传参
int main(int argc, char **argv)
{
 /* 代码 */
}
        argc 形参表示传入参数的个数包括应用程序自身路径和程序名,譬如运行当前目录下的 hello 可执行文件,并且传入参数,如下所示:
./hello 112233
        那么此时参数个数为 2 ,并且这些参数都是作为字符串的形式传递给 main 函数 :
        argv[0]等于 "./hello"
        argv[1]等于 "112233"
        有传参时 main 函数的写法并不只有这一种,只是这种写法最常用,对于其它的写法,后面学习过程中如果遇到了再给大家进行讲解,这里暂时先不去管。

1.5 开发环境

操作系统Ubuntu
 IDE 软件
vscode
编译器PCgcc
  ARM交叉编译工具(ARM 架构下的 gcc 编译器)
管理工程源文件用Makefile 来组织、管理工程源文件
cmake 来管理我们的源码工程
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

不做拖延王

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值