Linux下c实战

2015/09/23 Linux C

Linux下C编程实战

传统的UNIX下的程序开发语言是C语言,C语言是一种平台适应性强、易于移植的语言。

Linux下的C语言编程环境

Linux是用C语言写成的,反过来,Linux又为C语言提供了很好的支持,C语言编译工具gcc、调试工具gdb属于最早开发出来的一批自由软件。因此,Linux与C语言形成了完美的结合,为用户提供了一个强大的编程环境。

Linux为软件开发者提供了强大的C语言开发环境和丰富的开发维护工具,熟悉并掌握这些工具是进行Linux平台软件开发的必要条件。

  • 编辑工具:Linux系统提供了许多文本编辑程序,比较常用的有vi和emacs等。此外,Ubuntu还自带了gedit等编辑器,它们都可以用来编辑C语言源程序。
  • 编译工具:Linux带有功能强大的符合ANSI C语言标准的编译系统gcc,利用gcc可以编译C/C++语言源程序。
  • 调试工具:利用Linux带的调试工具gdb,可以调试C语言程序。
  • 维护工具:make程序可以对程序源文件进行有效的管理。 如果读者在Windows系统中对集成开发环境(IDE)做过开发,则一定不会对IDE感到陌生。在Linux下也有许多IDE可以用来开发C语言程序,如CodeBlocks、CodeLite、Anjuta、Eclipse等。其中,CodeBlocks、CodeLite与Windows系统中的Visual Studio界面非常类似,比较容易上手。

Linux下的文件编程

文件系统是现代操作系统中重要的组成部分之一。文件系统是指按一定规律组织起来的有序的文件组织结构,是构成系统中所有数据的基础。系统中的所有文件都是驻留在文件系统中某一特定的位置的。Linux提供的文件系统是树形的层次结构系统。所有文件最终都归结到根目录“/”。Linux支持多种文件系统,当前最通用的文件系统是ext4系统。

每种文件系统类型存储数据的基本格式都是不一样的。但是,在Linux下访问任何文件系统时,系统都把数据整理成一个目录树下的文件,并包括文件的属主和组ID、保护位,以及其他特征。事实上,属主、保护等信息只有那些能存储Linux文件的文件系统类型才能提供。对于没有存储这些信息的文件系统类型,用来访问这些文件系统的内核驱动程序会“伪造”这些信息。例如,MS-DOS文件系统没有文件属主的概念,但所有文件都显示成属主是root。用这种方法,在一定层次上,所有文件系统都很相似,每个文件都有一定的属性。至于这些文件属性是否真的在文件系统底层被使用,就是另外一回事了。

文件描述符

C语言的标准I/O库中的库函数,如fopen、fclose、fread、fwrite等,提供的是高层服务;而Linux的文件I/O调用提供的是底层的服务,底层的服务不提供缓冲而直接进入操作系统。标准I/O库中的高层服务归根到底还是要调用Linux所提供的底层服务。

基本文件I/O操作

本节将详细讲述普通文件的基本输入和输出操作,包括建立文件(Create)、打开文件(Open)、文件的写操作(Write)、文件的读操作(Read)、关闭一个打开的文件(Close)、设置文件读/写指针位置(Lseek)等。

open函数

调用open函数可以打开或创建一个文件,open是进程存取一个文件中的数据必须首先完成的系统调用。open函数的格式如下:

进程管理

介绍Linux下基于进程的系统调用,包括创建进程、使进程等待、结束进程、改变进程执行映像、改变进程执行的优先级等。

进程的创建

创建一个新进程的唯一方法是由某个已存在的进程调用fork或vfork函数,被创建的新进程称为子进程,已存在的进程称为父进程。当然,某些特殊进程并不需要通过这种方法来创建,如swapper进程、initt进程等,它们是作为系统启动的一部分而被内核创建的。

  • fork函数 fork函数是用来创建子进程的函数,具体调用如下:
    #include <sys/types.h>
    #include <unistd.h>
    #include <stdio.h>
    pid_t fork();
    

    fork函数没有参数,它是一个单调用双返回的函数。具体地说,某个进程调用此函数后,若创建子进程成功,则这个函数在父进程中的返回值是创建的子进程的进程标识号,而在子进程中的返回值为零,否则(创建不成功)返回-1。每个进程可以有许多子进程,但没有哪个函数调用是用来将子进程的进程标识号返回给父进程的,而每个进程至多有一个父进程,利用getppid函数可以得到父进程的进程标识号。因此,fork函数将子进程的进程标识号返回给了父进程,父进程利用此进程标识号与子进程取得联系,而统一给子进程返回零。

子进程是父进程的一个复制。具体说,子进程从父进程那里得到了数据段和堆栈段的复制,这些需要分配新的内存,而不是与父进程共享内存。对于只读的代码段,一般情况下,是使用共享内存的方式访问的。fork函数返回后,子进程和父进程一样,都是从调用fork函数的语句的下一条开始执行。

因为创建子进程之后常常伴随着exec的调用,现行的许多实现机制实际上并不采用上述方法,将父进程的所有数据、堆、栈全都复制下来,而是使用一种“写时复制(COW)”的技术。父进程和子进程共享数据和堆栈区域,但是内核将它们都设置为只读权限。当任一进程要修改这些区域时,再生成该块内存的复制,通常是虚存的一页。

线程管理

典型的线程包含一个运行时间系统,它可以按透明的方式来管理线程。通常线程包括对线程创建和删除,以及对互斥和条件变量的调用。POSIX标准线程库具有这些调用。这些线程包还提供线程的动态创建和删除,因此,直到运行时间之前,线程的个数不必知道。

线程具有一个ID、一个堆栈、一个执行优先权,以及执行的开始地址,POSIX线程通过pthread_t类型的ID来引用。pthread_t其实就是无符号长整数,在文件/usr/include/bit/pthreadtypes.h中有如下的定义:

typedef unsigned long int pthread_t;

线程的内部数据结构也包含调度和使用信息。进程的线程共享进程的完整地址空间,它们能够修改全局变量,访问打开的文件描述符,或用别的方式相互作用。

创建线程和结束线程

如果线程可在进程的执行期间的任意时刻被创建,并且线程的数量事先没有必要指定,这样的线程称为动态线程。在POSIX中,线程是用pthread_create动态地创建的。pthread_create能创建线程,并将它放入就绪队列。 该函数的定义如下:

#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void*(*start_routine)(void *),void *arg);

pthread_create创建一个线程,这个线程与创建它的线程同步执行。新创建的线程将执行函数start_routine,这个函数的参数由指针arg指定。这个线程可以通过pthread_exit来终止,或者当函数start_routine返回时自然终止。参数attr指定新线程的属性。将在讲述pthread_attr_init时具体介绍这个参数。参数attr可以为NULL,此时将使用默认的属性,即使用最小的堆栈空间,也使用通常的调度策略等。

与创建进程的系统调用fork不同的是,用pthread_create创建的线程并不是与父进程在同一点开始运行,而是从pthread_create指定的函数开始运行。这一点很明显,因为如果线程也像子进程一样与父进程从同一点开始运行,那么将有多个线程使用同一资源。正如前面讲过的,每一个进程都有自己的地址空间和资源,而多个线程要使用一个地址空间和资源。

如果执行成功,那么将返回0,并将新创建线程的标识符存放在由指针thread指向的地址。如果执行失败,那么将返回一个非零值。

在创建线程后,可以调用pthread_self函数得到线程的ID,该函数的定义如下:

#include <pthread.h>
pthread_t pthread_self(void);

要结束一个线程,需要调用函数pthread_exit。它的原型如下:

#include <pthread.h>
void pthread_exit(void* retval);

此函数用于结束一个线程。它将调用用户为线程注册的清除处理函数,然后结束当前线程,返回值为retval。这个函数在线程的中途退出时有用。

函数pthread_exit在成功调用时,返回0,失败时返回-1。

挂起线程

可以使用如下函数将一个线程挂起:

#include <pthread.h>
int pthread_join(pthread_t th,void **thread_return);

此函数用于挂起当前线程直至指定线程终止。参数th是一个线程标识符,用于指定要等待其终止的线程。参数thread_return用于存放其他线程的返回值。对于每一个可连接的线程都必须调用该函数一次。任何线程都不能对相同的线程调用此函数。

如果执行成功,那么参数th的返回值将保存在由参数thread_return指向的地址中,函数返回0,否则,返回一个非0值。

线程同步

当在同一内存空间运行多个线程时,要注意一个基本的问题就是不要让线程之间互相破坏。例如,两个线程要更新两个变量的值。一个线程要把两个变量的值都设成0;另一个线程要把两个变量的值都设成1。如果两个线程同时要做这件事情,结果可能是,一个变量的值是0;另一个变量的值是1。这是因为正好在第1个线程把第1个变量设为0后,运行环境切换,第2个线程将把两个变量都设成1,然后运行环境再切换,第1个线程恢复运行,把第2个变量设成0。结果就是,一个变量的值是0;另一个变量的值是l。

按照POSIX标准,POSIX提供了两种类型的同步机制,它们是互斥锁(Mutex)和条件变量(condition Variable)。互斥锁是一个简单的锁定命令,它可以用来锁定对共享资源的访问。对于线程来说,整个地址空间都是共享的资源,所以线程的任何资源都是共享的资源。互斥锁的特点如下所述。

  • 原子性:把一个互斥锁定为一个原子操作,这意味着操作系统(或pthread函数库)保证了如果一个线程锁定了一个互斥锁,没有其他线程在同一时间可以成功锁定这个互斥锁。
  • 唯一性:如果一个线程锁定了一个互斥锁,在它解除锁定之前,没有其他线程可以锁定这个互斥量。
  • 非繁忙等待:如果一个线程已经锁定了一个互斥锁,第2个线程又试图去锁定这个互斥锁,则第2个线程将被挂起(不占用任何CPIJ资源),直到第1个线程解除对这个互斥锁的锁定为止,第2个线程则被唤醒并继续执行,同时锁定这个互斥锁。

下面几个函数是处理互斥锁时常用的几个函数。

取消线程和取消处理程序

用户可以调用函数来取消一个线程。相应的函数说明如下。但要注意这种操作不是用于取消当前线程,而是当前线程调用函数来取消另一个线程。

Linux的线程库中用于取消线程的几个函数如下:

#include <pthread.h>

Linux允许一个线程终止另外一个线程的执行,即一个线程可以向另外一个线程发出一个终止请求。根据不同的设置,接收到这个终止请求的线程可以忽略这个请求,也可以立即终止或者延长一段时间后终止。

当一个线程最终响应终止请求时,它所执行的操作与调用pthread_exit函数时一样,即调用每一个cleanup处理程序,然后调用与线程相关的数据处理程序,最后线程终止执行。

  • 函数pthread_cancel发送一个终止请求到由参数thread指定的线程。调用成功时,返回值为0,调用失败时,返回错误代码。
  • 函数pthread_setcancelstate用于设置调用函数的线程自身的状态。参数state是要设置的新的状态。参数oldstate是指向存放要设置的状态的缓冲区的指针。调用成功时,返回值为0,调用失败时,返回错误代码。
  • 函数pthread_setcanceltype用于设置对取消的响应方式。响应方式有两种。PTHREAD_CANCEL_ASYNCHRONOUS:立刻取消;PTHREAD_CANCEL_DEFERRED:延迟取消至取消点。参数type是要设置的新的方式。参数oldtype是指向存放要设置的方式的缓冲区的指针。调用成功时,返回值为0,调用失败时,返回错误代码。
  • 函数ptbread_testcancel用于设置取消点。如果延迟取消请求挂起,那么此函数将取消当前进程。线程终止后,要做一些清理工作,Linux线程库提供了相应的处理函数。

线程特定数据的处理函数

因为一个进程中的线程要共享同一个地址空间,那么在这个共享空间中,所有的变量都将为所有的线程所公用。如果要使用某些一个线程特定的变量,该如何处理呢?下面先介绍一下线程特定数据的处理函数,然后通过一个例子来说明这些函数的使用方法。

  • pthread_key_create函数 函数原型如下:
    #include <pthread.h>
    int pthread_key_create(pthread_key_t *key, void (*destructor)(void*));
    

    函数pthread_key_create创建一个对进程中的所有线程都可见的关键字。这个关键字可以通过函数pthread_setspecific和pthread_getspecific来读取和设置。

当创建一个关键字时,进程中的所有线程的这个关键字的值都为NULL,当创建一个线程时,这个线程的所有的关键字的值都为NULL。 如果pthread_key_create执行成功,则返回0,并在参数key中保存新创建的关键字的ID。其他的值意味着错误。

  • pthread_key_delete函数 函数原型如下: ```shell
函数pthread_key_delete清除由参数key指定关键字。 如果pthread_key_delete执行成功,则返回0。其他的值意味着错误。

- pthread_setspecific函数

函数原型如下:
```shell

函数pthread_setspecific指定由参数pointer指定的指针指向由参数key指定关键字。每一个线程都有一个互相独立的指针,这个指针指向一个特定的关键字。

如果pthread_setspecific执行成功,将返回0。其他值意味着错误。

线程属性

每个POSIX线程有一个相连的属性对象来表示特性。线程的属性对象能与多个线程相连,POSIX具有创建、配置和删除属性对象的函数。线程可以分组,并将相同属性与组中所有成员相连。当属性对象的一个特性改变时,组中所有实体具有新的特性。线程属性对象的类型是pthread_attr_t,pthread_attr_t在文件/usr/include/bits/pthreadtypes.h中被定义,给出它的定义如下:


网络编程

套接字网络编程

面向连接的套接字工作过程是,服务器首先启动,通过调用socket函数建立一个套接字,然后调用bind将该套接字和本地网络地址联系在一起,再调用listen使套接字做好侦听的准备,并规定它的请求队列的长度,之后就调用accept来接收连接。客户在建立套接字后就可调用connect和服务器建立连接。连接一旦建立,客户机和服务器之间就可以通过调用read和write来发送和接收效据。最后,待数据传送结束后,双方调用close关闭套接字。

##

Search

    微信好友

    博士的沙漏

    Table of Contents