Linux-系统编程概述篇

1、引言
2、有关程序语言
2.1、为什么需要高级语言
2.2、高级语言中的c语言
3、有关操作系统
3.1、为什么需要操作系统(OS)
3.2、linux 系统编程涉及的基本章节
4、linux系统编程具体会涉及的一些知识和概念
4.1、登陆
4.2、目录和文件
4.3、输入和输出
4.4、程序和进程
4.5、ANSI C
4.6、原始系统数据类
4.7、出错处理
4.8、用户标识
4.9、信号
4.10、linux时间值
4.11、API接口中的库函数调用和系统调用之区别

1、引言
高级语言和操作系统OS对于现代计算机来说,是异常关键的,对此我们必须要有一个宏观的认识。OS的种类很多,但是我们目前具体将要学习的OS是嵌入式linux操作系统,本篇后续篇幅会对linux系统编程所具体涉及的概念和知识做一个概要性地介绍,为后续课程学习打下一定的基础。
其实操作系统(OS)和系统内核是有一定区别的,但是我们这里将不做区分,一律统一看待。
2、有关程序语言
2.1、为什么需要高级语言
为了减小项目开发难度,和更大的提高代码的编写效率,提高编写效率主要是从以下几个面。
a)高级语言的关键字基本都是熟知英语单词,语法简洁,从而减小了对语言实际操作的难度,因此
提高了代码的编写速度和可阅读性。
b)由于低级语言的硬件平台相关性太强,因此不利于跨平台软件的开发,但是高级语言实现的代码,
却能很好的在不同的平台上运行,当然我们也说过,编译器对于跨平台的实现同样也是功不没的。
2.2、高级语言中的c语言
c语言是面向过程的高级语言,创立之初的目的就是为了编写系统级软件(UNIX),因此我们才会发现c的众多特性非常符合系统级软件开发的需要。面向过程的语言和面向对象的语言,在各自
的领域都是佼佼者,因而我们不能随意的评判一种语言的优劣,因为它们各有各的专长。
3、有关操作系统
3.1、为什么需要操作系统(OS)
从前面有关高级语言的简单描述中,想必我们已经清楚,高级语言的目的就是为了减小软件开发的难度,高级语言中的c语言更是有利于开发系统级软件(系统软件就是一个大型软件)。那么我们为什么要开发出操作系统这样的大型软件呢?目的如下:
a)为了最大化地提高计算机硬件的使用效率,这就需要对硬件进行管理
b)为了降低工程项目的设计、项目的维护和升级的难度,大幅地提高项目开发效率。
前面说高级语言提高了代码的编写效率,这里又说OS提高了项目的开发效率。实际上,前者是站在了代码具体实现的角度,而后者是站在了项目宏观筹划的角度来说的(主要运用了分层思想)。从下图我们可以窥见下OS与底层硬件以及上层应用程序之间的角色关系是怎样的。

这里写图片描述

按照上图的布局,优点如下:
a)OS可以专门的负责管理的硬件,最大限度的提高了硬件的使用效率。
b)分层思想,做应用程序算法实现的人,不必再关心硬件问题,而做底层硬件驱动的人也不必
关心上层应用的具体实现,各自调用自己的接口即可,从而为大型工程项目的实现带来了可能。
c)由于上层和底层的分离,所以为夸平台的实现带来了一定便利,同一个应用程序可以运行在
不同的硬件平台上,只要他们使用相同的OS。
但任何事物总是有缺点的,我们无非总是在权衡利大于弊还是弊大于而已。当然使用操作系统肯定是利大于弊,但是总会有缺点的,那么缺点就是接口(不管是上层还是下层的)的使用比较繁琐,难于操作。
如果我们站在OS实现目的的角度来说,它的目的是为了:
a)为了提高计算机硬件的使用效率
b)提高应用项目的开发、维护和升级的效率。
为了实现这两个目的,OS对上和对下都应提供相应的服务,所以当我们站在OS是服务者的角度来说,它提供如下服务。
a)对上层提供应用编程接口,实现对上层的服务
b)对下层硬件提供驱动接口,实现对下层硬件管理的服务
    3.2、linux 系统编程涉及的基本章节
我们知道程序运行起来后就变成了进程,有了OS后,程序是运行在操作系统上的。OS对上层应用程序提供服务,也可以说成是为进程提供服务。
所以系统编程这门课其实主要就是学会,如何调用应用接口(函数调用),从而取得OS提供的各种服务(有些服务只需要调用库函数,不需要系统调用)。所以系统便的目的其实就是为了通过,调用各种库或系统提供的API接口来获取相应的服务,为谁提供服务呢?为我们的进程提供服务。
主要涉及章节知识如下图:

这里写图片描述

(以上章节的内容见另外的博文)
4、linux系统编程具体会涉及的一些知识和概念
4.1、登陆
4.1.1、涉及口令文件读写
    口令文件(/etc/passwd)中存入了每个用户的用户信息,具体包含的用户信息如下:
    用户名:密码:用户ID:组ID:用户说明:家目录路径:该用户所用shell类型
4.1.2、涉及shell
a)、Bourne shell(/bin/sh):几乎都支持
b)、KornShell(/bin/ksh):Bourne shell后继者,SVR4支持
c)、C shell(bin/csh):伯克利开发,所有的BSD版本和AT&T的V版本和SVR4都支持
我的用户名叫linux,运行cat /etc/passwd,结果如下:
........
pulse:x:496:494:PulseAudio System Daemon:/var/run/pulse:/sbin/nologin
gdm:x:42:42::/var/lib/gdm:/sbin/nologin
linux:x:500:500:linux:/home/linux:/bin/bash   //我的用户名
smb:x:501:501::/home/smb:/bin/bash
dhcpd:x:177:177:DHCP server:/:/sbin/nologin
从上面红色字体的末尾可以看出,我的linux这个用户默认用的是/bin/bash这个shell。
我们再运行 ls /bin/*sh -al 命令,显示结果如下:
-rwxr-xr-x. 1 root root 874184 Dec  2  2011 /bin/bash
lrwxrwxrwx. 1 root root      4 Jan 12  2012 /bin/csh -> tcsh
-rwxr-xr-x. 1 root root 102216 Nov 11  2010 /bin/dash
lrwxrwxrwx. 1 root root     21 Jan 12  2012 /bin/ksh -> /etc/alternatives/ksh
-rwxr-xr-x. 1 root root 242536 Sep 23  2011 /bin/mksh
lrwxrwxrwx. 1 root root      4 Jan 12  2012 /bin/sh -> bash  //bash等价于/bin/bash
-rwxr-xr-x. 1 root root 380456 Dec  8  2011 /bin/tcsh
从上面我们可以看出,链接文件/bin/sh 指向了/bin/ bash ,因此可以看出我的linux这个账号
用的是Bourne shell(/bin/sh)。
4.2、目录和文件
4.2.1、文件系统
利用树形结构(根是root)的层次安排,实现对文件的组织和管理。目录是一种特殊的文件,目录中包含的每个目录项说明了该目录中每个文件的属性(暂时可这么认为),我们后面会学习
state、fstate等函数对文件属性进行操作。文件属性包括文件类型、文件长度、所属用户、操作
权限、修改时间等信息。
4.2.2、文件名
文件的名称,名中不能包含‘/’和空格字符。新创建的目录中自动创建(.)和(..),一个点代表当前目录,两个点代表上一级父目录,根目下点和点点都均指当前目录。我们后面会学习很多不同种类的函数,以实现对各种文件的各式操作。
4.3.3、路径名
0个或多个斜杠(/)加目录文件名或其它文件名的组合。直接/开头,称为绝对路径,路径
起点是以root目录做为参照。点或点点开头的称为相对路径,相对路径起点是以当前工作目录 作为参照,相对路径仍然是绝对路径,因为当前目录本身就是以root目录作为参照的。
4.3.4、起始目录或称家目录
账户登录成功后,工作路径将会自动跳转到家目录,家目录的路径说明存放在了口令文件中,请看前面的3.1.2中cat /etc/passwd命令执行后,会发现linux用户的家目录路径是/home/linux。
4.4.5、当前工作目录
每一个进程都有一个当前工作目录,当前工作目录就是运行该可执行程序的目录。当前工作目录的位置是以根目录为参照的,是一个绝对路径,而所有的相对路径又是以当前目录所在位置为参照,所以说相对路径是一个相对的绝对路径。
/home/linux/shang_qian/1402/print_cpntrl/a.out
蓝色部分:可执行程序的所在路径
红    色:可执行程序的名称
[linux@localhost 1402]$./print_cpntrl/a.out
打印出来的当前工作目录为:/home/linux/shang_qian/1402
我们后面会学习到相应的函数来获取当前工作目录的
4.3、输入和输出
4.3.1、文件描述符
一个非负的正整数,当打开一个文件时,内核就会返回一个本进程当前未用的最小描述符,用于标识一个正在被操作的文件,之后的读写等操作均用文件描述符进行。
4.3.2、标准输入,标准输出,标准出错输出文件
之所以含有标准二字,我们可以这么理解,第一,涉及标准io函数,其二,他们涉及对键盘显示器的操作,而键盘和显示器是人机交互的标准配备,没有它们就不能实现人机交互。
标准输入的预定义文件指针是stdin,标准输出的预定义文件指针是stdout,标准出错输出的预定义文件指针是stderr。前面两个是行缓冲,第三个是无缓冲。这三个文件在内核启动时就被自动打开,之后的每个进程都将会继承这几个打开的文件指针,stdin对应文件描述符0,stdout对应文件描述符1,stderr对应文件描述符2,。
文件指针被用于标准io函数,而文件描述符被用于文件io函数。标准io函数是对文件io系统函数做的进一步的库函数封装。每个文件指针指向一个结构体,这个结构体被用来描述打开文件,而这个结构体中最后一项就保存了文件io操作需要用到的文件描述符,标准io函数向下调用实际上最终调用的还是文件io函数。
4.3.4、不用缓存的io函数
不用缓存的io指的就是文件io函数。这里所说的不用缓存,并不是说真的就用不到缓存了,这里是相对于标准io的库缓存而言的。涉及函数有open、read、write,close,lseek等,这些我们会在后面文件io的章节里陆续地接触到。
4.3.5、带缓存的io函数
带缓存的io指的就是标准io函数,它实际上就是对文件io做了进一步的封装,提供了一种对不带缓存的io函数的带缓存的界面,标准io会帮我们选取一个最佳的缓存长度,以提高对文件io系统调用的效率。涉及函数如fopen、printf、fprintf、scanf、fscanf等等
4.3.6、标准io和文件io对比图

这里写图片描述

从上图中我们可以看出,标准io多了一个库缓存,所以被称为了带缓存的io。标准io极大的提高了文件io的调用效率,同时标准io的标准化为实现软件在不同操作系统如windows和linux之间跨平台运行提供了基础。
在不同的OS下,使用相同的c标准定好的标准io库函数,向下调用的确是不同系统的文件io系统调用函数。
4.4、程序和进程
4.4.1、程序
静态地存放于磁盘上的可执行文件,需要使用exec函数族函数调用内核中的加载器将程序加载到虚拟内存中运行。
4.4.2、进程和进程ID
我们可以简单认为,在内存中动态运行的程序就是进程,实际上这句话是存在一定的问题的,因为学到到了后面我们就会知道,一个程序运行起来后可能会产生出很多的进程,但是目前我们为了能够初步了解进程的概念,我们可以认为每个运行起来的程序就是一个进程,不过某些系统喜欢将运行起来的程序称为一个任务(一个任务可能包含很多进程),这些只不过是些概念上的差异,这些差异并不会妨碍我们接下来的讨论。
内核为了管理进程,为每个进程都编排了一个非负的整形数,这个数就是进程ID。我们后面会学习getpid()函数,该函数专门用来获取某进程的进程ID。
4.4.3、进程控制
进程控制的目的就是为了让可执行程序动态运行起来,并对其进行一定的控制。涉及函数主要有fork、exec、wait,wairpid等,exec是个函数族,共6个,其中5个是变体。对于这些函数,后面相关的章节将会对其进行详细地介绍。
4.5、ANSI C
目前我们linux下所用的c标准基本都是ANSI C 。
4.5.1、ANSI C 介绍
ANSI C是美国国家标准协会(ANSI)对c语言发布的标准。于此相对应的是ISO(国际化标准组织)。
C 89标准:1983年,美国国家标准协会组成了一个委员会,X3J11,为了创立C的一套标准。经过漫长而艰苦的过程,该标准于1989年完成,并在作为ANSI X3.159-1989 "Programming Language C"正式生效。这个版本的语言经常被称作"ANSI C",或有时称为"C89"(为了区别C99)。   C 90标准:在1990年,ANSI C标准(带有一些小改动)被美国国家标准协会采纳为ISO/IEC 9899:1990。这个版本有时候称为C90。因此,C89和C90通常指同一种语言。
C 99标准:在2000年三月,ANSI采纳了ISO/IEC 9899:1999标准。这个标准通常指C99。
C 11标准:在2011年12月,ANSI采纳了ISO/IEC 9899:2011标准。这个标准通常即C11,它是C程序语言的现行标准。
4.5.2、函数原型
ANSI C标准对各类函数定下了标准的函数原型,提供给编程人员调用。函数原型是ANSI C标准的组成部分,比如下面的这些例子。
4.5.2.1、标准io库函数原型
头文件<stdio.h>包含了很多标准io函数的函数原型,例如fopen、printf、scanf等函数,
int printf(const char *format, ...);
int scanf(const char *format, ...);
FILE *fopen(const char *path, const char *mode);
4.5.2.1、系统调用函数的函数原型
头文件<unistd.h>包含了很多系统调用函数函数的函数原型,如open,read,write等:
int open(const char *pathname, int flags, mode_t mode);
ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void *buf, size_t count);
这些系统函数的目的主要是用于实现系统调用,但是我们看到的这些函数原型只不过是有ANSI C提供的函数接口而已,接口里面通过执行汇编软中断指令产生异常中断并且顺带传下系统调用号和相应参数。进行软中断的异常中断处理时,通过传递下来的系统调用号,会找到内核中真正的系统调用函数,将参数带入后,真正的系统调用才正式开始。
以上三个系统调用对应的内核中实际的系统调用函数的原型在内核的calls.s文件中,如下图所示:
Calls.s  
/* 0 */ .long   sys_ni_syscall        //系统调用号0
        .long   sys_exit                //系统调用号1
        .long   sys_fork_wrapper
        .long   sys_read
        .long   sys_write
/* 5 */ .long   sys_open                //系统调用号5
其它系统调用
4.5.2.2、其它的函数原型
<string.h>、<math.h>等等头文件中包含了其它的库函数或系统函数的函数原型,这里不再赘述。
4.6、原始系统数据类型
我们讲read函数的原型时,我们发现它的返回值类型是ssize_t,而且以后我们还会接触到很多xxx_t的奇怪数据类型。这些数据类型看似不懂,但实际上他们都是通过typedef转换过来的,其中_t应该指的就是typedef的意思。这些_t类型的目的如下:
a)提高类型名字的可辨识度,如ssize_t,一看就知道一定和读或写的字节数有关。
b)提高维护的效率,并且能够跨平台使用,比如某个函数的返回值类型在某个平台下用的long
型,而在其他平台用的是long long型,那么就可以用typedef将这两种类型统一为某xxx_t  类型。在不同的平台下时,同一个xxx_t名字却对应了不同的类型,实现跨平台的使用。
c)自定义全新的复杂数据类型,如下所示:
#include <stdio.h>
typedef char ww[100];
int main(void)
{
        int i = 0;
        ww a = {0};         
        for(i=0; i<5; i++) printf("a[%d] = %d\n", i, a[i]);
        return 0;
}
4.7、出错处理
当一个函数调用失败时,我们总是希望打印出导致函数调用失败的具体原因。例如open函数,大概有15种原因可能会导致函数调用失败,每一种失败原因都对应了一个整形的errno号,其中errno= =0时,代表函数调用成功,每一种错误号可以被strerror()函数映射为我们能够识别的字符串。
errno变量以及各种错误整形常量值存放在了errno.h中,具体映射的转换实现如下例:
#include <stdio.h>
#include <errno.h>
int main(void)
{
        FILE *fp = NULL;
        fp = open("file", "w+");
        if(NULL == fp) printf(stderr, "%s is fail: %s\n", "fopen", strerror(errno));
        return 0;
}
如果用strerror()函数转换errno为字符串的话,必须包含#include <errno.h>,其实我们还有其它的方式来实现错误原因的打印,那就是利用perror()函数。
#include <stdio.h>
int main(void)
{
        FILE *fp = NULL;
        fp = open("file", "w+");
        if(NULL == fp)  perror("fopen is fail");
        return 0;
}
perror函数会主动帮我们加入:和\n,缺点是无法格式化打印,如perror("%s  is fail", “fopen”)是错误的。这个函数不需要加入<errno.h>头文件,其实我们可以认为perror(“xxxx”)函数就是对 printf(stderr, "%s: %s\n", "xxxx", strerror(errno)的一种封装。
4.8、用户标识
4.8.1、用户ID
我在linux下的某用户名叫做linux,我们人能识别的是linux这个字符串,但是内核却喜欢利用非负整数对各个用户进行管理,所以每个用户,就比如以我的linux来说,内核会为其分配一个非负整数,这个数就被称为用户ID。字符串linux是给人看的,而用户ID是给计算机进行管理用的,但是它们之间有着一一对用的映射关系,通过前面的学习我们已经清楚,用户ID作为用户信息的一部分被存在了/etc/passwd口令文件中。
4.8.2、组ID
多个用户可以组成一组,共同的完成某个工程项目,这一组的组员用可以共享该工程项目中的数据文件,组外的其它用户对该组文件访问权限就会受到限制,从而实现了按权限进行文件数据的共享。组长用户的用户ID就是组ID,正常情况下,每个用户自己自成一组,组长是自己,组员也是自己。
4.8.3、添加组ID
如果某个用户参与了很多个不同项目设计,这样他可能会涉及经常以较高的组权限访问很多组的数据文件,这样会导致他频繁地进行不同组的切换,非常麻烦。针对这个情况我们可以通过设置添加组ID来解决,这样一个用户可以同时属于很多个不同的组,他就可以在不进行组切换的情况下,实现对不同组数据文件的访问。
4.9、信号
信号是一种信息量受限的一种异步通信方式,它应该是属于本机进程间通信方式的一种,信号是一种非常重要的机制,而且使用频度也是非常高的,我们后面会有相关章节专门对信号进行详细地讲解。
4.10、linux时间值
总体分两种不同的时间值:
4.10.1、日历时间
从国际时间1970年1月1日00:00:00到现在累积的总秒数,计算公元纪年时,需要将此总秒数转换成年、月、日、时、分、秒后,再加上1970年1月1日00:00:00即可得到现在的公元纪年。
4.10.2、进程时间或称为CPU时间
进程时间包括如下三方面:
a)时钟时间:描述的是进程总的运行时间的跨度
b)用户CPU时间:程序在用户空间实际运行的时间
c)系统CPU时间:进行系统调用时,系统函数实际运行的时间
实际运行时间 = 用户CPU时间 + 系统CPU时间
实际运行时间 <= 时钟时间 
为了很好的理解进程时间,我们这里打个比方,一个人完成一项工作,但是每天为这项工作只付出了2小时,其它时间去做其它事情,3个月后才完成了这项工作,那么这些时间有关系:
时钟时间 = 3个月
实际运行时间 = 90天*2小时
实际运行时间 <= 时钟时间
4.11、API接口中的库函数调用和系统调用之区别
我们前面为了叙述的方便,总是说应用程序是通过调用系统调用来获取系统服务的。但是实际情况是,有一些服务可能会调用到系统调用,而有些呢只需要调用纯粹的库函数即可,我们将这些接口统称为API(Application Program Interface),那么不同的接口间又有什么区别?如下图:

这里写图片描述

a)方式1:直接极性系统调用,这是最常见的方式,如文件io函数open、read和write等
b)方式2:先调用库函数接口,库函数向下再调用系统调用,如标准io的printf函数,先调用
标准io的库函数printf,然后库函数再调用文件io的write函数。所以标准io实际上还是通 
过借助文件io函数的调用来实现的。
c)方式3:只调用库函数,无需调用系统调用,因为库函数已经能够完成所需服务,不需要系
    统的参与。如strlen等字符串处理函数,像这类为字符串提供的运算服务,库函数足以胜任。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值