linux下的c语言【入门】

来源:互联网 发布:寅时三刻网络 编辑:程序博客网 时间:2024/05/17 17:15

本文是feixiaoxinglinux下的C语言开发系列 的阅读笔记。

大部分内容是来自原文,中间补充了一点网络上检索到的资料和自己的理解。

makefile

语法规则:
目标:依赖文件(多个文件以空格间隔)
[TAB]编译命令
举例:
main:main.o test1.o test2.o
     gcc -o main main.o test1.o test2.o
main.o:main.c test1.h test2.h
     gcc -c main.c 
test1.o:test1.c test1.h
     gcc -c test1.c
test2.o:test2.c test2.h
     gcc -c test.c

clean:
     rm -rf test
     rm -rf *.o

makefile 三个常用变量:$@   $^     $<
$@----目标文件
$^  ----所有依赖文件
$< ----第一个依赖文件
所以可以将上面的示例简化:
main:main.o test1.o test2.o
     gcc -o $@ $^
main.o:main.c test1.h test2.h
     gcc -c $<  
test1.o:test1.c test1.h
     gcc -c $<  
test2.o:test2.c test2.h
     gcc -c $<  

makefile还有一个默认规则:
..c.o:
     gcc -c $<
表示所有的.o文件都依赖于相应的.c文件
这样示例可以再次简化:
main:main.o test1.o test2.o
     gcc -o $@ $^
..c.o:
     gcc -c $<

gdb调试

编译是要添加-g选项,编译示例:gcc test.c -g -o test
然后执行gdb test进入调试模式
(01) 首先,输入gdb test(02) 进入到gdb的调试界面之后,输入list,即可看到test.c源文件(03) 设置断点,输入 b main (04) 启动test程序,输入run (05) 程序在main开始的地方设置了断点,所以程序在printf处断住(06) 这时候,可以单步跟踪。s单步可以进入到函数,而n单步则越过函数(07) 如果希望从断点处继续运行程序,输入c(08) 希望程序运行到函数结束,输入finish(09) 查看断点信息,输入 info break(10) 如果希望查看堆栈信息,输入bt(11) 希望查看内存,输入 x/64xh + 内存地址(12) 删除断点,则输入delete break + 断点序号(13) 希望查看函数局部变量的数值,可以输入print + 变量名
(14)希望修改内存值,直接输入 print  + *地址 = 数值(15) 希望实时打印变量的数值,可以输入display + 变量名(16) 查看函数的汇编代码,输入 disassemble + 函数名
(17) 退出调试输入quit即可



AT&T汇编语言

不理解作者为什么安排这么一小节!

静态库

一般库函数分为静态库和动态库。静态库是必须要链接到可执行文件中去的;而动态库是不需要链接到最后的可执行文件的。
编译静态库文件的方法:
1、首先生成.o文件:gcc -c staticlib.c -o staticlib.o
2、然后利用ar命令生成静态库:ar rc staticlib.a staticlib.o
如何使用静态库:
假设staticlibtest.c使用了该静态库的库函数,则在编译staticlibtest.c时:
gcc staticlibtest.c -o staticlibtest ./staticlib.a

动态库

windows下动态库文件的扩展名为.dll,在linux下为.so。
和静态库相比,动态库可以共享内存资源,这样可以减少内存消耗。
另外,动态库需要经过操作系统加载器的帮助才能被普通执行文件发现,所以动态链接库可以减少链接次数。
linux下编译动态链接库的方法:
gcc -shared -fPIC -o lib.so lib.c
如何使用动态链接库:
假设libtest.c使用了该动态库的库函数,则在编译libtest.c时:
gcc libtest.c -o libtest ./lib.so

定时器

linux中定时器就相当于系统每隔一段时间给进程发送一个定时信号,我们所要做的就是定义一个信号处理函数。
通过signal()函数注册信号的处理函数或回调函数,例如signal(SIGALRM, handle_timer)
其中SIGALRM信号可以由setitimer(ITIMER_REAL,&itv,&oldtv)函数发出!
setitimer函数支持三种计数器形式:
ITIMER_REAL:以系统真实时间来计算,送出SIGALRM信号
ITIMER_VIRTUAL:以进程在用户态下花费的时间来计算,送出SIGVTALRM信号
ITIMER_PROF:以进程在用户态和内核态花费的时间来计算,送出SIGPROF信号

而第二个参数itv是结构体itimerval的实例
struct itimerval{
     struct timeval it_interval;
     struct timeval it_value;
}
it_value 是减少的时间,当该值为0时,就发出相应的信号,然后再将it_value设置为it_interval,所以it_interval就是定时器的时间间隔。
第三个参数可以为NULL。
这里有个小技巧,在初始化itv.it_value时,可以设置任意值,即第一次启动计时器的延迟时间可以为任意值。

自动编译工具

我们下载一些开源软件后,进行编译安装时,一般只需要执行
./configure
make
make install
之所以这么简单,是因为工程使用了自动化编译工具生成了相应的configure文件。
这一小节介绍了如何使用autoconf和automake这两个工具创建工程的configure文件。
首先创建Makefile.am
然后执行autoscan,生成configure.scan,将其修改为configure.in
然后再修改这个脚本文件
接下来依次执行aclocal和autoheader
接着创建四个文件README,NEWS,AUTHORS和ChangeLog
然后依次执行automake -a、autoconf

输入./configure 即可生成最终的Makefile
如果需要编译,输入make
需要安装则make install
需要发布软件包 make dist

进程创建

这里仅介绍fork创建进程,由fork创建的子进程与父进程相比,除了代码共享之外,堆栈数据和全局数据均是独立的。
说是共享,其实是将父进程做了一份拷贝,即子进程,fork发送后父子进程中只有pid不同。
fork函数:
函数原型:pid_t  fork();     
                              //1.创建进程
                              //2.新进程的代码是什么:克隆父进程的代码,而且克隆了执行的位置.(从父进程复制过来的代码,fork之前的代码不会再子进程中执行,子进程只会执行从父进程复制过来的fork以后的代码)
                              //3.在子进程不调用fork所以返回值=0;(pid=0为子进程的)
                              //4.父子进程同时执行(fork之后是父进程先执行还是子进程先执行,这是取决于cpu调用算法的)
除了fork,还可以用system、popen、execl和execlp创建进程:
system函数:
函数原型:int system(const char*filename);
                //建立独立进程,拥有独立的代码空间,内存空间
                //等待新的进程执行完毕,system才返回.(阻塞)
         system:创建一个堵塞的新进程,新进程结束后,system才返回
popen函数:#include<stdio.h>
函数原型:    FILE * popen ( constchar * command , const char * type );
                    int pclose ( FILE * stream );
                    popen:创建子进程
                    在父子进程之间建立一个管道
                    command: 是一个指向以 NULL 结束的 shell 命令字符串的指针。这行命令将被传到 bin/sh 并使用 -c 标志,shell将执行这个命令。
                    type: 只能是读或者写中的一种,得到的返回值(标准I/O 流)也具有和 type 相应的只读或只写类型。如果 type 是 “r” 则文件指针连接到 command 的标准输出;如果 type 是 “w” 则文件指针连接到command 的标准输入。
                    返回值:如果调用成功,则返回一个读或者打开文件的指针,如果失败,返回NULL,具体错误要根据errno判断

execl  execle:
          代替当前进程的代码空间中的代码数据,函数本身不创建新的进程。
excel函数:
int execl(const char * path,const char*arg,….);
第一个参数:替换的程序
第二个参数…..:命令行
                      命令行格式:命令名 选项参数
                      命令行结尾必须空字符串结尾
案例:
                     使用exec执行一个程序。
                     体会:  *是否创建新的进程?没有
                              *体会execl的参数的命令行的格式
                              *体会execl与execlp的区别(execl只当前路径)(不是当前路径必须加绝对路径)
                                                               execlp使用系统的搜索路径
                               *体会execl替换当前进程的代码

进程等待

fork创建子进程后,子进程和父进程同时执行,具体执行的先后顺序取决于CPU调度算法,但总会有一个进程先执行完,在shell窗口中体现的结果如下:

父进程结束后直接退出到shell提示符界面,而这是子进程才开始运行,而子进程结束后并不能退出到shell的命令提示符界面,出现截图的情况:需要敲下回车才能再次进入shell命令提示符下。
如果能保证子进程先于父进程结束,就不会出现上述情况,譬如在父进程执行分支中添加sleep(5),再次执行的效果如图:

这是正常的,也是我们期望的程序运行结果和显示,但是sleep并不是一个好的解决方案,因为我们不知道子进程的执行时间,然而可以用wait(NULL)替换sleep(5).
wait函数让父进程等待子进程运行结束后才开始运行 
函数原型:int wait(int *status)
                    如果成功,wait会返回被收集的子进程的进程ID,如果调用进程没有子进程,调用就会失败,此时wait返回-1,同时errno被置为ECHILD。 
                    如果参数status的值不是NULL,wait就会把子进程退出时的状态取出并存入其中, 这是一个整数值(int),指出了子进程是正常退出还是被非正常结束的,以及正常结束时的返回值,或被哪一个信号结束的等信息。

信号处理

信号处理是linux程序的一个特色,用信号处理模拟操作系统的中断功能,对于程序员是个很好的选择。
使用信号处理,只需要定义一个信号处理函数,然后在程序中调用signal函数,将需要处理的信号与处理函数关联起来,当相应的信号出现时,会自动执行信号处理函数。
注意,信号只是用来通知某进程发生了什么事件,并不给该进程传递任何数据(除了信号类型)。
系统调用signal用来设定某个信号的处理方法。该调用声明的格式如下:
void (*signal(int signum, void (*handler)(int)))(int);
在使用该调用的进程中加入以下头文件:
#include <signal.h>

上述声明格式比较复杂,如果不清楚如何使用,也可以通过下面这种类型定义的格式来使用(POSIX的定义):
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
但这种格式在不同的系统中有不同的类型定义,所以要使用这种格式,最好还是参考一下联机手册。

在调用中,参数signum指出要设置处理方法的信号。第二个参数handler是一个处理函数,或者是
SIG_IGN:忽略参数signum所指的信号。
SIG_DFL:恢复参数signum所指信号的处理方法为默认值。
具体的信号类型可参考:http://www.cnblogs.com/taobataoma/archive/2007/08/30/875743.html

管道通信

linux系统本身为进程见通信提供了很多方式,例如管道、共享内存、socket通信、信号、信号量、消息队列等。
管的的使用非常简单,在创建了匿名管道后,只需要从一个管道发送数据、从另外一个管道接受数据即可。
创建匿名管道:
函数原型:int pipe(int fd[2])
参数:fd是文件描述符数组,其中fd[0]表示读端,fd[1]表示写端
写数据时用write(fd[1],"hello pipe!",strlen("hello pipe!"))
读数据用read(fd[0],buffer,LEN),如果管道中没有数据,默认情况下read会被阻塞,可以通过fcntl函数设置O_NONBLOCK标志,当管道中没有数据时,read立即返回-1;同理管道满时,write也存在对应的阻塞情况。
管道通信是单向的,并且最受先进先出的原则
管道是一个无结构,无结构,无固定大小的字节流
管道把一个进程的标准输出和另一个进程的标准输入连接在一起,数据读出后就意味着从管道中移走了,消失了,不能在从管道中读取之前的数据了。
pipe这种管道一般用于两个有亲缘关系的进程之间,例如父子进程。

多线程

线程共享进程的内存空间,虽然各个线程都有自己的堆栈空间,但是线程A依然可以通过地址访问线程B的局部变量(当然这么做其实没什么意义,因为线程A可能随时会结束,B再访问A的局部变量对应的地址空间就没有意义了)。
或者说,每个线程都有自己的堆栈,但它们共用进程的资源。
线程有自己的堆栈和局部变量,但线程没有单独的地址空间,一个线程死掉就等于整个进程死掉,所以多进程的程序要比多线程的程序健壮,但在进程切换时,耗费资源较大,效率要差一些。 
通过测试发现在进程A和进程B中通过malloc申请的小空间(8 byte)不是连续的。
所以线程的局部变量(包括malloc申请的)一般情况下不受其它线程影响。
通过pthread_create创建线程
通过pthread_join等待线程结束

如果在进程中任何一个线程中调用exit()或者_exit(),那么整个进程会终止。
线程正常退出的方法:线程从启动例程中返回(return)、线程被另外的线程终止(kill)、线程自己调用pthread_exit函数

传统的OS原理,相较于进程,线程被抽象成一种耗费较少资源,运行迅速的执行单元。但是linux不像windows或solaris,它没有线程的概念,线程被当作进程实现了,linux本身的进程已经足够轻量,线程是进程间共享资源的手段。
linux先fork和pthread_create函数都是调用clone函数实现的。
linux之所以存在线程机制,一是为迎合windows下的多线程编程习惯;二是为数据共享提供一种手段。
linux系统中进程和线程的区别不是很大,只是调用clone时的参数不同而已。


线程互斥

线程直接通过共享数据空间,提高了运行效率,但数据共享也带来了其它问题:共享数据不能同时被两个线程修改。线程互斥就是来解决这个问题的。
首先声明一个全局的pthread_mutex_t变量mutex,并在主线程中使用phtread_mutex_init(&mutex,NULL)初始化;
然后线程中修改共享数据的代码由pthread_mutex_lock(&mutex)和pthread_mutex_unlock(&mutex)包裹。

此外linux下还有条件变量和信号量等机制实现线程间的同步与互斥

同步与互斥区别?这个问题不好回答。

linux下没有临界区的概念。
 

















0 0
原创粉丝点击