linux c 编程模板总结(二)
来源:互联网 发布:windws10固态硬盘优化 编辑:程序博客网 时间:2024/05/01 10:36
(1)完善的编译环境,包括gcc、as、ld等编译、链接工具
(2)强大的调试环境,主要是gdb工具
(3)丰富的自动编译工具,主要是make工具
(4)多样化的os选择,ubuntu、redflag等等
(5)浩瀚的开源代码库
- #include <stdio.h>
- int iterate(int value)
- {
- if(1 == value)
- return 1;
- return iterate(value - 1) + value;
- }
- int main()
- {
- printf("%d\n", iterate(10));
- return 1;
- }
当然, 还会有一些朋友对程序的反汇编感兴趣,那么他需要两个步骤:1、gcc hello.c -g -o hello;2、objdump -S -d ./hello。之所以在gcc编译的时候加上-g是为了添加调试信息,objdump中的-S选项是为了在显示汇编代码的时候同时显示原来的C语言源代码。
- int iterate(int value)
- {
- 8048374: 55 push %ebp
- 8048375: 89 e5 mov %esp,%ebp
- 8048377: 83 ec 08 sub $0x8,%esp
- if(1 == value)
- 804837a: 83 7d 08 01 cmpl $0x1,0x8(%ebp)
- 804837e: 75 09 jne 8048389 <iterate+0x15>
- return 1;
- 8048380: c7 45 fc 01 00 00 00 movl $0x1,0xfffffffc(%ebp)
- 8048387: eb 16 jmp 804839f <iterate+0x2b>
- return iterate(value -1) + value;
- 8048389: 8b 45 08 mov 0x8(%ebp),%eax
- 804838c: 83 e8 01 sub $0x1,%eax
- 804838f: 89 04 24 mov %eax,(%esp)
- 8048392: e8 dd ff ff ff call 8048374 <iterate>
- 8048397: 8b 55 08 mov 0x8(%ebp),%edx
- 804839a: 01 c2 add %eax,%edx
- 804839c: 89 55 fc mov %edx,0xfffffffc(%ebp)
- 804839f: 8b 45 fc mov 0xfffffffc(%ebp),%eax
- }
- 80483a2: c9 leave
- 80483a3: c3 ret
首先编写add.c文件,
- #include "test.h"
- #include <stdio.h>
- int add(int a, int b)
- {
- return a + b;
- }
- int main()
- {
- printf(" 2 + 3 = %d\n", add(2, 3));
- printf(" 2 - 3 = %d\n", sub(2, 3));
- return 1;
- }
- #include "test.h"
- int sub(int a, int b)
- {
- return a - b;
- }
- #ifndef _TEST_H
- #define _TEST_H
- int add(int a, int b);
- int sub(int a, int b);
- #endif
- test: add.o sub.o
- gcc -o test add.o sub.o
- add.o: add.c test.h
- gcc -c add.c
- sub.o: sub.c test.h
- gcc -c sub.c
- clean:
- rm -rf test
- rm -rf *.o
编写代码过程中少不了调试。在windows下面,我们有visual studio工具。在linux下面呢,实际上除了gdb工具之外,你没有别的选择。那么,怎么用gdb进行调试呢?我们可以一步一步来试试看。
- #include <stdio.h>
- int iterate(int value)
- {
- if(1 == value)
- return 1;
- return iterate(value - 1) + value;
- }
- int main()
- {
- printf("%d\n", iterate(10));
- return 1;
- }
调试的步骤基本如下所示,
(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即可
同样是x86的cpu,但是却可以用不同形式的汇编语言来表示。在window上面我们使用的更多是intel格式的汇编语言,而在Linux系统上面使用的更多的常常是AT&T格式的汇编语言。那什么是AT&T格式的汇编代码呢?我们可以写一个试试看。
- .data
- message: .string "hello!\n"
- length = . - message
- .text
- .global _start
- _start:
- movl $length, %edx
- movl $message, %ecx
- movl $1, %ebx
- movl $4, %eax
- int $0x80
- movl $0, %ebx
- movl $1, %eax
- int $0x80
在as命令当中,由于我们使用了-gstabs选项,因此在hello执行文件中是包含调试信息的。所以,如果想单步调试的朋友可以输入gdb hello进行调试。
那么,hello执行文件反汇编的代码又是什么样的呢?我们可以输入objdump -S -d hello查看一下。
- 08048074 <_start>:
- .text
- .global _start
- _start:
- movl $length, %edx
- 8048074: ba 08 00 00 00 mov $0x8,%edx
- movl $message, %ecx
- 8048079: b9 9c 90 04 08 mov $0x804909c,%ecx
- movl $1, %ebx
- 804807e: bb 01 00 00 00 mov $0x1,%ebx
- movl $4, %eax
- 8048083: b8 04 00 00 00 mov $0x4,%eax
- int $0x80
- 8048088: cd 80 int $0x80
- movl $0, %ebx
- 804808a: bb 00 00 00 00 mov $0x0,%ebx
- movl $1, %eax
- 804808f: b8 01 00 00 00 mov $0x1,%eax
- int $0x80
- 8048094: cd 80 int $0x80
- ret
- 8048096: c3 ret
既然说到了库函数,那么一般来说库函数分为两种方式:静态库和动态库。两者的区别其实很小,静态库是必须要链接到执行文件中去的,而动态库是不需要链接到最后的执行文件中的。怎么理解呢?也就是说,对于最后的执行文件而言,你是否删除静态库无所谓。但是,一旦你删除了动态库,最后的执行文件就玩不转了。
今天我们讨论的问题是静态库。为了显示windows和linux创建静态库之间的差别,我们首先在windows上面利用Visual C++6.0创建一个静态库。源文件的代码很简单,
- #include "test.h"
- int add(int a, int b)
- {
- return a + b;
- }
- #ifndef _TEST_H
- #define _TEST_H
- int add(int a, int b);
- #endif
如果你需要在windows上面创建一个静态库,那么你需要进行下面的操作,
(1)打开visual C++ 6.0工具,单击【File】-> 【New】->【Projects】(2)选择【Win32 Static Library】,同时在【Project Name】写上项目名称,在【Location】选择项目保存地址
(3)单击【Ok】,继续单击【Finish】,再单击【Ok】,这样一个静态库工程就创建好了
(4)重新单击【File】->【New】->【Files】,选择【C++ Source Files】,
(5)选中【Add to pproject】,将源文件加入到刚才创建的工程中去,在File中输入文件名+.c后缀
(6)重复4、5的操作,加入一个文件名+.h头文件
(7)分别在头文件和源文件中输入上面的代码,单击F7按钮,即可在Debug目录中生成*.lib静态库文件
那么,在linux下面应该怎么运行呢?其实很简单,两条命令解决,
(1)首先生成*.o文件,输入gcc -c test.c -o test.o
(2)利用ar命令生成静态库,输入ar rc libtest.a test.o
此时如果还有一个hello.c文件使用到了这个静态库,比如说 ,
- #include <stdio.h>
- #include "test.h"
- int main()
- {
- printf("%d\n", add(2, 3));
- return 1;
- }
其实也很简单,输入一个简单的命令就可以生成执行文件了,
(1)首先输入gcc hello.c -o hello ./libtest.a
(2)输入./hello,验证生成的执行文件是否正确
(3)朋友们可以删除libtest.a文件,重新输入./hello,验证执行文件是否可以正常运行
- #include <stdio.h>
- #include "test.h"
- int main()
- {
- printf("%d\n", add(2, 3));
- return 1;
- }
其实也很简单,输入一个简单的命令就可以生成执行文件了,
(1)首先输入gcc hello.c -o hello ./libtest.a
(2)输入./hello,验证生成的执行文件是否正确
(3)朋友们可以删除libtest.a文件,重新输入./hello,验证执行文件是否可以正常运行
动态链接库不是linux独有的特性,在windows下面也存在这样的特性。一般来说,windows下面的动态连接库是以*.dll作为结尾的,而linux下面的动态连接库是以*.so结尾的。和静态链接库相比,动态连接库可以共享内存资源,这样可以减少内存消耗。另外,动态连接是需要经过操作系统加载器的帮助才能被普通执行文件发现的,所以动态连接库可以减少链接的次数。有了这个特点,我们就不难发现为什么很多软件的补丁其实都是以动态库发布的。
那么,在Linux上动态库是怎么生成的呢?
- #include "test.h"
- int add(int a, int b)
- {
- return a + b;
在Linux下面,编写makefile是一件辛苦的事情。因此,为了减轻程序员编写makefile的负担,人们发明了autoconf和automake这两个工具,可以很好地帮我们解决这个问题。
我们可以通过一个简单的示例来说明如何使用配置工具。
(1)首先,编写源文件hello.c。(2)接下来,我们需要创建一个Makefile.am,同时编写上脚本。}- #include <stdio.h>
- int main(int argc, char** argv[])
- {
- printf("hello, world!\n");
- return 1;
- }
- #ifndef _TEST_H
- #define _TEST_H
- int add(int a, int b);
- #endif
- #include <stdio.h>
- #include "test.h"
- int main()
- {
- printf("%d\n", add(2, 3));
- return 1;
- }
这个时候,有的朋友就会问了,那在windows下面dll应该怎么编写呢?其实也不难,只要在test.h上面稍作改变即可。其他的步骤和静态库的操作是基本类似的。
- #ifndef _TEST_H
- #define _TEST_H
- #ifdef USR_DLL
- #define DLL_API _declspec(dllexport)
- #else
- #define DLL_API _declspec(dllimport)
- #endif
- DLL_API int add(int a, int b);
- #endif
在Linux下面,编写makefile是一件辛苦的事情。因此,为了减轻程序员编写makefile的负担,人们发明了autoconf和automake这两个工具,可以很好地帮我们解决这个问题。
我们可以通过一个简单的示例来说明如何使用配置工具。
(1)首先,编写源文件hello.c。
- #include <stdio.h>
- int main(int argc, char** argv[])
- {
- printf("hello, world!\n");
- return 1;
- }
- SUBDIRS=
- bin_PROGRAMS=hello
- hello_SOURCES=hello.c
修改脚本AC_INIT(FULL-PACKAGE-NAME, VERSION, BUG-REPORT-ADDRESS)
为AC_INIT(hello, 1.0, feixiaoxing@163.com)
同时,在AC_CONFIG_HEADER([config.h])后面添加
AM_INIT_AUTOMAKE(hello, 0.1)
(4)依次输入aclocal命令、autoheader命令
(5)创建4个文件,分别为README、NEWS、AUTHORS和ChangeLog
(6)依次输入automake -a、autoconf命令
(7)输入./configure,生成最终的Makefile
(8)如果需要编译,输入make;如果需要安装, 输入make install;如果需要发布软件包,输入make dist
在Linux下面,创建进程是一件十分有意思的事情。我们都知道,进程是操作系统下面享有资源的基本单位。那么,在Linux下面应该怎么创建进程呢?其实非常简单,一个fork函数就可以搞定了。但是,我们需要清楚的是子进程与父进程之间除了代码是共享的之外,堆栈数据和全局数据均是独立的。
- #include <unistd.h>
- #include <stdio.h>
- #include <stdlib.h>
- #include <math.h>
- #include <errno.h>
- #include <sys/types.h>
- #include <sys/wait.h>
- int main()
- {
- pid_t pid;
- if(-1 == (pid = fork()))
- {
- printf("Error happened in fork function!\n");
- return 0;
- }
- if(0 == pid)
- {
- printf("This is child process: %d\n", getpid());
- }
- else
- {
- printf("This is parent process: %d\n", getpid());
- }
- return 0;
- }
所谓进程等待,其实很简单。前面我们说过可以用fork创建子进程,那么这里我们就可以使用wait函数让父进程等待子进程运行结束后才开始运行。注意,为了证明父进程确实是等待子进程运行结束后才继续运行的,我们使用了sleep函数。但是,在linux下面,sleep函数的参数是秒,而windows下面sleep的函数参数是毫秒。
- #include <stdio.h>
- #include <stdlib.h>
- #include <unistd.h>
- int main(int argc, char* argv[])
- {
- pid_t pid;
- pid = fork();
- if(0 == pid)
- {
- printf("This is child process, %d\n", getpid());
- sleep(5);
- }
- else
- {
- wait(NULL);
- printf("This is parent process, %d\n", getpid());
- }
- return 1;
- }
- [root@localhost fork]# ./fork
- This is child process, 2135
- This is parent process, 2134
信号处理是linux程序的一个特色。用信号处理来模拟操作系统的中断功能,对于我们这些系统程序员来说是最好的一个选择了。要想使用信号处理功能,你要做的就是填写一个信号处理函数即可。一旦进程有待处理的信号处理,那么进程就会立即进行处理。
- #include <stdio.h>
- #include <stdlib.h>
- #include <signal.h>
- int value = 0;
- void func(int sig)
- {
- printf("I get a signal!\n");
- value = 1;
- }
- int main()
- {
- signal(SIGINT, func);
- while(0 == value)
- sleep(1);
- return 0;
- }
为了显示linux对signal的处理流程,我们需要进行两个步骤。第一,输入gcc sig.c -o sig, 然后输入./sig即可;第二则重启一个console窗口,输入ps -aux | grep sig, 在获取sig的pid之后然后输入kill -INT 2082, 我们即可得到如下的输出。
- [root@localhost fork]#./sig
- I get a signal!
- [root@localhost fork]#
Linux系统本身为进程间通信提供了很多的方式,比如说管道、共享内存、socket通信等。管道的使用十分简单,在创建了匿名管道之后,我们只需要从一个管道发送数据,再从另外一个管道接受数据即可。
- #include <stdio.h>
- #include <unistd.h>
- #include <stdlib.h>
- #include <string.h>
- int pipe_default[2];
- int main()
- {
- pid_t pid;
- char buffer[32];
- memset(buffer, 0, 32);
- if(pipe(pipe_default) < 0)
- {
- printf("Failed to create pipe!\n");
- return 0;
- }
- if(0 == (pid = fork()))
- {
- close(pipe_default[1]);
- sleep(5);
- if(read(pipe_default[0], buffer, 32) > 0)
- {
- printf("Receive data from server, %s!\n", buffer);
- }
- close(pipe_default[0]);
- }
- else
- {
- close(pipe_default[0]);
- if(-1 != write(pipe_default[1], "hello", strlen("hello")))
- {
- printf("Send data to client, hello!\n");
- }
- close(pipe_default[1]);
- waitpid(pid, NULL, 0);
- }
- return 1;
- }
- [test@localhost pipe]$ ./pipe
- Send data to client, hello!
- Receive data from server, hello!
pipe(建立管道):
表头文件: #include
定义函数: int pipe(int filedes[2]);
函数说明: pipe()会建立管道,并将文件描述词由参数filedes数组返回。
返回值:
实例:
int main( void )
{
}
另外,说明一下管道读写的阻塞。
当管道中的数据被读取后,管道为空。一个随后的read()调用将默认的被阻塞,等待某些数据写入。
#include <unistd.h>
int pipe(int filedes[2]);
描述
pipe()函数创建一个管道和指向该管道的一对文件描述符,并且将文件描述符存储到文件描述符数组filedes[]中。其中filedes[0]为读端,filedes[1]为写端。
返回值
0 – 管道创建成功;
-1 – 管道创建失败,同时errno置位;
错误指示
EFAULT – 无效的输入参数filedes;
EMFILE – 达到当前进程允许的文件描述符最大值;
ENFILE – 达到系统允许的打开文件的最大数;
实例
下边的例子首先创建一个管道,然后通过fork()创建当先进程的子进程。接着每个进程关闭读写管道不需要的文件描述符。子进程在当前路径下执行“ls –a”命令,通过将管道写描述符fd[1]复制成标准输出,将命令执行输出写到管道;父进程通过fd[0]读取管道数据并显示。
多线程和多进程还是有很多区别的。其中之一就是,多进程是linux内核本身所支持的,而多线程则需要相应的动态库进行支持。对于进程而言,数据之间都是相互隔离的,而多线程则不同,不同的线程除了堆栈空间之外所有的数据都是共享的。说了这么多,我们还是自己编写一个多线程程序看看结果究竟是怎么样的。
- #include <stdio.h>
- #include <pthread.h>
- #include <unistd.h>
- #include <stdlib.h>
- void func_1(void* args)
- {
- while(1){
- sleep(1);
- printf("this is func_1!\n");
- }
- }
- void func_2(void* args)
- {
- while(1){
- sleep(2);
- printf("this is func_2!\n");
- }
- }
- int main()
- {
- pthread_t pid1, pid2;
- if(pthread_create(&pid1, NULL, func_1, NULL))
- {
- return -1;
- }
- if(pthread_create(&pid2, NULL, func_2, NULL))
- {
- return -1;
- }
- while(1){
- sleep(3);
- }
- return 0;
- }
和我们以前编写的程序有所不同,多线程代码需要这样编译,输入gcc thread.c -o thread -lpthread,编译之后你就可以看到thread可执行文件,输入./thread即可。
- [test@localhost Desktop]$ ./thread
- this is func_1!
- this is func_2!
- this is func_1!
- this is func_1!
- this is func_2!
- this is func_1!
- this is func_1!
- this is func_2!
- this is func_1!
- this is func_1!
和多进程一样,多线程也有自己的等待函数。这个等待函数就是pthread_join函数。那么这个函数有什么用呢?我们其实可以用它来等待线程运行结束。
- #include <stdio.h>
- #include <pthread.h>
- #include <unistd.h>
- #include <stdlib.h>
- void func(void* args)
- {
- sleep(2);
- printf("this is func!\n");
- }
- int main()
- {
- pthread_t pid;
- if(pthread_create(&pid, NULL, func, NULL))
- {
- return -1;
- }
- pthread_join(pid, NULL);
- printf("this is end of main!\n");
- return 0;
- }
编写wait.c文件结束之后,我们就可以开始编译了。首先你需要输入gcc wait.c -o wait -lpthread,编译之后你就可以看到wait可执行文件,输入./wait即可。
- [test@localhost thread]$ ./thread
- this is func!
- this is end of main!
对于编写多线程的朋友来说,线程互斥是少不了的。在linux下面,编写多线程常用的工具其实是pthread_mutex_t。本质上来说,它和Windows下面的mutex其实是一样的,差别几乎是没有。希望对线程互斥进行详细了解的朋友可以看这里。
- #include <stdio.h>
- #include <pthread.h>
- #include <unistd.h>
- #include <stdlib.h>
- static int value = 0;
- pthread_mutex_t mutex;
- void func(void* args)
- {
- while(1)
- {
- pthread_mutex_lock(&mutex);
- sleep(1);
- value ++;
- printf("value = %d!\n", value);
- pthread_mutex_unlock(&mutex);
- }
- }
- int main()
- {
- pthread_t pid1, pid2;
- pthread_mutex_init(&mutex, NULL);
- if(pthread_create(&pid1, NULL, func, NULL))
- {
- return -1;
- }
- if(pthread_create(&pid2, NULL, func, NULL))
- {
- return -1;
- }
- while(1)
- sleep(0);
- return 0;
- }
- [test@localhost thread]$ ./mutex
- value = 1!
- value = 2!
- value = 3!
- value = 4!
- value = 5!
- value = 6!
开始介绍网络编程的方法之前,我们可以回忆一下计算机网络的相关知识。目前为止,我们使用的最多网络协议还是tcp/ip网络。通常来说,我们习惯上称为tcp/ip协议栈。至于协议栈分成几层,有两种说法。一种是五层,一种是七层,我个人本身也比较倾向于五层的划分方法。大家可以通过下面的图看看协议栈是怎么划分的。
5、应用层4、传输层
3、网络层
2、数据链路层
1、物理层
网络的不同层次实现网络的不同功能。物理层主要实现报文的成帧处理;数据链路层完成对报文的优先级的管理,同时实现二层转发和流量控制;网络层实现路由和转发的功能,一方面它需要实现对报文的fragment处理,另外一方面它还需要对路由信息进行处理和保存;传输层实现报文的发送和接受,它利用计数、时序、定时器、重发等机制实现对报文的准确发送,当然这都是tcp的发送机制,而udp一般是不保证报文正确发送和接收的;应用层就是根据传输层的端口信息调用不同的程序来处理传输的内容,端口8080是http报文,端口21是ftp报文等等。上面的逻辑稍显复杂,朋友们可以这么理解, 物理层关心的是如何把电气信号变成一段报文;数据链路层关心的是mac地址、vlan、优先级等;网络层关心的是ip地址,下一跳ip;传输层关心的是端口资源;应用层关心的是报文组装、解析、渲染、存储、执行等等。
目前关于tcp/ip完整协议栈的代码很多,其中我认为写得比较好的还是linux内核/net/ipv4下面的代码。如果朋友们对ipv6的代码感兴趣,也可以看看/net/ipv6的代码。档案如果朋友们对整个协议栈的代码结构理解得不是很清楚,可以参考《linux网络分析与开发》这本书。
当然,作为应用层,我们的其实考虑的不用这么复杂。对于网络程序编写人员来讲,所有网络的资源只要和一个socket关联在一起就可以了。当然在socket可用之前,我们需要为它配置端口信息和ip地址。配置完了之后,我们就可以慢慢等待报文的收发了。所以一般来说,作为服务器端口的处理流程是这样的,
a) 创建socket
b) 绑定socket到特定的ip地址
c) 对socket进行侦听处理
d) 接受socket,表明有客户端和服务器连接
e) 和客户端循环收发报文
f) 关闭socket
作为服务器程序而言,它要对特定的端口进行绑定和侦听处理,这样稍显复杂。但是如果是编写客户端的程序,一切的一切就变得非常简单了,
a) 创建socket
b) 链接服务器端地址
c) 和服务器端的socket收发报文
上面只是对网络编程做了一个基本的介绍,但是好多的东西还是没有涉及到,比如说:(1) 什么时候该使用udp,什么时候该使用tcp?(2) 如何把多线程和网络编程联系在一起? (3) 如何把多进程和网络编程联系在一起? (4) 如何利用select函数、epoll_create机制、非阻塞函数提高socket的并发处理效率? (5) linux内核是怎么实现tcp/ip协议的? (6) 我们自己是否也可以实现协议的处理流程等等?
- linux c 编程模板总结(二)
- linux c 编程模板总结(一)
- linux c 编程实战:文件操作总结(二)
- linux c 编程实战:进程控制总结(二)
- 读《C专家编程》总结(二)
- linux C语言网络编程学习总结<二>
- Linux C 多线程编程学习(二)
- linux c 多线程编程练习(二)
- linux C 编程 一站式学习(二)
- linux环境下C编程(二)
- Linux C/C++ 编程 (二)
- [Linux C编程]嵌入式数据库(二)
- 【c语言】c语言常见编程题总结(二)
- Linux C 总结篇(socket编程)
- Linux下的C编程实战(二)----文件系统编程
- LINUX下的C编程入门(二)文件系统编程
- 【C/C++】《Linux C编程》总结
- linux学习笔记二(linux下c编程基础)
- Programing Exercise 4:Neural Networks Learning
- 虚拟机秘钥疑惑
- 数据结构与算法系列----Sunday算法详解
- C++ vector 类学习笔记 http://blog.csdn.net/whz_zb/article/details/6827999
- 匈牙利算法
- linux c 编程模板总结(二)
- java多线程系列01——多线程基础
- MATLAB对文件进行批量重命名
- 关于编程中遇到inf的情况
- HDU 5054Alice and Bob
- nyoj119 士兵杀敌(三) (线段树,两个value)
- Spring进阶之路(4)-容器中Bean作用域与集合类型配置
- Android应用程序性能优化
- Java服务器热部署的实现原理——java类的加载方式