存档在 ‘Linux下C编程’ 分类

Linux下的socket编程-服务器

2011年3月15日

我们都知道,同一台计算机上的进程可以通过IPC(进程间通信)机制进行通信;而不同机算计上运行的进程则通过网络IPC,即套接字(socket)进行通信。Linux下的socket API是基于BSD套接口而是实现的,通过这些统一的API就可以轻松实现进程间的网络通信。此外,socket API即可用于面向连接(TCP)的数据传输,又可用于无连接(UDP)的数据传输。一般使用Client/Server交互模型进行通信。

本文以及下文将实现一个面向连接的C/S通信模型。本文首先介绍服务器端的实现。

1.创建套接字

#include < sys/socket.h >
int socket(int domain, int type, int protocol);

通过socket函数可以创建一个套接字描述符,这个描述符类似文件描述符。通过这个套接字描述符就可以对服务器进行各种相关操作。

该函数包含三个参数,domain参数用于指定所创建套接字的协议类型。通常选用AF_INET,表示使用IPv4的TCP/IP协议;如果只在本机内进行进程间通信,则可以使用AF_UNIX。参数type用来指定套接字的类型,SOCK_STREAM用于创建一个TCP流的套接字,SOCK_DGRAM用于创建UDP数据报套接字。参数protocol通常取0。对于本文所描述的服务器,创建套接字的示例代码如下:

	if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {

		printf("socket error!\n");
		exit(1);
	}

2.绑定套接字

对于服务器而言,它的IP地址和端口号一般是固定的。服务器的IP即为本地IP,而服务器的端口号则需要显示的指定。通过bind函数可将服务器套接字和一个指定的端口号进行绑定。

在具体介绍绑定函数之前,先说明一下socket中的套接字地址结构。由于套接字是通过IP地址和端口号来唯一确定的,因此socket提供了一种通用的套接字地址结构:

   struct sockaddr {
               sa_family_t sa_family;
               char        sa_data[14];
           }

sa_family指定了套接字对应的协议类型,如果使用TCP/IP协议则改制为AF_INET;sa_data则用来存储具体的套接字地址。不过在实际应用中,每个具体的协议族都有自己的协议地址格式。比如TCP/IP协议组对应的套接字地址结构体为:

struct sockaddr_in {
	short int sin_family; /* Address family */
	unsigned short int sin_port; /* Port number */
	struct in_addr sin_addr; /* Internet address */
	unsigned char sin_zero[8]; /* Same size as struct sockaddr */
};

struct in_addr {
	unsigned long s_addr;
};

该地址结构和sockaddr结构均为16字节,因此通常在编写基于TCP/IP协议的网络程序时,使用sockaddr_in来设置具体地址,然后再通过强制类型转换为sockaddr类型。

绑定函数的函数原型如下:

       #include < sys/socket.h >
       int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

参数sockfd即服务器的套接字描述符;addr参数指定了将socket绑定到的本地地址;addrlen则为所使用的地址结构的长度。示例代码如下:

	memset(&my_addr, 0, sizeof(struct sockaddr_in));
	my_addr.sin_family = AF_INET;
	my_addr.sin_port = htons(SERV_PORT);
	my_addr.sin_addr.s_addr = htonl(INADDR_ANY);

	if(bind(sockfd, (struct sockaddr *)&my_addr,
				sizeof(struct sockaddr_in)) == -1) {

		printf("bind error!\n");
		exit(1);
	}

注意在上述代码中,将IP地址设置为INADDR_ANY,这样就既适合单网卡的计算机又适合多网卡的计算机。

3.在套接字上监听

对于C/S模型来说,通常是客户端主动的对服务器端发送连接请求,服务器接收到请求后再具体进行处理。服务器只有调用了listen函数才能宣告自己可以接受客户端的连接请求,也就是说,服务器此时处于被动监听状态。listen函数的原型如下:

       #include < sys/socket.h >
       int listen(int sockfd, int backlog);

sockfd为服务器端的套接字描述符,backlog指定了该服务器所能连接客户端的最大数目。超过这个连接书目后,服务器将拒绝接受客户端的连接请求。示例代码如下:

        #define BACKLOG 10
	if(listen(sockfd, BACKLOG) == -1) {

		printf("listen error!\n");
		exit(1);
	}

4.接受连接

listen函数只是将服务器套接字设置为等待客户端连接请求的状态,真正接受客户端连接请求的是accept函数:

      #include < sys/socket.h >
       int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

accept函数中所使用的参数都在上面的API中有所描述。accept函数执行成功后将返回一个代表客户端的套接字描述符,服务器进程通过该套接字描述符就可以与客户端进行数据交换。

5.数据传输

由于socket适用多个数据传输协议,则不同的协议就对应不同的数据传输函数。与TCP协议对应的发送数据和接受数据的函数如下:

       #include < sys/socket.h >
       #include < sys/types.h >
       ssize_t send(int sockfd, const void *buf, size_t len, int flags);
       ssize_t recv(int sockfd, void *buf, size_t len, int flags);

从这两个函数的原型可以看书,socket中的数据传输函数与普通文件的读写函数类似,只不过第一个参数需要传递套接字描述符;buf指定数据缓冲区;len为所传输数据的长度;flag一般取0。示例代码如下:

	while (1) {
		sin_size = sizeof(struct sockaddr_in);
		if ((client_fd = accept(sockfd, (struct sockaddr *)&remote_addr, &sin_size)) == -1) {
			printf("accept error!\n");
			continue;
		}
		/*
                 *进行相应的数据处理;
                 */
	}

如示例代码所示,通过while循环使得服务器对客户端进行持续监听。如果客户端有连接请求则新建一个代表客户端的套接字描述符,进而进行对客户端数据的接受和发送。

上述的几个函数属于网络编程中最基本的也是最关键的几个API,依次通过上述的方法就可以完成服务器端的程序的编写,具体的过程还可以参考下图:

正如上图所示,在处理完客户端的数据传输请求后,必须通过close函数关闭客户端的连接。

参考:

1.Linux C编程实战;童永清 著;人民邮电出版社;

exec族函数

2011年3月14日

通常我们使用了fork函数创建了一个子进程后,子进程会调用exec族函数执行另外一个程序。由于fork函数新建的子进程和父进程几乎一模一样(不过,后来引入了写时复制技术避免了这一点),因此需要使用exec函数使得子进程执行一个新的可执行程序。

我们之所以称exec为族函数是因为它有6种不同的形式,主要区别体现在参数上:

#include <unistd.h>;
extern char **environ;
int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execle(const char *path, const char *arg,
             ..., char * const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execve(const char *filename, char *const argv[],
              char *const envp[]);

总体来说,exec族函数的参数分为三类:文件路径,命令行参数,环境变量。不论那个exec函数,最终都是将上述三类参数传递给可执行程序中的main函数。这里有一个新的概念,即环境变量,它定义了用户的工作环境,包括用户的主目录,终端的类型,当前用户,当前目录等信息。事实上,main函数的完整形式应该是:

int main(int argc, char *argv[], char **envp);

通常我们并没有显示指定环境变量,这是因为main函数每次都默认使用系统自定义的全局变量environ。另外,通过在终端输入env命令,也可查看当前的所有环境变量信息。可以看到,main函数中的argv[]和envp与exec函数的参数有对应关系。

初次接触exec族函数,易被这6种不同的调用形式搞得混乱。下面我们将依次从上述三个参数的角度来区分这些函数的细微区别。

1.可执行文件的路径

以p结尾的exec函数可以直接传递可执行程序的文件名,此时这类函数会自动在PATH环境变量所指定的目录下查找这个可执行文件;非p结尾的exec函数则必须显示指定可执行程序的完整路径;

2.命令行参数

以execl开始的exec函数必须以列表的形式传递所有的命令行参数,并且最终以NULL作为命令行参数的结束;而以execv开始的exec函数则以字符串指针数组的形式传递所有命令行参数;

3.环境变量

以e结尾的exec函数必须显示指定环境变量;而非p结尾的exec函数则使用默认的环境变量,也就是main函数中的环境变量;

虽然exec函数有6种不同的调用形式,但是每个函数实现的功能都是一样的,即完成新的可执行程序的装入。下面的这个程序很好的演示了不同exec函数的使用方法:

#include < unistd.h >
#include < stdio.h >

int main()
{
	char *envp[]={
		"PATH = /tmp",
	        "USER = edsionte",
		NULL
	};

	char *argv_execv[] = {"echo", "excuted by execv", NULL};
	char *argv_execvp[] = {"echo", "executed by execvp", NULL};
	char *argv_execve[] = {"env", NULL};

	if (fork() == 0)
		if(execl("/bin/echo", "echo", "executed by execl", NULL) < 0)
			perror("Err on execl");
	if(fork() == 0)
		if(execlp("echo", "echo", "executed by execlp", NULL) < 0)
			perror("Err on execlp");
	if(fork() == 0)
		if(execle("/usr/bin/env", "env", NULL, envp) < 0)
			perror("Err on execle");
	if(fork() == 0)
		if(execv("/bin/echo", argv_execv) < 0)
			perror("Err on execv");
	if(fork() == 0)
		if(execvp("echo", argv_execvp) < 0)
			perror("Err on execvp");
	if(fork() == 0)
		if(execve("/usr/bin/env", argv_execve, envp) < 0)
			perror("Err on execve");

	printf("goodbye!\n");
	return 0;
}
//结果:
edsionte@edsionte-desktop:~/edsionte$ ./sys_process
goodbye!
edsionte@edsionte-desktop:~/edsionte$ executed by execl
excuted by execv
executed by execvp
executed by execlp
PATH = /tmp
USER = edsionte
PATH = /tmp
USER = edsionte

这个程序分别使用不同的exec函数调用了echo或env命令,并根据所传递的不同参数显示不同结果。该程序可能每次运行后的显示顺序都有所不同,这是进程的并发性所导致的。

值得注意的是,我们通常所说的命令行参数是以argv数组的第一个元素开始的,而第0号元素是可执行程序的名称。以第一个if语句为例,execl函数为/bin目录下的echo程序传递了两个参数:”echo”和”executed by execl”。由于我们通过execl函数已经装入了/bin/echo程序,因此在调用execl函数时,第一个参数”echo”可以替换成任意字符串,但是这并不代表就不需要传递该参数。

我们也可以让exec函数去执行我们自己编写的可执行程序。下面这个例子很好的演示了父进程和子进程的关系:

/*
 *Author: edsionte
 *Email:  edsionte@gmail.com
 *Time:   2011/03/14
*/

#include < stdio.h >
#include < unistd.h >
#include < stdlib.h >

int main()
{
	int pid;
	int val = 0;

	pid = fork();
	if (pid == 0) {
		//child proces;
		printf("[Child %d] I will be a new process!\n", getpid());
		if (execl("./sleeping","first arg", "20", NULL) == -1) {
			printf("execl error!\n");
			exit(1);
		}
		printf("[Child %d] I never go here!\n", getpid());
	} else if (pid > 0) {
		printf("[Parent %d] I am running!\n", getpid());
		/*
		wait(&val);
		printf("[Parent %d] All my child were leaving..\n", getpid());
		*/
	} else {
		printf("fork error!\n");
		exit(1);
	}

	printf("[process %d] I am leaving..\n", getpid());
	return 0;
}
//sleeping.c程序
#include < stdio.h >
#include < unistd.h >

int main(int argc, char** argv)
{
	int sec;

	sec = atoi(argv[1]);
	while (sec != 0) {
		sleep(1);
		printf("[New Child %d] I am running and my parent is %d\n", getpid(), getppid());
		sec--;
	}

	return 0;
}

该程序使用了与进程有关的四个最基本的系统调用函数:fork(),exec(),wait()和exit()。对于该程序的具体分析如下:

1.父进程通过fork函数创建子进程,然后父进程打印自己的pid;

2.子进程首先打印自己的pid,再通过execve函数装入可执行程序sleeping,并通过execve函数向sleeping传递了一个参数“20”;

3.在sleeping程序中,将主函数中传递的参数作为计数器,并依次循环睡眠20s。并且循环打印当前的sleeping进程的pid以及其父进程pid;

4.由于父进程先于子进程退出,则sleeping进程成为孤儿进程,因此在sleeping打印的父进程pid为1;如果去掉程序中的注释部分,则父进程等待子进程退出后再退出,那么sleeping所打印的父进程pid即为父进程的pid;

5.如果子进程执行exec函数成功,则主程序中最后一条语句就不会被子进程执行。说明子进程在执行了exec函数后就与父进程完全脱离。另外,在子进程执行过程中也可以通过ps命令查看当前的进程存在情况;

参考:

1.http://www.ibm.com/developerworks/cn/linux/kernel/syscall/part3/

2.Linux C编程实战;童永清 著;人民邮电出版社;

目录文件的访问权限

2011年1月17日

目录的访问权限

在linux当中,文件分为多种类型。比如普通文件,目录文件和符号链接文件等。我们通常所说的“文件”则默认为普通文件,也就是指文本文件和二进制文件(关于文件的分类描述,可参看这里)。

每一类文件还有相应的访问权限。对于一个文件而言,所谓的访问权限是指文件访问者是否对该文件具有读、写或执行的权利。另外,linux是一个多用户的操作系统,它将用户分为三种:用户(u,即文件所有者),用户所在组(g),其他用户(o)。因此,对于不同身份的用户,也就应当对一个文件有不同的访问权限。所以,每个文件有9个访问权限。

对于目录文件的权限,他和普通文件稍有不同;

读:可以读取该目录,从中获得该目录中所有文件名,但不能读取到每个文件的状态;
执行:可以对该目录进行搜索,也就是通过该目录可以搜索其内的特定文件名;
写:可以在该目录下新建或删除文件,但是首先必须对该目录拥有执行权限。如果该目录没有执行权限就不能对该目录下的文件进行搜索,新建或删除文件也就无从谈起;

测试

对于目录的访问权限,从文字描述的角度并不能理解的很清楚。因此,通过不断的测试才能理解这些访问权限对目录的作用效果。我们假设在edsionte主目录下有一个dirtest目录,其基本信息如下:

edsionte@edsionte-desktop:~$ ls -ld dirtest/
drwxr-xr-x 4 edsionte edsionte 4096 2011-01-17 11:06 dirtest/
edsionte@edsionte-desktop:~$ ls -l dirtest/
总用量 16
drwxr-xr-x 3 edsionte edsionte 4096 2011-01-15 16:10 cdev
-rw-r--r-- 1 edsionte edsionte  975 2010-10-17 22:06 pms.c
drwxr-xr-x 2 edsionte edsionte 4096 2011-01-13 20:49 shell
-rw-r--r-- 1 edsionte edsionte   19 2011-01-13 21:08 test.c

接下来的测试都是以edsionte用户身份对这个目录进行各种命令测试的,具体如下:

1.通过ls dirtest命令,可参看该目录下的文件名。因为这个目录具有读权限;

edsionte@edsionte-desktop:~$ ls dirtest/
cdev  pms.c  shell  test.c

2.先使得dirtest没有执行,再执行ls命令。可以看到,虽然可以读到每个文件名,但是每个文件的属性确无法获得;

edsionte@edsionte-desktop:~$ chmod a-x dirtest/
edsionte@edsionte-desktop:~$ ls dirtest/ -l
ls: 无法访问dirtest/shell: 权限不够
ls: 无法访问dirtest/test.c: 权限不够
ls: 无法访问dirtest/pms.c: 权限不够
ls: 无法访问dirtest/cdev: 权限不够
总用量 0
d????????? ? ? ? ?                ? cdev
-????????? ? ? ? ?                ? pms.c
d????????? ? ? ? ?                ? shell
-????????? ? ? ? ?                ? test.c

3.由于没有执行权限,无法搜索判断dirtest目录下是否已存在newfile.c文件,因此无法完成touch命令,即便该目录具有写权限;

edsionte@edsionte-desktop:~$ ls -ld dirtest/
drw-r--r-- 4 edsionte edsionte 4096 2011-01-17 11:06 dirtest/
edsionte@edsionte-desktop:~$ touch dirtest/newfile.c
touch: 无法创建"dirtest/newfile.c": 权限不够

对于目录文件,也是如此:

edsionte@edsionte-desktop:~$ chmod a-x dirtest/
edsionte@edsionte-desktop:~$ mkdir ./dirtest/newdir
mkdir: 无法创建目录"./dirtest/newdir": 权限不够

4.对该目录加上执行权限后,2和3中所遇到的问题都可以解决:

edsionte@edsionte-desktop:~$ chmod a+x dirtest/
edsionte@edsionte-desktop:~$ touch dirtest/newfile.c
edsionte@edsionte-desktop:~$ ls -l dirtest/
总用量 16
drwxr-xr-x 3 edsionte edsionte 4096 2011-01-15 16:10 cdev
-rw-r--r-- 1 edsionte edsionte    0 2011-01-17 11:36 newfile.c
-rw-r--r-- 1 edsionte edsionte  975 2010-10-17 22:06 pms.c
drwxr-xr-x 2 edsionte edsionte 4096 2011-01-13 20:49 shell
-rw-r--r-- 1 edsionte edsionte   19 2011-01-13 21:08 test.c

5.继续使得dirtest没有执行权限,由于不能读取dirtest下的目录cdev,也就更不能读取cdev目录下的文件,即便cdev具有可读可执行权限;

edsionte@edsionte-desktop:~$ ls -ld dirtest/cdev/
drwxr-xr-x 3 edsionte edsionte 4096 2011-01-15 16:10 dirtest/cdev/
edsionte@edsionte-desktop:~$ chmod a-x dirtest/
edsionte@edsionte-desktop:~$ ls -ld dirtest/cdev/
ls: 无法访问dirtest/cdev/: 权限不够
edsionte@edsionte-desktop:~$ ls -l dirtest/cdev/
ls: 无法访问dirtest/cdev/: 权限不够

6.如果dirtest有执行权限却没有写权限,则不能创建新文件。这一点很好理解;

edsionte@edsionte-desktop:~$ ls -ld dirtest/
dr-xr-xr-x 4 edsionte edsionte 4096 2011-01-17 11:36 dirtest/
edsionte@edsionte-desktop:~$ mkdir ./dirtest/newdir
mkdir: 无法创建目录"./dirtest/newdir": 权限不够

7.如果dirtest有写权限却没有执行权限,则不能在该目录下删除文件;

edsionte@edsionte-desktop:~$ chmod a-x dirtest/
edsionte@edsionte-desktop:~$ ls -ld dirtest/
drw-rw-rw- 4 edsionte edsionte 4096 2011-01-17 11:36 dirtest/
edsionte@edsionte-desktop:~$ rm ./dirtest/newfile.c
rm: 无法删除"./dirtest/newfile.c": 权限不够

8.如果dirtest目录有可写权限没有可执行权限,虽然可以打开该目录下的文件test.c,但是对其修改后不能保存;

edsionte@edsionte-desktop:~$ ls dirtest/
cdev  newfile.c  pms.c  shell  test.c
edsionte@edsionte-desktop:~$ chmod a-x dirtest/
edsionte@edsionte-desktop:~$ vim ./dirtest/test.c

9.如果要修改dirtest目录下的文件test.c,对于dirtest目录而言,必须具备执行权限;而该目录的写权限则与test.c的修改无关;

通过上面的测试,我们可以得出下面的结论:

1.对一个目录进行读操作,即指读该目录下的文件名;

2.对一个目录的“写”包括在该目录下删除、新建文件;更重要的是,只有该目录有可执行权限时,写操作才可以进行,否则,不能获取该目录下除文件名之外的任何文件状态信息;

3.如果在一次文件操作中,只要涉及到对目录的搜索,则该目录必须具备执行权限,否则写权限就无法进行;

一个目录的执行权限可以用下面的比喻来解释。一个目录就好像是一道门,该目录中的文件就是你想要的珍宝。并且,该门之后可能还会有其他门的存在。目录的可执行权限相当于打开这把门的钥匙,只有持有这把钥匙,你才能打开这扇目录之门。打开门后,你可能会拿走一些宝石(删除文件),或者放入一些宝石(新建文件)等。否则,你只能隔门相望门后的宝石了(只能读取文件名)。

内核同步方法-锁机制

2010年10月21日

本文将为你介绍内核同步算法中的自旋锁和信号量。在这之前,先了解一些概念。

执行线程:thread of execution,指任何正在执行的代码实例,可能是一个正在内核线程,一个中断处理程序等。有时候会将执行线程简称为线程。

临界区:critical region,即访问和操作共享数据的代码段。

多个执行线程并发访问同一资源通常是不安全的,通常使用自旋锁和信号量来实现对临界区互斥的访问。

自旋锁

自旋锁(spin lock)是一个对临界资源进行互斥访问的典型手段。自旋锁至多只能被一个执行线程持有。当一个执行线程想要获得一个已被使用的自旋锁时,该线程就会一直进行忙等待直至该锁被释放,就如同“自旋”所表达的意思那样:在原地打转。

我们也可以这么理解自旋锁:它如同一把门锁,而临界区就如同门后面的房间。当一个线程A进入房间后,它会关闭房门,使得其他线程不得进入。此时如果其他某个进程B需要进入房间,那么只能在门外“打转”。当A进程打开们后,进程B才能进入房间。

自旋锁的使用

1.定义初始化自旋锁

使用下面的语句就可以先定一个自旋锁变量,再对其进行初始化:

spinlock_t lock;
spin_lock_init(&lock);

也可以这样初始化一个自旋锁:

spinlock_t lock=SPIN_LOCK_UNLOCKED;

2.获得自旋锁

void spin_lock(spinlock_t*);
int spin_trylock(spinlock_t*);

使用spin_lock(&lock)这样的语句来获得自旋锁。如果一个线程可以获得自旋锁,它将马上返回;否则,它将自旋至自旋锁的保持者释放锁。

另外可以使用spin_trylock(&lock)来试图获得自旋锁。如果一个线程可以立即获得自旋锁,则返回真;否则,返回假,此时它并不自旋。

3.释放自旋锁

void spin_unlock(spinlock_t*);

使用spin_unlock(&lock)来释放一个已持有的自旋锁。注意这个函数必须和spin_lock或spin_trylock函数配套使用。

关于自旋锁的说明

1.保护数据。我们使用锁的目的是保护临界区的数据而不是临界区的代码。
2.在使用自旋锁时候,必须关闭本地中断,否则可能出现双重请求的死锁。比如,中断处理程序打断正持有自旋锁的内核代码,如果这个中断处理程序又需要这个自旋锁,那么这个中断处理程序就会自旋等待自旋锁被释放;但是这个锁的持有者此刻被中断打断,因此不可能在中断完毕之前释放锁。此时就出现了死锁。
3.自旋锁是忙等锁。正如同前面所说的那样,当其他线程持有自旋锁时,如果另有线程想获得该锁,那么就只能循环的等待。这样忙的功能对CPU来说是极大的浪费。因此只有当由自旋锁保护的临界区执行时间很短时,使用自旋锁才比较合理。
4.自旋锁不能递归使用。这一点也很好理解,如果当前持有锁的线程又需要再持有该锁,那么它必须自旋,等待锁被释放;但是这个锁本身又被它自己持有,因此这个线程永远无法继续向前推进。

信号量

信号量(semaphore)是保护临界区的一种常用方法。它的功能与自旋锁相同,只能在得到信号量的进程才能执行临界区的代码。但是和自旋锁不同的是,一个进程不能获得信号量时,它会进入睡眠状态而不是自旋。

利用上述自旋锁的门和锁的例子,我们可以这样解释信号量。我们将信号量比作钥匙。进程A想要进入房间,就必要持有一把钥匙。当钥匙被使用完之后,如果又有进程B想进房间,那么这个进程在等待其他进程从房间出来给它钥匙的同时会打盹。当房间里的某个进程出来时会摇醒这个睡觉的进程B。

信号量的使用

0.数据结构

  16struct semaphore {
  17        spinlock_t              lock;
  18        unsigned int            count;
  19        struct list_head        wait_list;
  20};

count:初始化信号量时所设置的信号量值。
wait_list:等待队列,该队列中即为等待信号量的进程。
lock:自旋锁变量

1.定义初始化信号量

使用下面的代码可以定义并初始化信号量sem:

struct semaphore sem;
sem_init(&sem,val);

其中val即为信号量的初始值。

如果我们想使用互斥信号量,则使用下面的函数:

init_MUTEX(&sem);

这个函数会将sem的值初始为1,即等同于sem_init(&sem,1);

如果想将互斥信号量的初值设为0,则可以直接使用下面的函数:

init_MUTEX_LOCKED(&sem);

除上面的方法,可以使用下面的两个宏定义并初始化信号量:

DECLARE_MUTEX(name);

DECLARE_MUTEX_LOCKED(name);

其中name为变量名。

2.获得信号量

down(&sem);

进程使用该函数时,如果信号量值此时为0,则该进车会进入睡眠状态,因此该函数不能用于中断上下文中。

down_interruptibale(&sem);

该函数与down函数功能类似,只不过使用down而睡眠的进程不能被信号所打断,而使用down_interruptibale的函数则可以被信号打断。

如果想要在中断上下文使用信号量,则可以使用下面的函数:

dwon_try(&sem);

使用该函数时,如果进程可以获得信号量,则返回0;否则返回非0值,不会导致睡眠。

3.释放信号量

up(&sem);

该函数会释放信号量,如果此时等待队列中有进程,则唤醒一个。

信号量的同步

信号量除了使进程互斥的访问临界区外,还可以用于进程间的同步。比如,当B进程执行完代码区C后,A进程才会执行代码段D,两者之间有一定的执行顺序。

字符设备驱动分析(2)

2010年9月24日

前文中,我们按照一般内核模块的结构分析了globalmem_init函数和globalmem_exit函数。通过上述两个函数可以完成字符驱动的加载和卸载。那么本文将进一步分析字符设备驱动的实现。

linux2.6内核中使用cdev结构体来表述一个字符设备驱动,但是一般我们并不直接使用cdev结构体,而是将与该设备相关的信息与cdev街头体结合爱一起,定义一个新的结构体,比如

struct globalmem_dev
{    struct cdev cdev;
      unsigned char mem[GLOBALMEM_SIZE];
};
struct cdev
{
	struct kobject kobj;//内嵌kobject对象
	struct module *owner;//指向实现驱动程序的模块的指针,通常为THIS_MODULE
	const struct file_operations *ops;//指向此设备驱动程序文件操作结构体的指针
	struct list_head list;//指向字符设备文件对应的索引节点链表的头
	dev_t dev;//设备号
	unsigned int count;//给该设备驱动程序分配的设备号范围的大小
};

就像前文所说的,设备号都是分配一个范围(count的大小),因此可能有很多个设备文件主设备号相同并且对应于同一个设备驱动。list所指向的链表就是由当前该设备驱动对应的设备文件索引节点组成。

我们现在回到globalmem_setup_cdev函数,它的主要作用就是申请并初始化一个cdev结构体,并且将通过cdev_add函数向系统内添加一个cdev,完成字符设备的注册。通常我们将cdev_add函数安排在字符设备驱动模块的加载函数中,而对应的将cdev_del函数放在字符设备驱动的卸载函数中。

static void globalmem_setup_cdev(struct globalmem_dev *dev, int index)
{
int err, devno = MKDEV(globalmem_major, index);

cdev_init(&dev->cdev, &globalmem_fops);
dev->cdev.owner = THIS_MODULE;
dev->cdev.ops = &globalmem_fops;
err = cdev_add(&dev->cdev, devno, 1);
if (err)
printk(KERN_NOTICE "Error %d adding cdev%d", err, index);
}

除此之外,globalmem_setup_cdev函数还会将cdev结构体中的struct file_operation类型的指针ops实例化。globalmem_fops全局变量是文件操作表,这个结构中含有许多文件操作函数类型的指针。当我们实现某些文件操作函数时,就可以将这些函数名赋值给这个结构中的相应变量。比如我们在稍候会实现globalmem_open函数,将其赋值给globalmem.open,那么当用户使用open系统调用对字符设备文件进行打开操作时,内核就会自动调用适合该设备文件的打开函数,也就是globalmem_open函数。

正如你所知的那样,Linux下一些皆为文件,当然设备也不例外。对于一个设备文件来说,用户通过VFS可以使用统一的系统调用接口对各种设备(文件)进行相关操作,比如open,read,write等等,用户可以不去考虑当前设备具体如何去操作。而在VFS层下——位于操作系统中的设备驱动就会对于每种设备去实现相应的操作函数。对于每类设备所实现的操作如何在用户层统一的表现出来,这就需要struct file_operations结构体。此结构体中包含大量的函数指针,这些函数指针便是用户层上统一的系统调用函数名,将设备驱动中实现的具体操作函数赋值给这些函数指针后,用户就可以使用统一的系统调用函数了。

接下来我们来看具体的文件操作函数是如何实现的。
文件打开函数将设备结构体指针赋值给私有数据,这个私有数据会在稍候的read以及write中被用到,而不是直接的使用globalmem_devp。

/*文件打开函数*/
int globalmem_open(struct inode *inode, struct file *filp)
{
  /*将设备结构体指针赋值给文件私有数据指针*/
  filp->private_data = globalmem_devp;
  return 0;
}

在读函数中,首先将私有数据赋值给一个设备结构体指针。然后,判断要读的长度是否合法。接着利用copy_to_user函数内核空间的数据(dev->mem)拷贝到用户空间。关于这个copy_to_user函数的详细拷贝过程,我们也可以对其进行代码分析。如果拷贝成功,那么修改相应的指针即可完毕读操作。

/*读函数*/
static ssize_t globalmem_read(struct file *filp, char __user *buf, size_t size,
  loff_t *ppos)
{
  unsigned long p =  *ppos;
  unsigned int count = size;
  int ret = 0;
  struct globalmem_dev *dev = filp->private_data; /*获得设备结构体指针*/

  /*分析获取有效的写长度*/
  if (p >= GLOBALMEM_SIZE)
    return count ?  - ENXIO: 0;
  if (count > GLOBALMEM_SIZE - p)
    count = GLOBALMEM_SIZE - p;

  /*从内核空间向用户空间写数据*/
  if (copy_to_user(buf, (void*)(dev->mem + p), count))
  {
    ret =  - EFAULT;
  }
  else
  {
    *ppos += count;
    ret = count;

    printk(KERN_INFO "read %d bytes(s) from %d\n", count, p);
  }

  return ret;
}

写函数与读函数的过程大体一直,不同的是使用了copy_from_user函数。这里不再详解。

接下来我们就可以使用一个简单测试程序来对我们所实现的字符设备驱动进行测试了。

windows 7 ultimate product key

windows 7 ultimate product key

winrar download free

winrar download free

winzip registration code

winzip registration code

winzip free download

winzip free download

winzip activation code

winzip activation code

windows 7 key generator

windows 7 key generator

winzip freeware

winzip freeware

winzip free download full version

winzip free download full version

free winrar download

free winrar download

free winrar

free winrar

windows 7 crack

windows 7 crack

windows xp product key

windows xp product key

windows 7 activation crack

windows7 activation crack

free winzip

free winzip

winrar free download

winrar free download

winrar free

winrar free

download winrar free

download winrar free

windows 7 product key

windows 7 product key